はじめに
タイトルの通りGo言語でServerSentEvent(以下SSE)を実装してみます。
まずSSEとはなあに?って方はこちらが参考になります
- https://www.webprofessional.jp/real-time-apps-websockets-server-sent-events/
- http://labs.gree.jp/blog/2014/08/11070/
- https://www.w3.org/TR/eventsource/
正直「WebSocketで良くない?」って雰囲気のある技術ですが、
仕様がシンプルなので好きな技術なのです
コード
まずはプロジェクト構成
├── main.go
└── static
└── index.html
とてもシンプルです
実際の各ファイルの中身を見ていきます
./main.go
package main
import (
"fmt"
"log"
"net/http"
"time"
)
func sse(w http.ResponseWriter, r *http.Request) {
flusher, _ := w.(http.Flusher)
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
// 1秒おきにデータを流す
t := time.NewTicker(1 * time.Second)
defer t.Stop()
go func() {
cnt := 1
for {
select {
case <-t.C:
fmt.Fprintf(w, "data: %d\n\n", cnt)
cnt++
flusher.Flush()
}
}
}()
<-r.Context().Done()
log.Println("コネクションが閉じました")
}
func main() {
dir := http.Dir("./static")
http.HandleFunc("/event", sse)
http.Handle("/", http.FileServer(dir))
http.ListenAndServe(":8080", nil)
}
解説
main
関数については特に説明は不要かと思いますので
func sse(...)
について処理を追って説明をしていきます。
レスポンスヘッダーの設定
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
まずはレスポンスヘッダーに必要な項目を設定します。
ここについては見慣れたコードになりますね。
Flusherへのキャスト
flusher, _ := w.(http.Flusher)
ここが一番のキモです!
http.ResponseWriter
をhttp.Flusher
へキャストします。
http.Flusher
とはResponseWritersに実装されたインタフェースで、
バッファしているデータをクライアントへフラッシュするものです。
後述しますがflusher.Flush()
をすることで、
書き込んだ内容をすぐにクライアントサイドへ送信することができます。
イベントの送信
t := time.NewTicker(1 * time.Second)
defer t.Stop()
go func() {
cnt := 1
for {
select {
case <-t.C:
fmt.Fprintf(w, "data: %d\n\n", cnt)
cnt++
flusher.Flush()
}
}
}()
1秒おきにカウントアップした値を送信しています。
fmt.Fprintf
で書き込んだ後に、先程用意したflusher.Flush()
を読んであげましょう。
こうすることで書き込んだ内容がクライアントへ送信されます。
data: XX\n\n
というのはSSEの仕様であり、
他にもevent
やid
を設定することで柔軟なイベント駆動が可能になるのですが、
ここではシンプルにdata
だけを送信します。
クローズ処理
// notify := w.(http.CloseNotifier).CloseNotify()
// <-notify
<-r.Context().Done()
log.Println("コネクションが閉じました")
ここではhttp.ResponseWriter
をhttp.CloseNotifier
へとキャストします。
キャスト後にCloseNotify()
の戻り値で
コネクションが閉じたかどうかがchan bool
返却されるため取得します。
-- 2020/03/05 追記 --
CloseNotifier
はだいぶ前に非推奨になったということなので
r.Context().Done()
を利用してコネクションが閉じたかを判定するほうが良いです。
-- 追記ここまで --
あとはこのチャンネルを監視し処理を止めておくことでSSE実装完了です!
ちなみに前述したTicker
はdefer t.Stop()
しているので勝手に止まってくれます。
./static/index.html
用意したエンドポイントへつなぐ部分です。
headタグなど書いても仕方ないので、bodyの中身だけ抜粋します。
<h1>count: <span id="cnt">0</span></h1>
<script>
const ev = new EventSource('/event');
ev.addEventListener('message', (e) => {
cnt.textContent = e.data;
});
</script>
EventSource
といったものを利用することで、
簡単に先程用意したエンドポイントへ接続することができます。
まとめ
SSE自体がHTTPの簡単な仕様の上で成り立っているので、
標準ライブラリのみで簡単に実装ができました。
実際に利用する場合には、
flusher
をハンドラ外で管理したり、外部のイベントストリームを監視したりと、
色々とロジックが必要になりますが、SSEの実装部分としてはこれだけです。
簡単にサーバーサイドプッシュを実装したいときには
有力な候補になるかなとおもいます
おしまい