前言 本文的目的是基於 GatewayWorker 官方手冊,梳理一次 GatewayWorker,併在實踐中與 MVC 框架整合的思路(附最終的項目源碼)。如果你已經理解了整合這一塊兒的知識,那麼就可以關掉這個網頁了。時間蠻寶貴的~ 很久以前就想做一個聊天室了。百度了下 "php 通信",看到了可 ...
前言
本文的目的是基於 GatewayWorker 官方手冊,梳理一次 GatewayWorker,併在實踐中與 MVC 框架整合的思路(附最終的項目源碼)。如果你已經理解了整合這一塊兒的知識,那麼就可以關掉這個網頁了。時間蠻寶貴的~
這篇是上篇,梳理 GatewayWorker 基礎,下篇是 GatewayWorker 與 Laravel 整合聊天室。如果你具備了 GatewayWorker 基礎,請直接閱讀下篇。
很久以前就想做一個聊天室了。查了下 "php 通信",找到了可用的東西:Socket、WebSocket、 Workerman 以及 GatewayWorker。Socket(介面)提供了一組端到端互相通信的介面,作為通信的核心功能。Websocket(協議)定義了通信中數據的封裝和顯示的格式,而且最大的特點是它支持服務端向客戶端的主動推送,這一點是 HTTP 做不到的。而 Workerman (框架)將這兩者很好地整合在了一起(當然不僅僅於此)。GatewayWorker(框架)是在 Workerman 的基礎上開發的 TCP 長連接應用框架,提供了單發、群發和廣播等介面,還可以客戶端和客戶端通信。
所以最終我選擇了 GatewayWorker 作為 Socket 監聽的服務端,Laravel 作為 HTTP 請求的業務處理框架,完成一個響應式的線上聊天室(項目地址在下一篇文章最後)。
GatewayWorker工作原理
先理解一下工作原理,可以對 GatewayWorker 有個整體的把握。這一塊兒其實手冊里已經詳細不啰嗦地解釋清楚了。我這裡再理一下:
1、Register、Gateway、BusinessWorker 3 種進程依次啟動(因為支持多進程,所以我說“種”,而不是“個”)
2、Gateway 進程和 BusinessWorker 進程啟動後向 Register 服務進程發起長連接註冊自身。
3、Register 服務進程收到 Gateway 的註冊後,把所有 Gateway 進程的通訊地址寫入記憶體。
4、Register 服務進程收到 BusinessWorker 的註冊後,把記憶體中的 Gateway 進程通訊地址發給所有 BusinessWorker 進程。
5、BusinessWorker 進程收到所有 Gateway 進程的通訊地址後,嘗試連接 Gateway。
6、至此,所有 Gateway 和 BusinessWorker 進程就通過 Register 服務進程建立了長連接。
如果期間有新的 Gateway 註冊到 Register(一般是分散式部署加機器),新 Gateway 的通訊地址會被廣播給所有 BusinessWorker,BusinessWorker 收到通知後建立新連接。
如果期間有 Gateway 下線,Register 會收到通知、刪除這個 Gateway 的內部通訊地址,並將新的內部通訊地址列表廣播給所有 BusinessWorker,BusinessWorker 不再連接下線的 Gateway。
7、客戶端的連接事件和連接上的數據會經由 Gateway 轉發給 BusinessWorker,BusinessWorker 預設調用 Events.php 中 Events 類的 onConnect、onMessage、onClose 事件回調處理業務邏輯。
8、BusinessWorker 負責運行所有的業務邏輯,實際的處理邏輯預設在 Events.php 中實現。
GatewayWorker進程模型
GatewayWorker 是以進程的形式進駐記憶體的,瞭解了它的工作原理之後,有必要理解一下它的進程模型。
GatewayWorker 主要有 3 種進程:Register 進程、Gateway 進程和 BusinessWorker 進程。這 3 種進程分別對應了內核源碼中的 Register 類、Gateway 類和 BusinessWorker 類,並且它們都是基於 Workerman 框架的 Worker 類開發的,所以這 3 種進程都有一些公共的屬性,比如 name、count、onWorkerStart、onWorkerStop 等等。可以說,GatewayWorker 里所有的進程都是 Worker 進程。
1、Register進程
Register 進程主要負責 Gateway 進程 與 BusinessWorker 進程建立連接並內部通訊。
該進程由 Register 類實例化,並隨進程啟動進駐記憶體。
它可定製的只有實例化時指定自身所在的服務進程地址。包括 IP 和埠,並且目前只支持 text 協議。text 協議是 Workerman 框架定義的一種文本協議(協議格式為:數據包 + 換行符)。
2、Gateway進程
Gateway 進程主要負責客戶端的連接以及連接上的數據,並將所有的請求轉發給 BusinessWorker 進程進行處理。BusinessWorker 進程的所有處理結果都經由 Gateway 進程轉發給客戶端。
該進程由 Gateway 類實例化,並隨進程啟動進駐記憶體。
它可定製的有:
(1)實例化。指定協議、IP 和埠。
協議:目前支持的有 Websocket 協議、text 協議、Frame 協議、自定義通訊協議和 裸 TCP 協議(不推薦,見通訊協議作用),不支持監聽 HTTP 協議。
IP:"0.0.0.0" 表示監聽本機所有網卡;"127.0.0.1"表示僅允許本機通過 127.0.0.1 訪問該進程;內網 IP 如 "192.168.11.2" 表示只允許該 IP 訪問;外網 IP 如 "110.110.110.110" 表示只允許該 IP 訪問。
埠:大於 1024 小於等於 65535。小於 1024 時需要 root 許可權運行該進程。
(2)name:Gateway 進程名。以便在 Bash 等終端里查看區分。
(3)count:Gateway 進程數。充分利用多 CPU 資源。預設為 1。如何設置進程數,請參考這裡。
(4)lanIp:Gateway 進程所在伺服器的內網 IP,預設填寫 "127.0.0.1" 即可。多伺服器分散式部署 時要填寫真實 IP。無論如何都不能填寫 "0.0.0.0"。
(5)startPort:Gateway 進程啟動後監聽的起始埠(本機埠),用來給 BusinessWorker 進程提供連接服務,然後兩者通過這個埠建立通訊。假設進程數 count 為 4,起始埠 startPort 為 2003,則 會啟動 4 個 Gateway進程,各進程分別監聽 2003、2004、2005、2006 埠。
(6)registerAddress:向 Register 進程的註冊地址,格式為"IP + 埠",如 "127.0.0.1:1236"。和 BusinessWorker 進程指定的註冊地址要保持一致。
(7)心跳設置:為了防止長時間不通訊被路由節點強行斷開或斷電斷網等極端事件,必須加心跳。相關屬性有 pingInterval、pingNotResponseLimit、pingInterval。詳細心跳設置請參考服務端到客戶端的心跳檢測。
pingInterval:心跳間隔,單位秒,0 表示不發送心跳檢測。
pingNotResponseLimit:客戶端連續 $pingNotResponseLimit * $pingInterval 秒內不回應心跳則斷開連接。
pingData:心跳數據,可任意,客戶端能識別就行。
(8)onWorkerStart:Gateway 進程啟動後的回調函數。
(9)onWorkerStop:Gateway 進程關閉的回調函數。
(10)onConnect:當有客戶端連接上來時觸發。與 Events::onConnect 的區別是 Events::onConnect 方法運行在 BusinessWorker 進程上。而 Gateway::onConnect 方法是運行在Gateway 進程上,無法使用 \GatewayWorker\Lib\Gateway 類提供的介面。
(11)onClose:當有客戶端連接關閉時觸發。同樣與Events::onClose的區別是 Gateway::onClose 方法是運行在 Gateway 進程上,無法使用 \GatewayWorker\Lib\Gateway 類提供的介面。
3、BusinessWorker進程
BusinessWorker 進程負責運行業務邏輯。BusinessWorker 進程收到 Gateway 進程轉發來的事件和請求時,會預設調用 Events.php 中的 onConnect、onMessage、onClose 方法處理事件和數據。
該進程由 BusinessWorker 類實例化,並隨進程啟動進駐記憶體。
它可定製的有:
(1)name:BusinessWorker 進程名。以便在 Bash 等終端里查看區分。
(2)count:BusinessWorker 進程數。充分利用多 CPU 資源。預設為 1。如何設置進程數,請參考這裡。
(3)registerAddress:向 Register 進程的註冊地址,格式為"IP + 埠",如 "127.0.0.1:1236"。和 Gateway 進程指定的註冊地址要保持一致。
(4)onWorkerStart:BusinessWorker 進程啟動後的回調函數
(5)onWorkerStop:BusinessWorker 進程關閉的回調函數。
(6)eventHandler:指定 BusinessWorker 進程里實際處理業務邏輯的類,預設是 Events。也就是預設使用 Events.php 中的 Events 類來處理業務。業務類至少要實現onMessage 靜態方法,onConnect 和 onClose 靜態方法可以不用實現。(如果使用了命名空間,建議填寫完全限定名稱的命名空間。)
。
Events.php
上面提到了 Events.php,它是實際處理業務邏輯的類 Events 所在的文件。我們在實際的開發中,只需要關註這一個文件。
Events 里有 5 個事件回調的處理方法,按照發生順序,依次是
- onWorkerStart (Worker $businessWorker):當 BusinessWorker 進程啟動時觸發。每個進程生命周期內只觸發一次。
-
onConnect (string $client_id):當客戶端連接上 Gateway 進程時觸發(TCP 三層握手)。
- onMesssge (string $client_id, mixed $recv_data):當客戶端發來數據,也就是 Gateway 進程收到數據後觸發。
-
onClose (string client_id):當客戶端連接斷開時觸發。無論是客戶端還是服務端主動斷開,都會觸發。
- onWorkerStop (Worker $businessWorker):當 BusinessWorker 進程退出時觸發。每個進程生命周期內只觸發一次。
這裡面我們常用到的是 onMessage 和 onClose 回調,其他比較少用。
上面的回調事件里有一個比較重要的參數:$client_id。client_id 是 20 個字元的定長字元串,用來全局標識一個 Socket 連接。每個客戶端連接都會被分配一個全局唯一的 client_id。客戶端關閉連接時,對應的 client_id 會失效。當客戶端再次打開一個 Socket 連接時,會被分配一個新的 client_id。
Lib\Gateway類提供的介面
既然(預設)在 Events.php 中處理實際的業務邏輯,回調的事件我們已經知道了。那麼怎麼向客戶端發送消息呢?
命名空間 \GatewayWorker\Lib\Gateway 指向的這個 Gateway 類,提供了一組單發、群發和廣播的介面,在 Events.php 中向客戶端發信的時候就可以使用這個類。它提供的介面非常豐富:
Gateway::sendToAll($data); // 向所有客戶端發送數據 Gateway::sendToClient($client_id, $data); // 向某個客戶端發送數據 Gateway::closeClient($client_id); // 關閉某個客戶端 Gateway::isOnline($client_id); // 判斷某客戶端連接是否線上 Gateway::bindUid($client_id, $uid); // 綁定 uid 與 client_id Gateway::unbindUid($client_id, $uid); // 取消 uid 與 某個 client_id 的綁定 Gateway::isUidOnline($uid); // 某個 uid 是否線上 Gateway::GetClientIdByUid(); // 獲取與 uid 綁定的 client_id 列表(一對多) Gateway::sendToUid($uid, $data); // 向所有 uid 發送 Gateway::joinGroup($client_id, $group); // 把該 client_id 加入群組 Gateway::leaveGroup($client_id, $group); // 將 client_id 離開群組 Gateway::sendToGroup($group, $data); // 向某群組 group 發送 Gateway::getClientCountByGroup($group); // 獲取某個組的線上連接數 Gateway::getClientSessionsByGroup($group); // 獲取某個組的連接信息 Gateway::getClientInfoByGroup($group); // getClientSessionsByGroup 的別名 Gateway::getAllClientCount(); // 獲取所有的線上連接數 Gateway::getAllClientSessions(); // 獲取所有線上用戶的 session Gateway::getAllClientInfo(); // getAllClientSessions 的別名 Gateway::setSession($client_id, $session); // 設置 session,原 session 值會被覆蓋 Gateway::updateSession($client_id, $session); // 更新 session,實際上是與舊的session合併 Gateway::getSession($client_id); // 獲取某個 client_id的 session
這裡面比較重要的是 GatewayWorker 的超全局數組 $_SESSION。每個客戶端連接對應一個 Session 會話,並由 Gateway 進程存儲在記憶體里。示例如下,在收到客戶端消息時,列印所有線上連接的 Session:
use \GatewayWorker\Lib\Gateway; class Events { ... public onMessage($client_id, $message) { $_SESSION['name'] = $message['name']; // 操作當前用戶的 Session 時,直接使用 $_SESSION 即可 var_export(Gateway::getAllClientSessions()); } ... } 列印出的數據類似如下: array( '7f00000108fc00000008' => array('name' => 'Tom'), '7f00000108fc00000009' => array('name' => 'Joan') )
註意上面的註釋,操作當前連接上的 Session 時,直接使用 $_SESSION['xx'] = 'xxx'; 的方式賦值即可,操作其他用戶的 Session 時用 Gateway::setSession 介面。
此外,如果你在 GatewayWorker 的進程模型里需要獲取客戶端、服務端的 IP,請使用 $_SERVER 數組。它由 Workerman 框架定義,內置了 5 個數組成員,數組 key 分別如下,詳細請參考文檔。
REMOTE_ADDR // 客戶端IP(如果客戶端處於區域網,則是客戶端所在區域網的出口IP) REMOTE_PORT // 客戶端埠(如果客戶端處於區域網,則是客戶端所在區域網的出口埠) GATEWAY_ADDR // Gateway 進程所在伺服器的 IP GATEWAY_PORT // Geteway 監聽的埠,這對於多埠應用中在 Events.php 里區分客戶端連的是哪個埠非常有用。 GATEWAY_CLIENT_ID // 全局唯一的客戶端 IP
好的。有關 GatewayWorker 框架的基礎暫時就梳理這麼多。更多 GatewayWorker 開發和部署的細節或問題,比如心跳檢測、設置定時器、合理選擇多進程、分散式部署、定製通訊協議、啟用 wss 協議等等,都在文檔里有詳細的介紹。車在下麵。
我感覺這一篇有點長了,所以將在下一篇開始梳理 GatewayWorker 與 Laravel 框架的整合。
相關鏈接
GatewayWorker 線上文檔:http://doc2.workerman.net/326102
Workerman 線上文檔:http://doc.workerman.net/
Workerman 官網:https://www.workerman.net/