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 官方文檔:https://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 官方文檔:http://doc.workerman.net/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM