Real-time web chat using PHP to implement websocket
Nov 30, 2016 pm 11:59 PM前言
websocket 作為 HTML5 里一個(gè)新的特性一直很受人關(guān)注,因?yàn)樗娴姆浅?幔蚱屏?http “請(qǐng)求-響應(yīng)”的常規(guī)思維,實(shí)現(xiàn)了服務(wù)器向客戶(hù)端主動(dòng)推送消息,本文介紹如何使用 PHP 和 JS 應(yīng)用 websocket 實(shí)現(xiàn)一個(gè)網(wǎng)頁(yè)實(shí)時(shí)聊天室;
以前寫(xiě)過(guò)一篇文章講述如何使用ajax長(zhǎng)輪詢(xún)實(shí)現(xiàn)網(wǎng)頁(yè)實(shí)時(shí)聊天,見(jiàn)鏈接: 網(wǎng)頁(yè)實(shí)時(shí)聊天之js和jQuery實(shí)現(xiàn)ajax長(zhǎng)輪詢(xún) ,但是輪詢(xún)和服務(wù)器的 pending 都是無(wú)謂的消耗,websocket 才是新的趨勢(shì)。
最近艱難地“擠”出了一點(diǎn)時(shí)間,完善了很早之前做的 websocket “請(qǐng)求-原樣返回”服務(wù)器,用js完善了下客戶(hù)端功能,把過(guò)程和思路分享給大家,順便也普及一下 websocket 相關(guān)的知識(shí),當(dāng)然現(xiàn)在討論 websocket 的文章也特別多,有些理論性的東西我也就略過(guò)了,給出參考文章供大家選擇閱讀。
正文開(kāi)始前,先貼一張聊天室的效果圖(請(qǐng)不要在意CSS渣的頁(yè)面):
然后當(dāng)然是源碼: 我是源碼鏈接 - github - 枕邊書(shū)
websocket
Introduction
WebSocket is not a technology, but a brand new protocol. It uses TCP's Socket (socket) and defines a new important capability for network applications: full-duplex transmission and two-way communication between the client and the server. It is a new trend for servers to push client messages after Java applets, XMLHttpRequest, Adobe Flash, ActiveXObject, and various Comet technologies.
Relationship with http
In terms of network layering, websocket and http protocols are both application layer protocols. They are both based on the tcp transport layer. However, when websocket establishes a connection, it borrows the 101 switch protocol of http to achieve protocol conversion (Upgrade). Switch from HTTP protocol to WebSocket communication protocol. This action protocol is called "handshake";
After the handshake is successful, websocket uses the method specified by its own protocol to communicate, and has nothing to do with http.
Handshake
Here is a typical handshake http header sent by my own browser:
After the server receives the handshake request, it extracts the "Sec-WebSocket-Key" field in the request header, recovers a fixed string '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', then performs sha1 encryption, and finally converts it to Base64 encoding, used as the key and returned to the client in the "Sec-WebSocket-Accept" field. After the client matches this key, the connection is established and the handshake is completed;
Data transfer
Websocket has its own specified data transmission format, called Frame. The following figure is the structure of a data frame, where the unit is bit:
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-------+-+-------------+-------------------------------+ |F|R|R|R| opcode|M| Payload len | Extended payload length | |I|S|S|S| (4) |A| (7) | (16/64) | |N|V|V|V| |S| | (if payload len==126/127) | | |1|2|3| |K| | | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + | Extended payload length continued, if payload len == 127 | + - - - - - - - - - - - - - - - +-------------------------------+ | |Masking-key, if MASK set to 1 | +-------------------------------+-------------------------------+ | Masking-key (continued) | Payload Data | +-------------------------------- - - - - - - - - - - - - - - - + : Payload Data continued ... : + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Payload Data continued ... | +---------------------------------------------------------------+
For the specific meaning of each field, if you are interested, you can read this article The WebSocket Protocol 5. The data frame feels that it is not very flexible in binary operations, so there is no challenge to write an algorithm to parse the data. The following data Frame parsing and encapsulation are both online algorithms used.
However, in my work when writing payment gateways, data hexadecimal operations are often used. This must be carefully studied and summarized. Well, write it down first.
PHP implements websocket server
If PHP implements websocket, it mainly uses PHP’s socket function library:
PHP’s socket function library is very similar to the socket function in C language. I have read APUE once before, so I think it is quite easy to understand. After reading the socket function in the PHP manual, I think everyone can also have a certain understanding of socket programming in PHP.
The functions used will be briefly commented in the code below.
File descriptor
You may be a little surprised by the sudden mention of 'file descriptor'.
But as a server, it is necessary to store and identify the connected socket. Each socket represents a user. How to associate and query the correspondence between user information and socket is a problem. Here a little trick about file descriptors is applied.
We know that Linux is 'everything is a file', and the socket implementation in C language is a 'file descriptor'. This file descriptor is generally an int value that increases in the order in which the file is opened, increasing from 0 (of course The system has limitations). Each socket corresponds to a file, and reading and writing sockets operate on the corresponding file, so the read and write functions can also be applied like a file system.
tips: In Linux, standard input corresponds to file descriptor 0; standard output corresponds to file descriptor 1; standard error corresponds to file descriptor 2; so we can use 0 1 2 to redirect input and output.
Then PHP sockets similar to C sockets naturally inherit this, and the sockets they create are also of type resource types such as int with a value of 4 5. We can use the (int) or intval() function to convert the socket into a unique ID, so that a 'class index array' can be used to store socket resources and corresponding user information;
The result is similar:
$connected_sockets = array( (int)$socket => array( 'resource' => $socket, 'name' => $name, 'ip' => $ip, 'port' => $port, ... ) )
Create server socket
The following is a piece of code to create a server socket:
// 創(chuàng)建一個(gè) TCP socket, 此函數(shù)的可選值在官方文檔中寫(xiě)得十分詳細(xì),這里不再提了 $this->master = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); // 設(shè)置IP和端口重用,在重啟服務(wù)器后能重新使用此端口; socket_set_option($this->master, SOL_SOCKET, SO_REUSEADDR, 1); // 將IP和端口綁定在服務(wù)器socket上; socket_bind($this->master, $host, $port); // listen函數(shù)使主動(dòng)連接套接口變?yōu)楸贿B接套接口,使得此 socket 能被其他 socket 訪問(wèn),從而實(shí)現(xiàn)服務(wù)器功能。后面的參數(shù)則是自定義的待處理socket的最大數(shù)目,并發(fā)高的情況下,這個(gè)值可以設(shè)置大一點(diǎn),雖然它也受系統(tǒng)環(huán)境的約束。 socket_listen($this->master, self::LISTEN_SOCKET_NUM);
這樣,我們就得到一個(gè)服務(wù)器 socket,當(dāng)有客戶(hù)端連接到此 socket 上時(shí),它將改變狀態(tài)為可讀,那就看接下來(lái)服務(wù)器的處理邏輯了。
服務(wù)器邏輯
這里著重講一下 socket_select($read, $write, $except, $tv_sec [, $tv_usec])
:
select 函數(shù)使用傳統(tǒng)的 select 模型,可讀、寫(xiě)、異常的 socket 會(huì)被分別放入 $socket, $write, $except 數(shù)組中,然后返回 狀態(tài)改變的 socket 的數(shù)目,如果發(fā)生了錯(cuò)誤,函數(shù)將會(huì)返回 false.
需要注意的是最后兩個(gè)時(shí)間參數(shù),它們只有單位不同,可以搭配使用,用來(lái)表示 socket_select 阻塞的時(shí)長(zhǎng),為0時(shí)此函數(shù)立即返回,可以用于輪詢(xún)機(jī)制。 為 NULL 時(shí),函數(shù)會(huì)一直阻塞下去, 這里我們置 $tv_sec 參數(shù)為null,讓它一直阻塞,直到有可操作的 socket 返回。
下面是服務(wù)器的主要邏輯:
$write = $except = NULL; $sockets = array_column($this->sockets, 'resource'); // 獲取到全部的 socket 資源 $read_num = socket_select($sockets, $write, $except, NULL); foreach ($sockets as $socket) { // 如果可讀的是服務(wù)器 socket, 則處理連接邏輯; if ($socket == $this->master) { socket_accept($this->master); // socket_accept() 接受 請(qǐng)求 “正在 listen 的 socket(像我們的服務(wù)器 socket )” 的連接, 并一個(gè)客戶(hù)端 socket, 錯(cuò)誤時(shí)返回 false; self::connect($client); continue; } // 如果可讀的是其他已連接 socket ,則讀取其數(shù)據(jù),并處理應(yīng)答邏輯 } else { // 函數(shù) socket_recv() 從 socket 中接受長(zhǎng)度為 len 字節(jié)的數(shù)據(jù),并保存在 $buffer 中。 $bytes = @socket_recv($socket, $buffer, 2048, 0); if ($bytes < 9) { // 當(dāng)客戶(hù)端忽然中斷時(shí),服務(wù)器會(huì)接收到一個(gè) 8 字節(jié)長(zhǎng)度的消息(由于其數(shù)據(jù)幀機(jī)制,8字節(jié)的消息我們認(rèn)為它是客戶(hù)端異常中斷消息),服務(wù)器處理下線邏輯,并將其封裝為消息廣播出去 $recv_msg = $this->disconnect($socket); } else { // 如果此客戶(hù)端還未握手,執(zhí)行握手邏輯 if (!$this->sockets[(int)$socket]['handshake']) { self::handShake($socket, $buffer); continue; } else { $recv_msg = self::parse($buffer); } } // 廣播消息 $this->broadcast($msg); } } }
這里只是服務(wù)器處理消息的基礎(chǔ)代碼,日志記錄和異常處理都略過(guò)了,而且還有些數(shù)據(jù)幀解析和封裝的方法,各位也不一定看愛(ài),有興趣的可以去 github 上支持一下我的源碼~~
此外,為了便于服務(wù)器與客戶(hù)端的交互,我自己定義了 json 類(lèi)型的消息格式,形似:
$msg = [ 'type' => $msg_type, // 有普通消息,上下線消息,服務(wù)器消息 'from' => $msg_resource, // 消息來(lái)源 'content' => $msg_content, // 消息內(nèi)容 'user_list' => $uname_list, // 便于同步當(dāng)前在線人數(shù)與姓名 ];
客戶(hù)端
創(chuàng)建客戶(hù)端
前端我們使用 js 調(diào)用 Websocket 方法很簡(jiǎn)單就能創(chuàng)建一個(gè) websocket 連接,服務(wù)器會(huì)為幫我們完成連接、握手的操作,js 使用事件機(jī)制來(lái)處理瀏覽器與服務(wù)器的交互:
// 創(chuàng)建一個(gè) websocket 連接 var ws = new WebSocket("ws://127.0.0.1:8080"); // websocket 創(chuàng)建成功事件 ws.onopen = function () { }; // websocket 接收到消息事件 ws.onmessage = function (e) { var msg = JSON.parse(e.data); } // websocket 錯(cuò)誤事件 ws.onerror = function () { };
發(fā)送消息也很簡(jiǎn)單,直接調(diào)用 ws.send(msg)
方法就行了。
頁(yè)面功能
頁(yè)面部分主要是讓用戶(hù)使用起來(lái)方便,這里給消息框 textarea 添加了一個(gè)鍵盤(pán)監(jiān)控事件,當(dāng)用戶(hù)按下回車(chē)鍵時(shí)直接發(fā)送消息;
function confirm(event) { var key_num = event.keyCode; if (13 == key_num) { send(); } else { return false; } }
還有用戶(hù)打開(kāi)客戶(hù)端時(shí)生成一個(gè)默認(rèn)唯一用戶(hù)名;
然后是一些對(duì)數(shù)據(jù)的解析構(gòu)造,對(duì)客戶(hù)端頁(yè)面的更新,這里就不再啰嗦了,感興趣的可以看源碼。
用戶(hù)名異步處理
這里不得不提一下用戶(hù)登陸時(shí)確定用戶(hù)名時(shí)的一個(gè)小問(wèn)題,我原來(lái)是想在客戶(hù)端創(chuàng)建一個(gè)連接后直接發(fā)送用戶(hù)名到服務(wù)器,可是控制臺(tái)里報(bào)出了 “websocket 仍在連接中或已關(guān)閉” 的錯(cuò)誤信息。
Uncaught DOMException: Failed to execute 'send' on 'WebSocket': Still in CONNECTING state.
考慮到連接可能還沒(méi)處理好,我就實(shí)現(xiàn)了 sleep 方法等了一秒再發(fā)送用戶(hù)名,可是錯(cuò)誤仍然存在。
后來(lái)忽然想到 js 的單線程阻塞機(jī)制,才明白使用 sleep 一直阻塞也是沒(méi)有用的,利用好 js 的事件機(jī)制才是正道:于是在服務(wù)器端添加邏輯,在握手成功后,向客戶(hù)端發(fā)送握手已成功的消息;客戶(hù)端先將用戶(hù)名存入一個(gè)全局變量,接收到服務(wù)器的握手成功的提醒消息后再發(fā)送用戶(hù)名,于是成功在第一時(shí)間更新用戶(hù)名。
小結(jié)
聊天室擴(kuò)展方向
簡(jiǎn)易聊天室已經(jīng)完成,當(dāng)然還要給它帶有希望的美好未來(lái),希望有人去實(shí)現(xiàn):
- 頁(yè)面美化(信息添加顏色等)
- 服務(wù)器識(shí)別 '@' 字符而只向某一個(gè) socket 寫(xiě)數(shù)據(jù)實(shí)現(xiàn)聊天室的私聊;
- 多進(jìn)程(使用 redis 等緩存數(shù)據(jù)庫(kù)來(lái)實(shí)現(xiàn)資源的共享),可參考我以前的一篇文章: 初探PHP多進(jìn)程
- 消息記錄數(shù)據(jù)庫(kù)持久化(log 日志還是不方便分析)
- ...
總結(jié)
多讀些經(jīng)典書(shū)籍還是很有用的,有些東西真的是觸類(lèi)旁通,APUE/UNP 還是要再多翻幾遍。此外互聯(lián)網(wǎng)技術(shù)日新月異,挑一些自己喜歡的學(xué)習(xí)一下,跟大家分享一下也是挺舒服的(雖然程序和博客加一塊用了至少10個(gè)小時(shí)...)。
參考:
websocket協(xié)議翻譯
刨根問(wèn)底 HTTP 和 WebSocket 協(xié)議(下)
學(xué)習(xí)WebSocket協(xié)議—從頂層到底層的實(shí)現(xiàn)原理(修訂版)
嗯,持續(xù)更新。喜歡的可以點(diǎn)個(gè)推薦或關(guān)注,有錯(cuò)漏之處,請(qǐng)指正,謝謝。

Hot AI Tools

Undress AI Tool
Undress images for free

Undresser.AI Undress
AI-powered app for creating realistic nude photos

AI Clothes Remover
Online AI tool for removing clothes from photos.

Clothoff.io
AI clothes remover

Video Face Swap
Swap faces in any video effortlessly with our completely free AI face swap tool!

Hot Article

Hot Tools

Notepad++7.3.1
Easy-to-use and free code editor

SublimeText3 Chinese version
Chinese version, very easy to use

Zend Studio 13.0.1
Powerful PHP integrated development environment

Dreamweaver CS6
Visual web development tools

SublimeText3 Mac version
God-level code editing software (SublimeText3)
