【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)
メモリ上でのセッション管理は、こちらのサイトの方法を参考にさせていただきました。
② [ golang ] WebSocketを使ったチャット機能を実装してみる。
(Wild Data Chase -データを巡る冒険-)
単一のルームにおける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
メソッドはチャネルにより入退室状況・メッセージの送受信状況を管理しており、Write
、Read
メソッドはデータの読み込み・書き出しを行います。詳細は参照元のページを確認ください。
②部屋がまだ存在しない場合
//②部屋がまだ存在しない場合 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通信を行うことが可能になります。