Golang の websocket サンプルを書き直してみた

Golang の勉強がてらちょっとしたツールを作ろうと思って、まず websocket サーバーを書いてみた。 書いてみたと言っても go の websocket chat は golang-samples/websocket · GitHub ここにサンプルがあって、これをそのまま使っても良かったのだけれどいくつか気になったのでよりシンプルに書き直してみた。

気になったのは二つあって、一個は client id が競合しそうに見えた点。 https://github.com/golang-samples/websocket/blob/master/websocket-chat/src/chat/client.go#L35 ここで maxId をインクリメントしている. この NewClient 関数は https://github.com/golang-samples/websocket/blob/master/websocket-chat/src/chat/client.go#L35 ここで呼ばれていて、この onConnected はハンドラとして渡されているので GOMAXPROCS が 1 じゃなければ同時に実行されることがありそうとおもった。インクリメント処理はアトミックじゃない(と思ってる)ので、ここでインクリメントすると maxId を破壊することがありそう?? こういう時例えば Java の Thread とかだと maxId を synchronized で排他制御するのが考えられるけど、go では channel を使って制御するのがオシャレなようだ。

func (server *Server) WebsocketHandler() websocket.Handler {
    return websocket.Handler(func (ws *websocket.Conn) {
        client := NewClient(ws, server.removeClientCh, server.messageCh)
        server.addClientCh <- client
        client.Start()
    })
}

競合を避けるために、server に client 追加用の channel(addClientCh) を持たせ、websocket で接続されたらサーバーオブジェクトに client を渡してその先で client id を入れてもらうようにした。この addClientCh を liesten してるのは単一の goroutine なので競合することはない(が、詰まる可能性はある??)

気になったもう一点は、クライアントをハンドルする goroutine が複数あること。client (クライアント、ブラウザとかをハンドリングするサーバー側のオブジェクト)はクライアントからのメッセージを listen しなくちゃいけないのだけれど、チャットなので他のクライアントからのメッセージを受け取って自身が担当するクライアントに投げないといけない。そのため client はクライアント(=ブラウザ)だけでなく server も listen している。たぶん下の図ような感じになっている

f:id:hatz48:20141221192131p:plain

黒線は生成線で、グレーの線が listen 関係を表している。白い箱が goroutine で色ついてるのがオブジェクトとか。request handler が点線になっているのは揮発性(すぐ終了する)goroutine を表現してみた。 これはパフォーマンス的にはいいのかもしれない(?)けどちょっと複雑になって読みづらかった。クライアント切断時に goroutine を殺し忘れないように終了処理もちゃんとしないといけない。以下のように、client を担当する goroutine が一つになっている方がわかりやすかった。

f:id:hatz48:20141221192136p:plain

client から server を listen する goroutine がいなくなったので、message を送る際に server が client のメソッドを直接呼び出している。そのまま呼び出すと server の goroutine がブロックするのでメソッド呼び出すだけの goroutine を生成してる。これだと client の終了処理も goroutine 一つ潰すだけで済む。

まとめ

Golang の勉強がてら websocket server 書き直してみた。 websocket は状態を持つので常駐する goroutine が複数存在することになるのだけど、なるべく簡素化したい。 とくに双方向に listen し合うと複雑になるなーと感じたのでそうならないように書いてみた。

とここまで書いてはみたものの Golang 初心者なので理解が間違ってるのかもしれない。できれば有益なマサカリをゲットしたい。