# Swoole WebSocket 聊天室實作 :::success :bookmark: 書籤 [TOC] ::: # 參考資料 | 標題 | 網址 | | --- | --- | |Swoole WebSocket官方文件|https://wiki.swoole.com/wiki/page/397.html| |SQL Query Builder|https://github.com/nilportugues/php-sql-query-builder| --- # 什麼是WebSocket? WebSocket 是網路協定的一種, Client 可以透過此協定與 Server 做溝通,而他和一般 http 或 https 不同的是, WebSocket 協定只需透過一次連結便能保持連線,不必再透過一直發送 Request 來與 Server 互動 ## WebSocket各種方法比較 | 比較 | 短輪詢| 長輪詢 | SSE | WebSocket | | --- | --- | --- | --- | --- | |優點 |開發簡單瀏覽器支援度高|改善了短輪詢,無用請求的次數|進入瀏覽器與Server建立連線後,Server即可推送資料給Client,且不需要裝額外套件,上手簡單|進入瀏覽器與Server建立連線後,以http請求升級成ws/wws協定後,全雙工雙向輸出| |缺點|無用請求多,且降低每秒請求時間造成效能浪費|Server卡住請求仍然消耗性能|Server在查詢有沒有更新時耗費SQL效能|上手不易| |即時性|高,但消耗Server資源|高,但消耗Server資源|高|同步| --- # Http 與 WebSocket ## HTTP 1. 無狀態協議。 2. 短連接。(Ajax輪詢方式或Long poll方式實現"持久連接"狀態) 3. 被動型。客戶端請求->服務器端響應。服務端不能主動聯繫客戶端,只能有客戶端發起。 4. 每一個request對應一個response ## WebSocket 1. WebSocket是一種雙向通信協議。 2. 在建立連接後,WebSocket服務器端和客戶端都能主動向對方發送或接收數據,就像Socket一樣。 3. 解決了Http的被動性,當Server完成協定升級後HTTP->Websocket),Server就可以主動推送信息給Client啦。 ![](https://i.imgur.com/7kHLc7L.png) # 為什麼使用Swoole? ## Swoole介紹 Swoole 並不是一個新的程式語言也不是php框架,他是一個以純 C 語言編寫的 PHP Extension(擴充),簡單來說他有以下特點: * 純 C 語言編寫,所以性能超強 * 事件驅動 (Event Driven) * 異步 I/O * 支援 異步/同步/協程 (Coroutine) * 支援 TCP/UDP/UnixSock 通信協定 * 支援異步 Server 端和異步 Client 端 * 支援多行程 (Process) 與多執行緒 (Thread) * 支援 IPv4 與 IPv6 * 低 CPU 消耗與支援 daemon 模式 * 總結:你可以把Swoole想像成NodeJS,但對於PHP來說將有更高性能。 ## Swoole中的同步、非同步、阻塞、非阻塞 假設有一個Client,程式邏輯是要請求三個不同的Server,處理各自的response。 傳統模型當然是順序執行,先發送第一個請求,等待收到響應數據後再發送第二個請求,以此類推。就像是單核CPU,一次只能處理一件事,其他事情被暫時阻塞。 而並發模式可以讓三個server同時處理各自請求,這就可以使大量時間復用。 ![](https://i.imgur.com/AYuc6iD.png) ## 舉例 ### 同步阻塞: 在鄉下診所中,小明需要掛號才能進去看診,所以小明(調用者)必須等待護士(被調用者)掛號完成 對於護士(被調用者)來說,它們是"同步"的,所以小明(調用者)正在被護士(被調用者)"阻塞"在櫃檯。 補充:就像傳統FORM表單一請求一響應 ### 異步阻塞: 在都市診所中,小明想要掛號時,護士告知只需要把健保卡放到櫃台至等候區,等待護士呼叫付錢並看診 那麼這時候對於護士(被調用者)來說是"異步"的,可是對於小明(調用者)來說,還是被"阻塞"在等候區 補充:小明請求了護士,但是護士正在處理其他人的掛號叫你先到等候區,等到護士處理到你的就會響應你了 ### 同步非阻塞: 在鄉下診所中,小明需要掛號才能進去看診,但這次小明在等待掛號時滑手機做自己的事情,等待掛號完成。 對於護士(被調用者)來說,它們是"同步"的護士,但是小明(調用者)在等待掛號時,可以做自己的事情為"非阻塞" 補充:為什麼護士是同步的,因為護士還是必須一個一個來掛號看診 ### 異步非阻塞: 在都市診所中,小明掛號只需要把健保卡放置櫃台中至等候區,等待護士呼叫付錢並看診,而小明也在等候區玩手機 那麼這時候對於護士(被調用者)來說是"異步"的,對於小明(調用者)也可以做自己的事等待護士呼叫"非阻塞" * 同步:A調用B,此時只有等B有結果了才返回。 * 異步: A調用B,B立即返回,無須等待。當B處理完之後會通過通知或者回調函數的方式來告訴A結果。 * 阻塞:A調用B,A會被被掛起,一直在等待B的結果,什麼事都不能幹。 * 非阻塞:A調用B,自己用被掛起等待B的結果,可以去干其他的事情。 傳統上來說,一個客戶端發起的 request 在伺服器端處理時會有很多 I/O 的操作,而 I/O 通常是最耗費時間的,但是伺服器端會等相關的 I/O 處理完才會一次回傳給客戶端結果,此時便呈現阻塞的 (Blocked) 狀態,一旦伺服器該請求未完全處理完,其他客戶端的請求就必須等待。 而異步能讓伺服器在 I/O 未處理完時就繼續處理下一個用戶端的請求,當前面的 I/O 處理完成時再回覆前次的請求,藉由這種方式充份地使用的伺服器的效能。 在Server程序中如果需要執行很耗時的操作,比如一個聊天服務器發送廣播,Web服務器中發送郵件。如果直接去執行這些函數就會阻塞當前進程,導致服務器響應變慢。 # WebSocket 實作範例 ## 環境安裝 > https://hackmd.io/nP_deI8zS3OsyAbWbI51wg ## GitHub > https://github.com/Wudavio/Swoole-WebSocket ## WebSocket_Server.php Swoole WebSocket建立,第一個參數是Server監聽的ip,第二個參數是port 需與前端請求WebSocket交握一樣請參考index.php ``` $this->server = new Swoole\WebSocket\Server("0.0.0.0", 1234); ``` ### open Client 與 Server 連線後做什麼事情 ``` $server->on('open', function (Swoole\WebSocket\Server $server, $request) { echo "server: handshake success with fd{$request->fd}\n"; }); ``` ### message Client 傳送資料到 Server 後做什麼事情 ``` $server->on('message', function (Swoole\WebSocket\Server $server, $frame) { echo "編號:{$frame->fd},訊息:{$frame->data}\n"; $server->push($frame->fd, "this is server"); }); ``` ### close Client 關閉連線後做什麼事情 ``` $server->on('close', function ($ser, $fd) { echo "client {$fd} closed\n"; }); ``` ### push Server 推送給 Client 資料 ``` $server->push($frame->fd, "this is server"); ``` 例子(可應用在open、message、close...) ``` $server->on('message', function (Swoole\WebSocket\Server $server, $frame) { $server->push($frame->fd, "this is server"); }); ``` 以上都是單向推送,要往所有的Client推送的話必須使用以下方法 ``` // $server->connections 遍歷所有websocket連接Client的fd,给所有Client推送 foreach ($server->connections as $fd) { // 需要先判斷是否是正確的websocket連線,否則有可能會推送失敗 if ($server->isEstablished($fd)) { $server->push($fd, $request->get['message']); } } ``` ### request swoole_websocket_server 繼承了 swoole_http_server * 使用 onRequest 回調,websocket Server 也可以同時作為 Http Server * 未使用 onRequest 回調,websocket Server 收到 http 請求後會返回 http 400 錯誤 ``` $this->server->on('request', function ($request, $response) { // 接收來自http的請求從GET或POST參數的值,给Client推送 foreach ($this->server->connections as $fd) { if ($this->server->isEstablished($fd)) { $this->server->push($fd, $request->get['message']); } } }); ``` ### 完整例子 需要在Linux環境下執行 ![](https://i.imgur.com/pbLZF35.png) ``` <?php class Websocket_Server {//寫成物件可以在繼承model的方法調用 public $server; public function __construct() { //建立 websocket 物件,監聽 0.0.0.0:1234 連接埠 $this->server = new Swoole\WebSocket\Server("0.0.0.0", 1234); echo "成功建立WebSocket Server\n"; $this->server->on('open', function (swoole_websocket_server $server, $request) { //echo "server: handshake success with fd{$request->fd}\n"; //遍立所有連線的使用者並推送到所有的使用者 foreach ($this->server->connections as $fd) { // 需要先判断是否是正确的websocket连接,否则有可能会push失败 if ($this->server->isEstablished($fd)) { //open所push的會往message推 $this->server->push($fd, "第".$request->fd."位user online\n"); } } }); $this->server->on('message', function (Swoole\WebSocket\Server $server, $request) { // echo "receive from {$frame->fd}:{$frame->data},opcode:{$frame->opcode},fin:{$frame->finish}\n"; // $server->push($frame->fd, "this is server"); foreach ($this->server->connections as $fd) { // 需要先判断是否是正确的websocket连接,否则有可能会push失败 if ($this->server->isEstablished($fd)) { $this->server->push($fd, $request->data); } } }); $this->server->on('close', function ($ser, $fd) { echo "client {$fd} closed\n"; }); // swoole_websocket_server 继承自 swoole_http_server // 设置了 onRequest 回调,websocket 服务器也可以同时作为 http 服务器 // 未设置 onRequest 回调,websocket 服务器收到 http 请求后会返回http 400错误页面 // 如果想通过接收 http 触发所有 websocket 的推送,需要注意作用域的问题, // 面向过程请使用“global”对 swoole_websocket_server 进行引用,面向对象可以把 swoole_websocket_server 设置成一个成员属性 $this->server->on('request', function ($request, $response) { // 接收http请求从get获取message参数的值,给用户推送 // $this->server->connections 遍历所有websocket连接用户的fd,给所有用户推送 foreach ($this->server->connections as $fd) { // 需要先判断是否是正确的websocket连接,否则有可能会push失败 if ($this->server->isEstablished($fd)) { $this->server->push($fd, $request->get['message']); } } }); $this->server->start(); } } new Websocket_Server(); ?> ``` --- ## index.php ### WebSocket 事件 | 事件 | 事件處理程序 | 描述 | | ----- | -------------- |---------------------- | |open |Socket.onopen | 連接建立時觸發| |message|Socket.onmessage|客戶端接收服務端數據時觸發| |error |Socket.onerror |通信發生錯誤時觸發| |close |Socket.onclose |連接關閉時觸發| ### WebSocket 方法 |方法 |描述| |---|---| |Socket.send() |使用連接發送數據| |Socket.close() |關閉連接| ### 例子 ``` var wsServer = 'ws://localhost:1234';//請求port要跟server一樣 var websocket = new WebSocket(wsServer); /* websocket basic */ websocket.onopen = function (evt) { console.log("成功連接到 WebSocket 服務"); }; websocket.onclose = function (evt) { console.log(websocket.data); }; websocket.onmessage = function (evt) { let data = evt.data; let datasplit = data.split(","); if(datasplit.length > 1){ pushMessage(datasplit[0],datasplit[1]);//0編號1留言 } }; websocket.onerror = function (evt, e) { console.log('發生錯誤: ' + evt.data); }; ```