Real-time web chat using PHP to implement websocket
Nov 30, 2016 pm 11:59 PM前言
websocket 作為 HTML5 里一個新的特性一直很受人關(guān)注,因為它真的非??幔蚱屏?http “請求-響應(yīng)”的常規(guī)思維,實現(xiàn)了服務(wù)器向客戶端主動推送消息,本文介紹如何使用 PHP 和 JS 應(yīng)用 websocket 實現(xiàn)一個網(wǎng)頁實時聊天室;
以前寫過一篇文章講述如何使用ajax長輪詢實現(xiàn)網(wǎng)頁實時聊天,見鏈接: 網(wǎng)頁實時聊天之js和jQuery實現(xiàn)ajax長輪詢 ,但是輪詢和服務(wù)器的 pending 都是無謂的消耗,websocket 才是新的趨勢。
最近艱難地“擠”出了一點時間,完善了很早之前做的 websocket “請求-原樣返回”服務(wù)器,用js完善了下客戶端功能,把過程和思路分享給大家,順便也普及一下 websocket 相關(guān)的知識,當(dāng)然現(xiàn)在討論 websocket 的文章也特別多,有些理論性的東西我也就略過了,給出參考文章供大家選擇閱讀。
正文開始前,先貼一張聊天室的效果圖(請不要在意CSS渣的頁面):
然后當(dāng)然是源碼: 我是源碼鏈接 - github - 枕邊書
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)建一個 TCP socket, 此函數(shù)的可選值在官方文檔中寫得十分詳細(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ù)使主動連接套接口變?yōu)楸贿B接套接口,使得此 socket 能被其他 socket 訪問,從而實現(xiàn)服務(wù)器功能。后面的參數(shù)則是自定義的待處理socket的最大數(shù)目,并發(fā)高的情況下,這個值可以設(shè)置大一點,雖然它也受系統(tǒng)環(huán)境的約束。 socket_listen($this->master, self::LISTEN_SOCKET_NUM);
這樣,我們就得到一個服務(wù)器 socket,當(dāng)有客戶端連接到此 socket 上時,它將改變狀態(tài)為可讀,那就看接下來服務(wù)器的處理邏輯了。
服務(wù)器邏輯
這里著重講一下 socket_select($read, $write, $except, $tv_sec [, $tv_usec])
:
select 函數(shù)使用傳統(tǒng)的 select 模型,可讀、寫、異常的 socket 會被分別放入 $socket, $write, $except 數(shù)組中,然后返回 狀態(tài)改變的 socket 的數(shù)目,如果發(fā)生了錯誤,函數(shù)將會返回 false.
需要注意的是最后兩個時間參數(shù),它們只有單位不同,可以搭配使用,用來表示 socket_select 阻塞的時長,為0時此函數(shù)立即返回,可以用于輪詢機制。 為 NULL 時,函數(shù)會一直阻塞下去, 這里我們置 $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() 接受 請求 “正在 listen 的 socket(像我們的服務(wù)器 socket )” 的連接, 并一個客戶端 socket, 錯誤時返回 false; self::connect($client); continue; } // 如果可讀的是其他已連接 socket ,則讀取其數(shù)據(jù),并處理應(yīng)答邏輯 } else { // 函數(shù) socket_recv() 從 socket 中接受長度為 len 字節(jié)的數(shù)據(jù),并保存在 $buffer 中。 $bytes = @socket_recv($socket, $buffer, 2048, 0); if ($bytes < 9) { // 當(dāng)客戶端忽然中斷時,服務(wù)器會接收到一個 8 字節(jié)長度的消息(由于其數(shù)據(jù)幀機制,8字節(jié)的消息我們認(rèn)為它是客戶端異常中斷消息),服務(wù)器處理下線邏輯,并將其封裝為消息廣播出去 $recv_msg = $this->disconnect($socket); } else { // 如果此客戶端還未握手,執(zhí)行握手邏輯 if (!$this->sockets[(int)$socket]['handshake']) { self::handShake($socket, $buffer); continue; } else { $recv_msg = self::parse($buffer); } } // 廣播消息 $this->broadcast($msg); } } }
這里只是服務(wù)器處理消息的基礎(chǔ)代碼,日志記錄和異常處理都略過了,而且還有些數(shù)據(jù)幀解析和封裝的方法,各位也不一定看愛,有興趣的可以去 github 上支持一下我的源碼~~
此外,為了便于服務(wù)器與客戶端的交互,我自己定義了 json 類型的消息格式,形似:
$msg = [ 'type' => $msg_type, // 有普通消息,上下線消息,服務(wù)器消息 'from' => $msg_resource, // 消息來源 'content' => $msg_content, // 消息內(nèi)容 'user_list' => $uname_list, // 便于同步當(dāng)前在線人數(shù)與姓名 ];
客戶端
創(chuàng)建客戶端
前端我們使用 js 調(diào)用 Websocket 方法很簡單就能創(chuàng)建一個 websocket 連接,服務(wù)器會為幫我們完成連接、握手的操作,js 使用事件機制來處理瀏覽器與服務(wù)器的交互:
// 創(chuàng)建一個 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 錯誤事件 ws.onerror = function () { };
發(fā)送消息也很簡單,直接調(diào)用 ws.send(msg)
方法就行了。
頁面功能
頁面部分主要是讓用戶使用起來方便,這里給消息框 textarea 添加了一個鍵盤監(jiān)控事件,當(dāng)用戶按下回車鍵時直接發(fā)送消息;
function confirm(event) { var key_num = event.keyCode; if (13 == key_num) { send(); } else { return false; } }
還有用戶打開客戶端時生成一個默認(rèn)唯一用戶名;
然后是一些對數(shù)據(jù)的解析構(gòu)造,對客戶端頁面的更新,這里就不再啰嗦了,感興趣的可以看源碼。
用戶名異步處理
這里不得不提一下用戶登陸時確定用戶名時的一個小問題,我原來是想在客戶端創(chuàng)建一個連接后直接發(fā)送用戶名到服務(wù)器,可是控制臺里報出了 “websocket 仍在連接中或已關(guān)閉” 的錯誤信息。
Uncaught DOMException: Failed to execute 'send' on 'WebSocket': Still in CONNECTING state.
考慮到連接可能還沒處理好,我就實現(xiàn)了 sleep 方法等了一秒再發(fā)送用戶名,可是錯誤仍然存在。
后來忽然想到 js 的單線程阻塞機制,才明白使用 sleep 一直阻塞也是沒有用的,利用好 js 的事件機制才是正道:于是在服務(wù)器端添加邏輯,在握手成功后,向客戶端發(fā)送握手已成功的消息;客戶端先將用戶名存入一個全局變量,接收到服務(wù)器的握手成功的提醒消息后再發(fā)送用戶名,于是成功在第一時間更新用戶名。
小結(jié)
聊天室擴展方向
簡易聊天室已經(jīng)完成,當(dāng)然還要給它帶有希望的美好未來,希望有人去實現(xiàn):
- 頁面美化(信息添加顏色等)
- 服務(wù)器識別 '@' 字符而只向某一個 socket 寫數(shù)據(jù)實現(xiàn)聊天室的私聊;
- 多進(jìn)程(使用 redis 等緩存數(shù)據(jù)庫來實現(xiàn)資源的共享),可參考我以前的一篇文章: 初探PHP多進(jìn)程
- 消息記錄數(shù)據(jù)庫持久化(log 日志還是不方便分析)
- ...
總結(jié)
多讀些經(jīng)典書籍還是很有用的,有些東西真的是觸類旁通,APUE/UNP 還是要再多翻幾遍。此外互聯(lián)網(wǎng)技術(shù)日新月異,挑一些自己喜歡的學(xué)習(xí)一下,跟大家分享一下也是挺舒服的(雖然程序和博客加一塊用了至少10個小時...)。
參考:
websocket協(xié)議翻譯
刨根問底 HTTP 和 WebSocket 協(xié)議(下)
學(xué)習(xí)WebSocket協(xié)議—從頂層到底層的實現(xiàn)原理(修訂版)
嗯,持續(xù)更新。喜歡的可以點個推薦或關(guān)注,有錯漏之處,請指正,謝謝。

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)
