Hyperf分散式websocket解決方案
場景:
1、多個websocket server
2、客戶端 clientA 連線到 serverA 得到 fdA, clientB 連線到 serverB 得到 fdB
3、clientA 想要給 clientB 傳送訊息
注:fd 類似於當前連結的檔案描述符,每一次新的連線會自增 1 ,可以理解為連線號
問題:clientA 和 clientB 連線到的是不同的伺服器,fd作用域僅限於當前伺服器,要想跨伺服器想實現通訊,需要藉助中介軟體來傳遞訊息
解決方案
方案一、使用redis釋出訂閱(pub/sub):
第一步:生成分散式fd
這裡的分散式FD, 目的是讓不同伺服器在推送訊息的時候能夠知道這個fd是哪個server的,然後進行定向推送
附程式碼:
//連結上伺服器時的回撥 需要在這裡生成分散式fd,且儲存使用者與分散式fd的對映關係
public function onOpen($server, Request $request): void
{
$uid = $request->get['uid'];
$server->push($request->fd, 'Opened');
//我這裡用了環境變數APP_NAME去區分了server 你們也可以看著辦 只要能區分出伺服器就行
$server_name = env('APP_NAME');
//這就是一個簡易的分散式fd,能夠確保我能透過某種方法解析出伺服器名稱和在這個伺服器上的fd即可
$fd = $server_name . '_' . $request->fd;
redis()->set("user:$uid", $fd);
}
redis中能夠看到對映關係已經建立好了
以上已經儲存好了使用者與分散式fd的對映,接下來就是傳送訊息的時候怎麼轉發給指定伺服器的問題了
//收到客戶端訊息回撥
public function onMessage($server, Frame $frame): void
{
$data = json_decode($frame->data, true);
$uid = (int)$data['uid'];
$text = $data['data'];
$target_fd = redis()->get("user:$uid");
if (!$target_fd) {
$server->push($frame->fd, 'not exist');
return;
}
//這裡我們們根據onOpen時生成分散式fd的規則 結析出伺服器和fd
[$server_name, $server_fd] = explode('_', $target_fd);
//向訂閱了這個伺服器channel釋出訊息
//這裡如果封裝的話 最好判斷一下是不是本伺服器,如果是的話就不需要透過(pub/sub)了
redis()->publish($server_name, json_encode([
'fd' => (int)$server_fd,
'data' => $text
]));
}
注意的點:由於redis的subscribe方法是阻塞的,所以需要在hyperf中使用自定義程式,該程式只負責訂閱和回撥,不影響其他程式,收到訂閱訊息後執行回撥即可
有關於hyperf自定義程式請看自定義程式
/**
* 訂閱redis頻道程式
* @Process(name="subscribe_process")
*/
class SubscribeProcess extends AbstractProcess
{
public function handle(): void
{
//這裡依然是訂閱了本伺服器上的環境變數APP_NAME,這樣就能夠實現指定釋出到某一個channel上
$server_name = env('APP_NAME');
$redis = redis();
//訂閱是阻塞的 如果這裡的redis連結限制了超時時間 那麼到時間後就會斷開 該程式也就失效了,所以這裡要解除超時限制
$redis->setOption(\Redis::OPT_READ_TIMEOUT, -1);
$redis->subscribe([
$server_name,
], [
$this,
'dispatchChannel'
]);
}
/**
* Notes: 訂閱事件回撥 這裡其實就是讓這個伺服器推送訊息了
* User: 陳朋
* DateTime: 2022/06/28 15:22
* @param $redis
* @param string $channel
* @param string $msg
* @return void
*/
private function dispatchChannel($redis, string $channel, string $msg): void
{
//當然這裡能做的不只是推送訊息,也可以在$msg中傳遞一個自定義的type欄位,根據type來做不同的處理,比如強制斷開連線
$msg = json_decode($msg, true);
$data = $msg['data'];
$fd = $msg['fd'];
if (!server()->exists($fd)) {
return;
}
//到這裡就是給指定的fd傳送訊息了
server()->push($fd, $data);
}
}
上面的程式碼就能夠解決不同客戶端連線到不同server時的通訊問題
本地開啟兩個不同埠的server
我這裡是9501 和 9503
配置env
啟動第一個server
然後修改env
啟動第二個server
寫一個簡易的view當作websocket客戶端
我這裡的demo是想要實現給指定的uid傳送訊息
然後分別連線到兩個不同的websocket伺服器
當然分散式解決方案還有訊息佇列,閘道器等。有空我會再出一個透過訊息佇列實現的帖子。本文簡單的演示了使用redis的釋出訂閱實現的分散式websocket專案搭建,有不足之處請大神們指教
AstonChenDev/hyperf-distribute-websocket: 基於redis釋出訂閱和非同步佇列實現的ws分散式通訊 (github.com)
本作品採用《CC 協議》,轉載必須註明作者和本文連結