Hyperf搭建websocket叢集專案(透過redis釋出訂閱)

aston_chen發表於2022-06-29

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中能夠看到對映關係已經建立好了
Hyperf搭建websocket叢集專案

以上已經儲存好了使用者與分散式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

Hyperf搭建websocket叢集專案(透過redis釋出訂閱)

啟動第一個server

Hyperf搭建websocket叢集專案(透過redis釋出訂閱)

然後修改env

Hyperf搭建websocket叢集專案(透過redis釋出訂閱)

啟動第二個server

Hyperf搭建websocket叢集專案(透過redis釋出訂閱)

寫一個簡易的view當作websocket客戶端

我這裡的demo是想要實現給指定的uid傳送訊息

然後分別連線到兩個不同的websocket伺服器

Hyperf搭建websocket叢集專案(透過redis釋出訂閱)

Hyperf搭建websocket叢集專案(透過redis釋出訂閱)

當然分散式解決方案還有訊息佇列,閘道器等。有空我會再出一個透過訊息佇列實現的帖子。本文簡單的演示了使用redis的釋出訂閱實現的分散式websocket專案搭建,有不足之處請大神們指教

AstonChenDev/hyperf-distribute-websocket: 基於redis釋出訂閱和非同步佇列實現的ws分散式通訊 (github.com)

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章