PHP實(shí)現(xiàn)WebSocket實(shí)例詳解
WebSocket 是什么?
摘抄網(wǎng)上的一些解釋?zhuān)?/p>
WebSocket 協(xié)議是基于 TCP 的一種新的網(wǎng)絡(luò)協(xié)議。它實(shí)現(xiàn)了瀏覽器與服務(wù)器全雙工(full-duplex)通信——允許服務(wù)器主動(dòng)發(fā)送信息給客戶(hù)端。
WebSocket 通信協(xié)議于2011年被 IETF 定為標(biāo)準(zhǔn) RFC 6455,并被 RFC7936 所補(bǔ)充規(guī)范。
—— 百度百科
WebSocket 是一個(gè)持久化的協(xié)議,這是相對(duì)于 http 非持久化來(lái)說(shuō)的。
舉個(gè)簡(jiǎn)單的例子,http1.0 的生命周期是以 request 作為界定的,也就是一個(gè) request,一個(gè) response,對(duì)于 http 來(lái)說(shuō),本次 client 與 server 的會(huì)話(huà)到此結(jié)束;而在 http1.1 中,稍微有所改進(jìn),即添加了 keep-alive,也就是在一個(gè) http 連接中可以進(jìn)行多個(gè) request 請(qǐng)求和多個(gè) response 接受操作。然而在實(shí)時(shí)通信中,并沒(méi)有多大的作用,http 只能由 client 發(fā)起請(qǐng)求,server 才能返回信息,即 server 不能主動(dòng)向 client 推送信息,無(wú)法滿(mǎn)足實(shí)時(shí)通信的要求。而 WebSocket 可以進(jìn)行持久化連接,即 client 只需進(jìn)行一次握手,成功后即可持續(xù)進(jìn)行數(shù)據(jù)通信,值得關(guān)注的是 WebSocket 實(shí)現(xiàn) client 與 server 之間全雙工通信,即 server 端有數(shù)據(jù)更新時(shí)可以主動(dòng)推送給 client 端。
上圖是一個(gè)演示client和server之間建立WebSocket連接時(shí)握手部分
client 建立 WebSocket 時(shí)向服務(wù)器端請(qǐng)求的信息
GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket //告訴服務(wù)器現(xiàn)在發(fā)送的是WebSocket協(xié)議 Connection: Upgrade Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw== //是一個(gè)Base64 encode的值,這個(gè)是瀏覽器隨機(jī)生成的,用于驗(yàn)證服務(wù)器端返回?cái)?shù)據(jù)是否是WebSocket助理 Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13 Origin: http://example.com
服務(wù)器獲取到 client 請(qǐng)求的信息后,根據(jù) WebSocket 協(xié)議對(duì)數(shù)據(jù)進(jìn)行處理并返回,其中要對(duì) Sec-WebSocket-Key 進(jìn)行加密等操作
HTTP/1.1 101 Switching Protocols Upgrade: websocket //依然是固定的,告訴客戶(hù)端即將升級(jí)的是Websocket協(xié)議,而不是mozillasocket,lurnarsocket或者shitsocket Connection: Upgrade Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk= //這個(gè)則是經(jīng)過(guò)服務(wù)器確認(rèn),并且加密過(guò)后的 Sec-WebSocket-Key,也就是client要求建立WebSocket驗(yàn)證的憑證 Sec-WebSocket-Protocol: chat
PHP 服務(wù)端
<?php if(($socket = socket_create(AF_INET,SOCK_STREAM,SOL_TCP)) < 0) { echo "socket_create() 失敗的原因是:".socket_strerror($sock)."\n"; } if(($ret = socket_bind($socket,'127.0.0.1','9090')) < 0) { echo "socket_bind() 失敗的原因是:".socket_strerror($ret)."\n"; } if(($ret = socket_listen($socket,3)) < 0) { echo "socket_listen() 失敗的原因是:".socket_strerror($ret)."\n"; } $all_sockets = [$socket]; // socket 集合 do { $copy_sockets = $all_sockets; // 單獨(dú)拷貝一份 // 因?yàn)榭蛻?hù)端是長(zhǎng)連接,如果客戶(hù)端非正常斷開(kāi),服務(wù)端會(huì)在 socket_accept 阻塞,現(xiàn)在使用 select 非阻塞模式 socket if(socket_select($copy_sockets, $write, $except, 0) === false) exit('sosket_select error!'); // 接收第一次 socket 連入,連入后移除服務(wù)端 socket if(in_array($socket, $copy_sockets)) { $client = socket_accept($socket); if($client) { $buf = socket_read($client, 1024); echo $buf; // 匹配 Sec-Websocket-Key 標(biāo)識(shí) if (preg_match("/Sec-WebSocket-Key: (.*)\r\n/i",$buf,$match)) { // 需要將 Sec-WebSocket-Key 值累加字符串,并依次進(jìn)行 SHA-1 加密和 base64 加密 $key = base64_encode(sha1($match[1] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11',true)); // 拼湊響應(yīng)內(nèi)容 $res= "HTTP/1.1 101 Switching Protocol".PHP_EOL ."Upgrade: WebSocket".PHP_EOL ."Connection: Upgrade".PHP_EOL ."WebSocket-Location: ws://127.0.0.1:9090".PHP_EOL ."Sec-WebSocket-Accept: " . $key .PHP_EOL.PHP_EOL; // 注意這里,需要兩個(gè)換行 // 向客戶(hù)端應(yīng)答 Sec-WebSocket-Accept socket_write($client, $res, strlen($res)); // 向客戶(hù)端發(fā)送消息 socket_write($client, buildMsg('socket ok'), 1024); // 加入客戶(hù)端 socket $all_sockets[] = $client; } // 移除服務(wù)端 socket $key = array_search($socket, $copy_sockets); unset($copy_sockets[$key]); // socket_close($client); } } // 循環(huán)所有客戶(hù)端 sockets foreach ($copy_sockets as $s) { // 獲取客戶(hù)端發(fā)給服務(wù)端的內(nèi)容 $buf = socket_read($s, 8024); echo strlen($buf).'---'.PHP_EOL; // 代表客戶(hù)端主動(dòng)關(guān)閉 if(strlen($buf) < 9) { $key = array_search($s, $all_sockets); unset($all_sockets[$key]); socket_close($s); continue; } // 輸出 echo getMsg($buf).PHP_EOL; } }while(true); socket_close($socket); // 編碼服務(wù)端向客戶(hù)端發(fā)送的內(nèi)容 function buildMsg($msg) { $frame = []; $frame[0] = '81'; $len = strlen($msg); if ($len < 126) { $frame[1] = $len < 16 ? '0' . dechex($len) : dechex($len); } else if ($len < 65025) { $s = dechex($len); $frame[1] = '7e' . str_repeat('0', 4 - strlen($s)) . $s; } else { $s = dechex($len); $frame[1] = '7f' . str_repeat('0', 16 - strlen($s)) . $s; } $data = ''; $l = strlen($msg); for ($i = 0; $i < $l; $i++) { $data .= dechex(ord($msg{$i})); } $frame[2] = $data; $data = implode('', $frame); return pack("H*", $data); } // 解析客戶(hù)端向服務(wù)端發(fā)送的內(nèi)容 function getMsg($buffer) { $res = ''; $len = ord($buffer[1]) & 127; if ($len === 126) { $masks = substr($buffer, 4, 4); $data = substr($buffer, 8); } else if ($len === 127) { $masks = substr($buffer, 10, 4); $data = substr($buffer, 14); } else { $masks = substr($buffer, 2, 4); $data = substr($buffer, 6); } for ($index = 0; $index < strlen($data); $index++) { $res .= $data[$index] ^ $masks[$index % 4]; } return $res; }
客戶(hù)端
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <script> // 創(chuàng)建一個(gè)Socket實(shí)例 var socket = new WebSocket('ws://localhost:9090'); // 打開(kāi)Socket socket.onopen = function(event) { // 發(fā)送一個(gè)初始化消息 socket.send("init msg"); }; socket.onmessage = function(event) { console.log('收到消息',event); }; // 監(jiān)聽(tīng)Socket的關(guān)閉 socket.onclose = function(event) { console.log('關(guān)閉監(jiān)聽(tīng)',event); }; function send() { socket.send("client msg"); } </script> </head> <body> <button onclick="send()">發(fā)送消息</button> </body> </html>
運(yùn)行測(cè)試:
Client
Server
到此這篇關(guān)于PHP實(shí)現(xiàn)WebSocket實(shí)例詳解的文章就介紹到這了,更多相關(guān)PHP實(shí)現(xiàn)WebSocket內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Laravel 中創(chuàng)建 Zip 壓縮文件并提供下載的實(shí)現(xiàn)方法
這篇文章主要介紹了Laravel 中創(chuàng)建 Zip 壓縮文件并提供下載,本文通過(guò)兩個(gè)任務(wù),實(shí)例代碼相結(jié)合的形式給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-04-04在 Laravel 6 中緩存數(shù)據(jù)庫(kù)查詢(xún)結(jié)果的方法
這篇文章主要介紹了在 Laravel 6 中緩存數(shù)據(jù)庫(kù)查詢(xún)結(jié)果的方法,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-12-12php安裝擴(kuò)展mysqli的實(shí)現(xiàn)步驟及報(bào)錯(cuò)解決辦法
這篇文章主要介紹了 php安裝擴(kuò)展mysqli的實(shí)現(xiàn)步驟及報(bào)錯(cuò)解決辦法的相關(guān)資料,希望通過(guò)本文能幫助到大家,需要的朋友可以參考下2017-09-09使用PHPUnit進(jìn)行單元測(cè)試并生成代碼覆蓋率報(bào)告的方法
這篇文章主要介紹了使用PHPUnit進(jìn)行單元測(cè)試并生成代碼覆蓋率報(bào)告的方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-03-03WordPress中訪(fǎng)客登陸實(shí)現(xiàn)郵件提醒的PHP腳本實(shí)例分享
這篇文章主要介紹了WordPress中訪(fǎng)客登陸實(shí)現(xiàn)郵件提醒的PHP腳本實(shí)例分享,類(lèi)似于社交網(wǎng)站的異地IP登陸提醒,不過(guò)IP所在地顯示的實(shí)現(xiàn)并沒(méi)有在本文介紹范圍中,需要的朋友可以參考下2015-12-12php調(diào)用dll的實(shí)例操作動(dòng)畫(huà)與代碼分享
這是我錄制的一個(gè)gif操作動(dòng)畫(huà),圖片比較大,如果大家在線(xiàn)看圖感覺(jué)不流暢的話(huà)可以把圖片保存到本機(jī)再看2012-08-08使用Git實(shí)現(xiàn)Laravel項(xiàng)目的自動(dòng)化部署
這篇文章主要介紹了使用Git實(shí)現(xiàn)Laravel項(xiàng)目的自動(dòng)化部署,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11PHP快速按行讀取CSV大文件的封裝類(lèi)分享(也適用于其它超大文本文件)
這篇文章主要介紹了一個(gè)PHP快速按行讀取CSV大文件的封裝類(lèi),這個(gè)類(lèi)同時(shí)也適用于其它體積較大的文本文件,需要的朋友可以參考下2014-04-04