1. 程式人生 > >WebSocket協議 與 IO多路複用

WebSocket協議 與 IO多路複用

最近在把 Facebook Message 接入客服系統,由於與 Facebook Message 對接的收發訊息都是通過呼叫 http 介面來實現的,如果想實現即時通訊,還需要在中間加一個 WebSocket 來轉發訊息。如下圖: ![](https://img2020.cnblogs.com/blog/1313428/202012/1313428-20201226154746317-1778404235.png) 其中用到了 WebSocket 協議和 IO多路複用相關的知識。在這裡做一個學習記錄。 ## 為什麼需要 WebSocket 協議 - 因為 HTTP 協議有一個缺陷:通訊只能先由客戶端發起,然後伺服器再作出響應,並不能由伺服器主動向客戶端推送訊息。 - WebSocket 協議最大的特點是,伺服器可以主動向客戶端推送資訊,客戶端也可以主動向伺服器傳送資訊。 ## WebSocket 與 socket 的之間關係 - WebSocket 是一個網路通訊協議,是屬於網路七層模型中的應用層的協議,同樣屬於應用層的協議還有 HTTP 協議、FTP協議、SMTP協議等等。 - 而 socket 是作業系統提供的一套介面,利用這一套介面就可以編寫程式實現程序之間的通訊、網路通訊等功能。 ## 一個 WebSocket 連線是如何建立起來的 WebSocket 連線的初期是基於 HTTP 協議的,假如 WebSocket 的地址是這個:wss://www.xxx.com/websocket ,在連線 WebSocket 的初期瀏覽器首先會向這個地址發出一個 HTTP GET 請求,請求頭資訊截圖如下: ![](https://blog-static.cnblogs.com/files/nnngu/1313428-20201226210629980-1486202535.gif) 紅色框標出的是比較重要的請求頭: - `Connection: Upgrade` 告訴服務端這個連線需要升級。 - `Upgrade: websocket` 告訴服務端需要升級到 WebSocket 協議。 - `Sec-WebSocket-Key: d97OXZzuRlSJV/6SrX+uUA==` 是瀏覽器隨機生成的一個字串。 服務端接收到這個 HTTP 請求,會作出響應,響應頭的截圖如下: ![](https://img2020.cnblogs.com/blog/1313428/202012/1313428-20201226212249991-1620977593.png) 紅色框標出的是比較重要的響應頭: - `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 標頭檔案在如下位置: ![](https://img2020.cnblogs.com/blog/1313428/202012/1313428-20201227002646256-812826521.png) 開啟 socket.h 檔案: ![](https://img2020.cnblogs.com/blog/1313428/202012/1313428-20201227003226217-1236767379.png) ![](https://img2020.cnblogs.com/blog/1313428/202012/1313428-20201227004437249-2045781485.png) ![](https://img2020.cnblogs.com/blog/1313428/202012/1313428-20201227005919128-925133060.png) 開啟另一個目錄下的 socket.h 檔案: ![](https://img2020.cnblogs.com/blog/1313428/202012/1313428-20201227162805703-1794571022.png) socket 程式設計的流程如下: ![](https://img2020.cnblogs.com/blog/1313428/202012/1313428-20201227010548638-1175343013.png) 在 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 標頭檔案在如下位置: ![](https://img2020.cnblogs.com/blog/1313428/202012/1313428-20201227164358779-1454646660.png) 開啟 epoll.h 檔案: ![](https://img2020.cnblogs.com/blog/1313428/202012/1313428-20201227171901930-183847246.png) ![](https://img2020.cnblogs.com/blog/1313428/202012/1313428-20201227171000604-887100463.png) ![](https://img2020.cnblogs.com/blog/1313428/202012/1313428-20201227164921176-1330788485.png) epoll 的使用流程如下: ![](https://img2020.cnblogs.com/blog/1313428/202012/1313428-20201227165955956-1630330884.png) 看到網上有文章說 redis 和 nginx 也有使用 epoll,為了驗證他講的是不是真的。我們找 redis 和 nginx 的原始碼看一看: ![](https://img2020.cnblogs.com/blog/1313428/202012/1313428-20201227173601613-1800455532.png) ![](https://img2020.cnblogs.com/blog/1313428/202012/1313428-20201227174736250-1322205206.png) 果然 redis 和 nginx 的原始碼裡面都有使用 epoll。 ## WebSocket 程式設計,還有其他方案 ### Swoole 擴充套件: - 需要 php-7.1 或更高版本 - 用法如下: ```php //建立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 官方文件:
### Workerman: 在學習 WebSocket 的過程中,還發現了一個純 PHP 實現的框架:Workerman - 需要 PHP 5.3.3 或更高版本 - 用法如下: ```php count = 4; // 當收到客戶端發來的資料後返回hello $data給客戶端 $ws_worker->onMessage = function($connection, $data) { // 向客戶端傳送hello $data $connection->send('hello ' . $data); }; // 執行worker Worker::runAll(); ``` 想了解更多,請參考 Workerman 官方文件: