WebSocket協議 與 IO多路復用
最近在把 Facebook Message 接入客服系統,由於與 Facebook Message 對接的收發消息都是通過調用 http 介面來實現的,如果想實現即時通訊,還需要在中間加一個 WebSocket 來轉發消息。如下圖:
其中用到了 WebSocket 協議和 IO多路復用相關的知識。在這裡做一個學習記錄。
為什麼需要 WebSocket 協議
- 因為 HTTP 協議有一個缺陷:通訊只能先由客戶端發起,然後伺服器再作出響應,並不能由伺服器主動向客戶端推送消息。
- WebSocket 協議最大的特點是,伺服器可以主動向客戶端推送資訊,客戶端也可以主動向伺服器發送資訊。
WebSocket 與 socket 的之間關係
- WebSocket 是一個網路通訊協議,是屬於網路七層模型中的應用層的協議,同樣屬於應用層的協議還有 HTTP 協議、FTP協議、SMTP協議等等。
- 而 socket 是作業系統提供的一套介面,利用這一套介面就可以編寫程式實現進程之間的通訊、網路通訊等功能。
一個 WebSocket 連接是如何建立起來的
WebSocket 連接的初期是基於 HTTP 協議的,假如 WebSocket 的地址是這個:wss://www.xxx.com/websocket ,在連接 WebSocket 的初期瀏覽器首先會向這個地址發出一個 HTTP GET 請求,請求頭資訊截圖如下:
紅色框標出的是比較重要的請求頭:
Connection: Upgrade
告訴服務端這個連接需要升級。Upgrade: websocket
告訴服務端需要升級到 WebSocket 協議。Sec-WebSocket-Key: d97OXZzuRlSJV/6SrX+uUA==
是瀏覽器隨機生成的一個字元串。
服務端接收到這個 HTTP 請求,會作出響應,響應頭的截圖如下:
紅色框標出的是比較重要的響應頭:
HTTP/1.1 101 Switching Protocols
告訴瀏覽器,服務端已經成功切換了協議。Sec-WebSocket-Accept: axMY+KY1i8F9y9zyUMPhrfuYtPw=
這個是服務端拿到請求頭中的Sec-WebSocket-Key: d97OXZzuRlSJV/6SrX+uUA==
,在d97OXZzuRlSJV/6SrX+uUA==
後面拼接一個固定的字元串258EAFA5-E914-47DA-95CA-C5AB0DC85B11
,對拼接後的字元串做SHA1,得到16進位表示的字元串,將每兩位當作一個位元組進行分隔,得到位元組數組,再對這個位元組數組做Base64,得到最後的結果,把最後的結果放到Sec-WebSocket-Accept
響應頭裡返回。
瀏覽器也會使用同樣的演算法把請求頭中的 Sec-WebSocket-Key
算出一個結果,將這個結果與服務端返回的 Sec-WebSocket-Accept
做對比。就像對暗號一樣,兩邊的暗號相同,WebSocket 連接就會被建立起來。這個過程也叫做握手,握手成功後,就可以愉快的使用這個 WebSocket 連接來收發消息了。
作業系統提供的 socket 介面
WebSocket 的通訊,其實是利用了作業系統給我們提供的一套 socket 編程介面。接下來,我把 Linux 系統中給我們提供的 socket 頭文件找出來,看看裡面有哪些介面提供給我們使用,以及每個介面的作用是什麼。找到 socket.h 頭文件在如下位置:
打開 socket.h 文件:
打開另一個目錄下的 socket.h 文件:
socket 編程的流程如下:
在 socket 服務端除了用到上面流程圖列出來的函數,還用到了 setsockopt() 函數,這個函數可以用來設置一些 socket 選項。比如:我在開發調試的過程中,改完程式碼後需要殺掉運行中的 socket 進程,重新運行新編譯出來的 socket。這時候經常會運行失敗,原因是進程是立馬被殺掉了,但是原來被進程監聽的那個埠會進入 TIME_WAIT 狀態,而不會立即被釋放出來。解決方法有兩個:1、殺掉進程後等一會兒,埠被釋放了就能被再次使用了。2、在綁定埠之前,利用 setsockopt() 函數,給埠設置一個 SO_REUSEPORT 選項,這樣殺掉這個進程後立馬重新運行這個進程,也不會運行失敗。
IO 多路復用(IO Multiplexing)
在項目中還用到了IO 多路復用:
- 什麼是 IO ?答:電腦的輸入和輸出(Input、Output)
- 什麼是 IO 多路復用?答:網上看到一個例子比較有意思。假如一個班有 50 名學生,老師在黑板上布置了一道題目讓學生做,
如果老師按照學號先看 1 號學生做出來沒有,做出來了就檢查他,還沒做出來就在原地等他做出來,然後檢查他,檢查完 1 號學生才輪到 2 號學生……這個就是單進程/單執行緒。
如果老師能分身,一共分出 50 個分身,每個學生旁邊站一個老師……這就是多進程/多執行緒。
如果老師站在講台上,有哪位學生做完了就舉手,老師下去檢查他,檢查完老師又回到講台上,看有哪位同學舉手,然後去檢查他……這就是 IO 多路復用。
IO 多路復用有3 種:select、poll、epoll。在項目中用到的是 epoll。接下來,我把 Linux 系統中給我們提供的 epoll 頭文件找出來,看看裡面有哪些介面提供給我們使用,以及每個介面的作用是什麼。找到 epoll.h 頭文件在如下位置:
打開 epoll.h 文件:
epoll 的使用流程如下:
看到網上有文章說 redis 和 nginx 也有使用 epoll,為了驗證他講的是不是真的。我們找 redis 和 nginx 的源碼看一看:
果然 redis 和 nginx 的源碼裡面都有使用 epoll。
WebSocket 編程,還有其他方案
Swoole 擴展:
- 需要 php-7.1 或更高版本
- 用法如下:
//創建WebSocket Server對象,監聽0.0.0.0:9502埠
$ws = new Swoole\WebSocket\Server('0.0.0.0', 9502);
//監聽WebSocket連接打開事件
$ws->on('open', function ($ws, $request) {
var_dump($request->fd, $request->server);
$ws->push($request->fd, "hello, welcome\n");
});
//監聽WebSocket消息事件
$ws->on('message', function ($ws, $frame) {
echo "Message: {$frame->data}\n";
$ws->push($frame->fd, "server: {$frame->data}");
});
//監聽WebSocket連接關閉事件
$ws->on('close', function ($ws, $fd) {
echo "client-{$fd} is closed\n";
});
$ws->start();
想了解更多,請參考 Swoole 官方文檔://wiki.swoole.com/#/
Workerman:
在學習 WebSocket 的過程中,還發現了一個純 PHP 實現的框架:Workerman
- 需要 PHP 5.3.3 或更高版本
- 用法如下:
<?php
use Workerman\Worker;
require_once __DIR__ . '/Workerman/Autoloader.php';
// 注意:這裡與上個例子不同,使用的是websocket協議
$ws_worker = new Worker("websocket://0.0.0.0:2000");
// 啟動4個進程對外提供服務
$ws_worker->count = 4;
// 當收到客戶端發來的數據後返回hello $data給客戶端
$ws_worker->onMessage = function($connection, $data)
{
// 向客戶端發送hello $data
$connection->send('hello ' . $data);
};
// 運行worker
Worker::runAll();
想了解更多,請參考 Workerman 官方文檔://doc.workerman.net/