最近在把 Facebook Message 接入客服系統,因爲與 Facebook Message 對接的收發消息都是經過調用 http 接口來實現的,若是想實現即時通信,還須要在中間加一個 WebSocket 來轉發消息。以下圖:php
其中用到了 WebSocket 協議和 IO多路複用相關的知識。在這裏作一個學習記錄。nginx
WebSocket 鏈接的初期是基於 HTTP 協議的,假如 WebSocket 的地址是這個:wss://www.xxx.com/websocket ,在鏈接 WebSocket 的初期瀏覽器首先會向這個地址發出一個 HTTP GET 請求,請求頭信息截圖以下:web
紅色框標出的是比較重要的請求頭:redis
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 鏈接來收發消息了。數組
WebSocket 的通訊,實際上是利用了操做系統給咱們提供的一套 socket 編程接口。接下來,我把 Linux 系統中給咱們提供的 socket 頭文件找出來,看看裏面有哪些接口提供給咱們使用,以及每一個接口的做用是什麼。找到 socket.h 頭文件在以下位置:瀏覽器
打開 socket.h 文件:服務器
打開另外一個目錄下的 socket.h 文件:websocket
socket 編程的流程以下:
在 socket 服務端除了用到上面流程圖列出來的函數,還用到了 setsockopt() 函數,這個函數能夠用來設置一些 socket 選項。好比:我在開發調試的過程當中,改完代碼後須要殺掉運行中的 socket 進程,從新運行新編譯出來的 socket。這時候常常會運行失敗,緣由是進程是立馬被殺掉了,可是原來被進程監聽的那個端口會進入 TIME_WAIT 狀態,而不會當即被釋放出來。解決方法有兩個:一、殺掉進程後等一下子,端口被釋放了就能被再次使用了。二、在綁定端口以前,利用 setsockopt() 函數,給端口設置一個 SO_REUSEPORT 選項,這樣殺掉這個進程後立馬從新運行這個進程,也不會運行失敗。
在項目中還用到了IO 多路複用:
IO 多路複用有3 種:select、poll、epoll。在項目中用到的是 epoll。接下來,我把 Linux 系統中給咱們提供的 epoll 頭文件找出來,看看裏面有哪些接口提供給咱們使用,以及每一個接口的做用是什麼。找到 epoll.h 頭文件在以下位置:
打開 epoll.h 文件:
epoll 的使用流程以下:
看到網上有文章說 redis 和 nginx 也有使用 epoll,爲了驗證他講的是否是真的。咱們找 redis 和 nginx 的源碼看一看:
果真 redis 和 nginx 的源碼裏面都有使用 epoll。
//建立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 官方文檔:https://wiki.swoole.com/#/
在學習 WebSocket 的過程當中,還發現了一個純 PHP 實現的框架:Workerman
<?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 官方文檔:http://doc.workerman.net/