# 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);
};
```