SSE を触れたことがなかったので簡単なサンプルを作ってみました。
Server-Sent Events (SSE)
Traditionally, a web page has to send a request to the server to receive new data; that is, the page requests data from the server. With server-sent events, it's possible for a server to send new data to a web page at any time, by pushing messages to the web page. These incoming messages can be treated as Events + data inside the web page.
従来、Web ページは新しいデータを受信するにはサーバーにリクエストを送信する必要があり、ページがサーバーにデータを要求する必要がありましたが、SSEを使用すると、サーバー側が メッセージを Web ページにプッシュすることで、サーバーはいつでも Web ページに新しいデータを送信できるようになるための技術であるようです。 Web Socket と似ていますが、Web Socket は双方向通信が必要であるのに対して、SSE はサーバーからクライアントへの一方向通信であるため、WebSocket よりもシンプルな実装が可能とされているようです。
サンプル
以下にPHPのコードがあったので、これをGoに置き換えたものを作成してみました。
クライアントサイドの HTML はほぼそのままです。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Server-sent events demo</title> </head> <body> <button>Close the connection</button> <ul> </ul> <script> const button = document.querySelector('button'); const evtSource = new EventSource('/sse'); // PHP版の 'sse.php' から '/sse' に変更 console.log(evtSource.withCredentials); console.log(evtSource.readyState); console.log(evtSource.url); const eventList = document.querySelector('ul'); evtSource.onopen = function() { console.log("Connection to server opened."); }; evtSource.onmessage = function(e) { const newElement = document.createElement("li"); newElement.textContent = "message: " + e.data; eventList.appendChild(newElement); }; evtSource.onerror = function() { console.log("EventSource failed."); }; button.onclick = function() { console.log('Connection closed'); evtSource.close(); }; evtSource.addEventListener("ping", function(e) { var newElement = document.createElement("li"); var obj = JSON.parse(e.data); newElement.innerHTML = "ping at " + obj.time; eventList.appendChild(newElement); }, false); </script> </body> </html>
サーバーサイドを以下のような形にしました。
ackage main import ( "fmt" "log" "math/rand" "net/http" "time" ) func sseHandler(w http.ResponseWriter, r *http.Request) { loc, err := time.LoadLocation("Asia/Tokyo") if err != nil { log.Printf("Error loading timezone: %v", err) loc = time.UTC // フォールバックとしてUTCを使用 } w.Header().Set("X-Accel-Buffering", "no") w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") // 追加のSSEヘッダー (一般的に必要) w.Header().Set("Connection", "keep-alive") // フラッシャーの確認 flusher, ok := w.(http.Flusher) if !ok { http.Error(w, "Streaming not supported", http.StatusInternalServerError) return } counter := rand.Intn(10) + 1 for { curDate := time.Now().In(loc).Format(time.RFC3339) fmt.Fprintf(w, "event: ping\n") fmt.Fprintf(w, "data: {\"time\": \"%s\"}\n\n", curDate) counter-- if counter == 0 { fmt.Fprintf(w, "data: This is a message at time %s\n\n", curDate) counter = rand.Intn(10) + 1 } flusher.Flush() select { case <-r.Context().Done(): // 接続が中断された場合 log.Println("Connection aborted by client") return default: // 接続継続中 } // sleep(1) time.Sleep(1 * time.Second) } } func main() { // ランダムシード初期化) rand.New(rand.NewSource(time.Now().UnixNano())) // ルートハンドラー http.HandleFunc("/sse", sseHandler) // サーバー起動 fmt.Println("SSE server started at http://localhost:8080/sse") if err := http.ListenAndServe(":8080", nil); err != nil { log.Fatal(err) } }
実行方法
% go run main.go
ブラウザで http://localhost:8080/sse を開くと、サーバーからのメッセージが表示されます。
まとめ
SSEに触れるために、MDNのサンプルを参考にして、Goで実装してみました。 SSEは、サーバーからクライアントへの一方向通信を実現するためのシンプルな技術であり、HTTPベースで動作するため、特別なプロトコルを必要としないので、標準ライブラリのみでとりあえず実装できました。
自動再接続機能やイベント ID によるメッセージの追跡が可能で、テキストベースの通信が行えるため、さまざまな用途に利用できるでしょう。