🦜
Go標準でブラウザにイベントストリーミングする
WebSocketのツラミ
- 中継サービスの対応がないと切れる
- ルーターによっては長時間アクセスがないと切れる
- 切れたら繋ぎなおすのはクライアントの実装次第
- セキュアにつなぐためにはサーバーもクライアントも新バージョンのサポートが必要
- 接続数が膨れず、安定して接続を維持するのには結構ノウハウが求められる
- 単純に切れたら即繋ぐでは中継やサーバーに問題が発生することもある
そこでEventSourceですよ
- メジャーブラウザでサポート・互換性も高い
- プロトコル仕様がただのHTTPロングポール+アルファ
- なのでほとんどの接続経路で中継トラブルが少ない
- JSのEventSource実装がセッション維持を頑張ってくれる
- サーバーから切断されたら再接続をしようとする
- 特にGoなら標準機能でさっくりサーバーが書ける
クライアント実装
let es = new EventSource("/sse");
es.addEventListener("message", function (ev) {
if (ev.data.length > 0) {
let event = JSON.parse(ev.data);
// eventを使った処理
}
});
基本のコード
type Event struct {
// 任意のフィールドセット
}
var (
subscribe = make(chan chan<- Params, 1)
unsubscribe = make(chan chan<- Params, 1)
)
func sse(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
ch := make(chan Event, 64)
subscribe <- ch
log.Printf("connect from: %v", r.RemoteAddr)
defer func() {
unsubscribe <- ch
log.Printf("disconnect from: %v", r.RemoteAddr)
}()
timeout := time.NewTicker(30 * time.Second)
for {
select {
case <-r.Context().Done():
return
case v := <-ch:
timeout.Reset(30 * time.Second)
b, _ := json.Marshal(v)
fmt.Fprintf(w, "data: %s\n\n", string(b))
case <-timeout.C:
fmt.Fprintf(w, "data: \n\n")
}
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
}
}
func do(publish chan<- Event) {
for {
// publishに投げ込む任意の処理
}
}
func main() {
publish := make(chan Event, 64)
go func () {
m := map[chan<- Event]struct{}{}
for {
select {
case v := <-subscribe:
m[v] = struct{}{}
case v := <-unsubscribe:
delete(m, v)
close(v)
case v := <-publish:
for c := range m {
c <- v
}
}
}
}
http.Handle("/sse", http.HandlerFunc(sse))
go do(publish)
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
あとは「publish」にEvent値を投げ込めば接続済みのクライアントに通知されます。
まとめ
- EventSourceは自動的に5秒おきに再接続してくれるので実装がシンプル
- Go側(バックエンド)もややこしいライブラリが無くてもシンプルに実装できちゃう
- Go側の再起動後、クライアントは勝手に再接続してくれる
- 現実的な実装にするには各種goroutineの中断用にcontext.Contextを渡すようにしましょう
Discussion
ちなみに「data:」の前に「event: any-name」を加えてやると、addEventListener用の任意のイベント名としてブラウザJSにイベントを投げることができます。