【Golang】個々のチャットルーム(ウェブページ)毎で独立してWebSocket接続を管理する方法例


経緯

複数のチャットルームが存在するチャットアプリでWebSocketを使用するため、それぞれのルーム(ページ)毎にWebSocketを管理する必要がありました。そこで、セッション管理の仕組みを応用して、各ルームのIDとルームのインスタンスとのマップとしてメモリ上で管理することにしました。

全コードは以下のリンクにアップしています。

https://github.com/taishi-enomoto/go-chat-app/blob/main/wsserver/wsserver.go

参考にしたサイト

① 6.2 Goはどのようにしてsessionを使用するか
 (Build Web Application With Golang)

astaxie.gitbooks.io

メモリ上でのセッション管理は、こちらのサイトの方法を参考にさせていただきました。

② [ golang ] WebSocketを使ったチャット機能を実装してみる。
 (Wild Data Chase -データを巡る冒険-)

wild-data-chase.com

単一のルームにおけるWebSocketの実装は、こちらのサイトを参考にさせていただきました。

実装

チャットルームマネージャーの実装

まず、各ルームを保持する、チャットルームマネージャーを実装します。(リンク先①のセッションマネージャーと同じ仕組みです。) WsChatroomは、それぞれのチャットルームを表す構造体です。

type MANAGER struct {
    WsRooms map[string]*WsChatroom
}

var Manager *MANAGER

func init() {
    Manager = NewManager()
}

func NewManager() *MANAGER {
    database := make(map[string]*WsChatroom)
    return &MANAGER{WsRooms: database}
}

マネージャーは、各ルームのIDをキー、ルームインスタンスを値としたマップを保持します。

WebSocketHandlerの実装

次に、チャットルームへアクセスがあった際の処理を実装していきますが、ルームの存在状況により、①ルームが既に存在する場合と②部屋がまだ存在しない場合の2つの分岐を用意します。

func WebSocketHandler(ws *websocket.Conn) {
    defer ws.Close()

    var chatroomJson string
    if err := websocket.Message.Receive(ws, &chatroomJson); err == nil {
        var chatroom WsChat
        json.Unmarshal([]byte(chatroomJson), &chatroom)
        roomId := chatroom.Id

       //①ルームが既に存在する場合
        if _, exist := Manager.WsRooms[roomId]; exist {

            WsClient := &WsClient{
                Send: make(chan string),
                Room: Manager.WsRooms[roomId],
            }

            Manager.WsRooms[roomId].Join <- WsClient
            defer func() {
                Manager.WsRooms[roomId].Leave <- WsClient
                if len(Manager.WsRooms[roomId].clients) == 0 {
                    delete(Manager.WsRooms, roomId)
                    fmt.Println("WebSocket用ルーム削除")
                }
            }()

            go WsClient.Write(ws)
            WsClient.Read(ws)
        } else {
                        //②部屋がまだ存在しない場合
            WsChatroom := &WsChatroom{
                forward: make(chan string),
                Join:    make(chan *WsClient),
                Leave:   make(chan *WsClient),
                clients: make(map[*WsClient]bool),
            }

            go WsChatroom.ChatroomRun()

            var chatroom WsChat
            json.Unmarshal([]byte(chatroomJson), &chatroom)
            WsChatroom.id = chatroom.Id

            Manager.WsRooms[WsChatroom.id] = WsChatroom


            WsClient := &WsClient{
                Send: make(chan string),
                Room: WsChatroom,
            }

            WsChatroom.Join <- WsClient
            defer func() {
                WsChatroom.Leave <- WsClient
                Manager.WsRooms[WsChatroom.id].Leave <- WsClient
                if len(Manager.WsRooms[WsChatroom.id].clients) == 0 {
                    delete(Manager.WsRooms, WsChatroom.id)
                    fmt.Println("WebSocket用ルーム削除")
                }
            }()

            go WsClient.Write(ws)
            WsClient.Read(ws)
        }
    }
}
    var chatroomJson string
    if err := websocket.Message.Receive(ws, &chatroomJson); err == nil {
        var chatroom WsChat
        json.Unmarshal([]byte(chatroomJson), &chatroom)
        roomId := chatroom.Id

冒頭のコードについてだけ先に説明しておくと、初回接続確率時に送信されるデータにルームNoが含まれる仕組みにしており、ここで得られたルームNoとルームインスタンスを紐付けています。html側での詳しい処理については、ここでは割愛します。

①ルームが既に存在する場合

       //①ルームが既に存在する場合
        if _, exist := Manager.WsRooms[roomId]; exist {

            WsClient := &WsClient{
                Send: make(chan string),
                Room: Manager.WsRooms[roomId],
            }

            Manager.WsRooms[roomId].Join <- WsClient
            defer func() {
                Manager.WsRooms[roomId].Leave <- WsClient
                if len(Manager.WsRooms[roomId].clients) == 0 {
                    delete(Manager.WsRooms, roomId)
                    fmt.Println("WebSocket用ルーム削除")
                }
            }()

            go WsClient.Write(ws)
            WsClient.Read(ws)

最初に取得したルームIDを元に、チャットルームマネージャーが入室したチャットルームのインスタンスを保持しているかexistで確認します。チャットルームがすでに存在する場合、ユーザーを表すWsClientインスタンスを生成、ルームインスタンスのJoinフィールドに追加します。

【補足】 ルーム構造体のChatroomRunメソッドはチャネルにより入退室状況・メッセージの送受信状況を管理しており、WriteReadメソッドはデータの読み込み・書き出しを行います。詳細は参照元のページを確認ください。

②部屋がまだ存在しない場合

                        //②部屋がまだ存在しない場合
            WsChatroom := &WsChatroom{
                forward: make(chan string),
                Join:    make(chan *WsClient),
                Leave:   make(chan *WsClient),
                clients: make(map[*WsClient]bool),
            }

            go WsChatroom.ChatroomRun()

            var chatroom WsChat
            json.Unmarshal([]byte(chatroomJson), &chatroom)
            WsChatroom.id = chatroom.Id

            Manager.WsRooms[WsChatroom.id] = WsChatroom

            WsClient := &WsClient{
                Send: make(chan string),
                Room: WsChatroom,
            }

            WsChatroom.Join <- WsClient
            defer func() {
                WsChatroom.Leave <- WsClient
                Manager.WsRooms[WsChatroom.id].Leave <- WsClient
                if len(Manager.WsRooms[WsChatroom.id].clients) == 0 {
                    delete(Manager.WsRooms, WsChatroom.id)
                    fmt.Println("WebSocket用ルーム削除")
                }
            }()

            go WsClient.Write(ws)
            WsClient.Read(ws)
        }
    }
}

本来書く順序的には①より前が良さそうですが、、ルームがまだ作成されていなかった場合の処理になります。

            WsChatroom := &WsChatroom{
                forward: make(chan string),
                Join:    make(chan *WsClient),
                Leave:   make(chan *WsClient),
                clients: make(map[*WsClient]bool),
            }

            go WsChatroom.ChatroomRun()

            var chatroom WsChat
            json.Unmarshal([]byte(chatroomJson), &chatroom)
            WsChatroom.id = chatroom.Id

            Manager.WsRooms[WsChatroom.id] = WsChatroom

まずルーム構造体を初期化して入室したチャットルームのインスタンスを生成します。次に取得したルームIDをキーに、ルームインスタンスをチャットルームマネージャーで保持します。後は①の時と同様です。

以上の仕組みで、個々のチャットルームで独立したWebSocket通信を行うことが可能になります。