- WebSocket はリアルタイム通信に有用だが、常に必要とは限らず、HTTP ベースの代替手段のほうがよりシンプルで安定している場合がある
- トランザクション処理、接続管理、サーバーの複雑性といった面で、WebSocket は過剰なオーバーヘッドを引き起こすことがある
- HTTP Streaming とライブラリ
eventkit を活用すれば、WebSocket なしでもリアルタイム同期やイベント処理が可能
WebSocket とは何か
- WebSocket はクライアントとサーバーの間に持続的な双方向通信チャネルを開く技術
- HTTP を通じて接続を開始するが、その後は別のプロトコルで通信が行われる
- リアルタイムアプリケーションの実装でよく使われ、双方向通信が可能という点で有用
WebSocket メッセージはトランザクション方式ではない
- WebSocket はリクエストとレスポンスの直接的な対応関係を保証しない
- 状態変更コマンドとその結果メッセージが同じストリーム上で混在して到着することがある
- たとえば、1 つのクライアントが状態を変更してエラーが発生しても、どのコマンドに対するエラーなのか把握しにくい
- 解決策として
requestId を含めてコマンドとレスポンスを結び付ける方法があるが、これは複雑性と管理コストを増やす
- HTTP を使ったトランザクション方式でコマンドを送り、WebSocket は状態変更のブロードキャストのみに使うほうがシンプル
- 送信側は HTTP リクエスト、受信側は WebSocket または別のストリーミング方式として分離できる
WebSocket 接続ライフサイクル管理の難しさ
- WebSocket を使う場合、接続の開始、終了、エラー、再接続などを自前で処理しなければならない
- ブラウザでの基本的な処理例には、接続オープン、メッセージ受信、エラー発生、接続終了のイベントハンドリングが含まれる
- 再接続ロジック、メッセージバッファリング、指数バックオフなどの追加ロジックが必要になる
- 一方 HTTP はリクエスト単位で開始と終了が明確なため、実装がシンプル
- 複雑なライフサイクル管理は、WebSocket を使う理由が明確な場合にのみ正当化される
サーバーコードの複雑性が増す
- WebSocket では HTTP アップグレードリクエストを処理する必要があり、そのため追加のハンドシェイクロジックが求められる
Sec-WebSocket-Key のような特殊ヘッダーを検証し、レスポンスヘッダーを適切に返さなければならない
- WebSocket 接続後は継続的なメッセージ受信・送信状態を維持する必要があり、部分フレーム処理のような問題も起こりうる
- HTTP のみを使う場合と比べて、デバッグやエラー処理の難易度が高くなる
- フレームワークが一部の工程を抽象化してくれても、根本的な複雑性は依然として残る
代替案: HTTP Streaming
- HTTP はもともとストリーミングをサポートするプロトコルで、ファイル全体ではなくデータストリームをリアルタイムで転送できる
- 従来 WebSocket が担っていた受信側の機能だけを HTTP ストリーミングで置き換えられる
- 非同期ジェネレーターを使って状態更新をストリーム形式で処理できる
- サーバー側の流れ
- 状態更新はコマンド処理関数で実行される
- 接続されたクライアントはジェネレーターを通じて、新しい値が出るたびに受け取る
- 状態変更コマンドは HTTP POST で送信し、リアルタイムストリームは GET リクエストで購読する
- クライアント側の流れ
- Fetch API と Stream Reader を通じてリアルタイムデータを受信する
- テキストをデコードした後に UI を更新する
- この構成により、WebSocket なしでもリアルタイムな状態同期を実装できる
ボーナス: eventkit ライブラリの紹介
eventkit は非同期ストリームを簡単に構成・監視できるようにするライブラリ
- RxJS に似ているが、副作用管理が改善されており、ジェネレーター ベースで設計されている
- 状態更新をストリームに push すると、クライアント側でそれをリアルタイムに受信できる
Stream および AsyncObservable により、サーバー/クライアントの両方で簡単に実装できる
- サーバー側での eventkit 活用
- 状態変更を Stream に push し、クライアントはそのストリームを購読する
- クライアント側での eventkit 活用
- ストリームデータを受け取り、デコード後に UI を更新する
- 公式 GitHub リポジトリと HTTP Streaming ガイドも提供されている
GitHub: https://github.com/hntrl/eventkit
3件のコメント
Hacker Newsの意見
HTTPストリーミングはこのパターンを念頭に設計されたものではないと思う。HTTPストリーミングは大きなデータを断片に分ける用途だ。ストリーミングを pub/sub メカニズムのように使うと後悔するかもしれない。HTTPの中継者たちはこのトラフィックパターンを想定していない(NGINX、CloudFlare など)。WiFi 接続が切れるたびに fetch API がリクエスト失敗のエラーを投げそうだ
RequestID をサーバーに送ってリクエスト/レスポンスのサイクルを得るのは、奇妙でも過剰でもない。本気のアプリなら
send(message).then(res => ...)のような API を備えている価値は常にあるheaders['authorization']を読むミドルウェアを再利用する代わりに、リクエストヘッダーのふりをしたconnectionParamsオブジェクトにアクセスしなければならない動画ストリーミングは、クライアントが範囲指定でチャンクを要求するもので、単一の HTTP 接続ではない
EventKit の代わりに SSE を使うのがよい
POC では従来の HTTP フォーム送信を使う予定だ。それ以外は必要ない
HTTP/2 の問題は、サーバープッシュが既存プロトコルの上に追加されたことだ。HTTP はリソース転送プロトコルであり、不要なオーバーヘッドを加える。HTTP/2 の主な目的は、サーバーがファイル/リソースをクライアントに事前プッシュして往復遅延を減らすことだ
WebSockets はストリームとして送るのではなく、データグラム(パケット)として送るものだ。JavaScript ライブラリの WebSockets API はバックプレッシャーを処理できず、すべてのエラーも処理できない。TCP ストリームとして使うなら注意が必要だ
WebSockets を本番投入したあとで後悔した。NGINX が 4/8 時間後に接続を切ったり、ブラウザがスリープ後に再接続しなかったりする問題があった。可能なら WebSockets と長期接続は避けるべきだ
WebSockets には理想化された認識がある。ストリーミング/リアルタイム用途に WebSockets を使おうとしがちだ。WebSockets は HTTP ツールのシンプルさと利点を失わせる。ストリーミングなサーバー変更への解決策は h2/h3 と SSE だ。クライアントごとに最大 0.5req/s でバッチ化できるなら WebSockets は必要ない
HTTP ストリーミングに関心がある人は Braid-HTTP を確認するとよい。HTTP にイベントストリーミングをエレガントに拡張し、強力な状態同期プロトコルを提供する