最初のバイトはいつ来るのか
目標
SSEで実際に直面する問題、つまりコードは合っているのに、画面に一度にまとめて出てくるという問題を、 自分で作り出し、時間で確認します。
ルール
- ファイルはすべて
/root/work/sseに作ります。 - アプリのインスタンス名は、必ず次の名前にします(
app)。採点ツールが皆さんのapp.pyを 直接importして、ストリームを読みます。 - イベントの間隔は0.3秒、ハートビートは0.2秒に合わせてください。採点ツールが 時間を測ります。
サーバーを起動する
cd /root/work/sse
uvicorn app:app --host 127.0.0.1 --port 8000 > /tmp/uv.log 2>&1 &
curl -N -s localhost:8000/stream
-Nを外すと、curlが自分の側でバッファリングするため、サーバーがきちんと流していても、
一度にまとめて出てきたように見えます。診断するとき最初に疑うべきなのは、
測定ツール自身です。
最初のバイトを測る
curl -N -s -o /dev/null -w '%{time_starttransfer}
' localhost:8000/stream
ステップ
/stream: 0.3秒間隔で3個- ワイヤーフォーマット →
02-wire.txt id:・retry:Last-Event-IDによる再開 →04-resume.txt/idle: コメントのハートビート/bufferedの再現 →06-timing.txt/long+finally→closed.log- まとめ →
08-notes.md
参考
空行(`
`)でイベントを終えないと、クライアントはまだ終わっていないと 考えて待ち続けます。「何も来ない」の原因の第1位です。
終わらないレスポンスを作る
/root/work/sse/app.pyにFastAPIアプリを作り、GET /streamがtext/event-streamでイベント3個を0.3秒間隔で送るようにしてください。
mkdir -p /root/work/sseを実行します。アプリの名前は必ずappです。StreamingResponse(gen(), media_type="text/event-stream")で十分です(sse-starletteもインストールされています)。ジェネレーターの中でawait asyncio.sleep(0.3)を呼んでください。確認はcurl -N localhost:8000/streamです。-Nがcurlのバッファリングを無効にします。
ワイヤーフォーマットを合わせる
各イベントにevent: tokenとdata:を付け、空行でイベントを終えてください。受け取った内容をそのまま02-wire.txtに残します。
1つのイベントはevent: token\ndata: 안녕\n\nです(dataの値は、韓国語で「こんにちは」を意味する語です)。最後の空行を忘れることが、「何も来ない」の原因の第1位です。保存: curl -N -s localhost:8000/stream > 02-wire.txt。
idとretryを付ける
イベントごとにid:を1から付け、ストリームの先頭にretry:を1回送ってください。
idは、ブラウザが覚えておいて、再接続のときにLast-Event-IDヘッダーで返す値です。retry: 3000は、再接続の待ち時間(ミリ秒)です。
見逃した分から続きを送る
リクエストにLast-Event-IDヘッダーがあれば、その次のidから送るようにしてください。送る総数を5個に増やし、Last-Event-ID: 3で受け取った結果を04-resume.txtに残します。
request.headers.get("last-event-id")です(小文字で参照してください)。なければ1から、あればその値+1からです。確認: curl -N -s -H 'Last-Event-ID: 3' localhost:8000/stream > 04-resume.txt。idは4と5だけが出る必要があります。これが、SSEがWebSocketより運用が安い理由です。
アイドル接続を生かしておく
GET /idleを作り、イベントなしで、コメントのハートビートだけを0.2秒間隔で5回送るようにしてください。
: ping\n\nです。コロンで始まる行はコメントなので、クライアントはイベントとして扱いません。バイトだけが流れるので、ロードバランサーのアイドルタイムアウト(たいてい60秒)を越えられます。
バッファリングを再現する
GET /bufferedを作ってください。/streamと同じ量(0.3秒ずつ5回)を待ちますが、すべて作り終えてから一度に返します。最初のバイトが届く時刻を2つの経路で測り、06-timing.txtに残してください。
ジェネレーターの代わりにリストを作り、Response(...)で返せば足ります。測定: curl -N -s -o /dev/null -w '%{time_starttransfer}\n' localhost:8000/streamと/bufferedを比べてください。ストリーミングは0.3秒以内、バッファは1.5秒ほどになります。合計時間は同じでも、最初のバイトが違います。これが、「コードは合っているのに、画面に一度にまとめて出てくる」症状の正体です。
切断に気づく
GET /longを作って長く流し続け、ジェネレーターにfinallyを入れて、切断されたときに/root/work/sse/closed.logへ1行残すようにしてください。
クライアントが切断しても、ジェネレーターは気づくまで回り続けます。try: ... finally:で後始末のコードを入れないと、タブを閉じるたびにサーバーにゾンビタスクが溜まります。確認: curl -N -s --max-time 1 localhost:8000/long > /dev/null; cat closed.log。
SSEを選ぶのはいつで、WebSocketを選ぶのはいつか
08-notes.mdに3行以上書いてください。ステップ6の2つの数値が何を意味するのか、Last-Event-IDが肩代わりしてくれることは何か、そしてWebSocketを選ぶべき場合を1つ書きます。
本文に버퍼링・Last-Event-ID・WebSocketを含めてください(1つ目の語は、韓国語で「バッファリング」を意味する語です)。判断基準は1つだけ覚えれば足ります。接続が切れたときに、何が自動で復旧されるのか。