WebSocketStream: ストリームと WebSocket API の統合

バックプレッシャーを適用して、アプリが WebSocket メッセージで埋もれたり、WebSocket サーバーにメッセージが殺到したりしないようにします。

WebSocket API は、WebSocket プロトコルへの JavaScript インターフェースを提供します。これにより、ユーザーのブラウザとサーバー間で双方向のインタラクティブな通信セッションを開くことができます。この API を使用すると、サーバーにメッセージを送信し、サーバーに返信をポーリングすることなく、イベントドリブンのレスポンスを受信できます。

Streams API

Streams API を使用すると、JavaScript はネットワーク経由で受信したデータ チャンクのストリームにプログラムからアクセスし、必要に応じて処理できます。ストリームのコンテキストで重要なコンセプトは、バックプレッシャーです。これは、単一のストリームまたはパイプチェーンが読み取りまたは書き込みの速度を制御するプロセスです。ストリーム自体またはパイプライン チェーン内の後続のストリームがまだビジー状態であり、さらにチャンクを受け入れられる状態になっていない場合、適宜配信を遅らせるためにチェーン内を後方にシグナルを送信します。

現在の WebSocket API の問題

受信したメッセージにバックプレッシャーを適用することはできません

現在の WebSocket API では、メッセージへの対応は WebSocket.onmessage で行われます。これは、サーバーからメッセージが受信されたときに呼び出される EventHandler です。

新しいメッセージが受信されるたびに、負荷の高いデータ処理オペレーションを実行する必要があるアプリケーションがあるとします。おそらく、次のコードに似たフローを設定するでしょう。process() 呼び出しの結果を await しているため、問題ないはずです。

// A heavy data crunching operation.
const process = async (data) => {
  return new Promise((resolve) => {
    window.setTimeout(() => {
      console.log('WebSocket message processed:', data);
      return resolve('done');
    }, 1000);
  });
};

webSocket.onmessage = async (event) => {
  const data = event.data;
  // Await the result of the processing step in the message handler.
  await process(data);
};

ちがう!現在の WebSocket API の問題は、バックプレッシャーを適用する方法がないことです。process() メソッドが処理できるよりも速くメッセージが到着すると、レンダリング プロセスは、これらのメッセージをバッファリングしてメモリを使い果たすか、CPU 使用率が 100% に達して応答しなくなるか、その両方になります。

送信されたメッセージにバックプレッシャーを適用するのは人間工学に適していません

送信されたメッセージにバックプレッシャーを適用することは可能ですが、WebSocket.bufferedAmount プロパティをポーリングする必要があります。これは非効率的で人間工学に適していません。この読み取り専用プロパティは、WebSocket.send() の呼び出しを使用してキューに追加されたが、まだネットワークに送信されていないデータのバイト数を返します。この値は、キューに登録されたデータがすべて送信されるとゼロにリセットされますが、WebSocket.send() を呼び続けると値は増え続けます。

WebSocketStream API とは何ですか?

WebSocketStream API は、ストリームを WebSocket API と統合することで、バックプレッシャーが存在しない、または人間工学に適していないという問題に対処します。つまり、追加費用なしで「無料で」バックプレッシャーを適用できます。

WebSocketStream API の推奨ユースケース

この API を使用できるサイトの例:

  • インタラクティビティ(特に動画や画面共有)を維持する必要がある高帯域幅の WebSocket アプリケーション。
  • 同様に、動画キャプチャなどのアプリケーションは、ブラウザで大量のデータを生成します。このデータはサーバーにアップロードする必要があります。バックプレッシャーを使用すると、クライアントはメモリにデータを蓄積するのではなく、データの生成を停止できます。

現在のステータス

ステップ ステータス
1. 説明を作成する 完了
2. 仕様の最初の下書きを作成する 作成中
3. フィードバックを収集してデザインを反復する 作成中
4. オリジン トライアル 完了
5. リリース 開始していません

WebSocketStream API の使用方法

WebSocketStream API は Promise ベースであるため、最新の JavaScript の世界で自然に扱うことができます。まず、新しい WebSocketStream を作成して、WebSocket サーバーの URL を渡します。次に、接続が opened になるまで待機します。これにより、ReadableStream または WritableStream が生成されます。

ReadableStream.getReader() メソッドを呼び出すと、最終的に ReadableStreamDefaultReader が取得されます。このオブジェクトから、ストリームが完了するまで({value: undefined, done: true} 形式のオブジェクトが返されるまで)read() データを取得できます。

したがって、WritableStream.getWriter() メソッドを呼び出すと、最終的に WritableStreamDefaultWriter が取得され、write() にデータを渡すことができます。

  const wss = new WebSocketStream(WSS_URL);
  const {readable, writable} = await wss.opened;
  const reader = readable.getReader();
  const writer = writable.getWriter();

  while (true) {
    const {value, done} = await reader.read();
    if (done) {
      break;
    }
    const result = await process(value);
    await writer.write(result);
  }

バックプレッシャー

約束されたバックプレッシャー機能はどうなりますか?追加の手順は必要ありません。process() に時間がかかる場合、次のメッセージはパイプラインの準備が整うと消費されます。同様に、WritableStreamDefaultWriter.write() ステップは安全な場合にのみ進みます。

高度な例

WebSocketStream の 2 番目の引数は、将来の拡張を可能にするオプション バッグです。唯一のオプションは protocols です。これは、WebSocket コンストラクタの 2 番目の引数と同じように動作します。

const chatWSS = new WebSocketStream(CHAT_URL, {protocols: ['chat', 'chatv2']});
const {protocol} = await chatWSS.opened;

選択した protocol と潜在的な extensions は、WebSocketStream.opened プロミスから利用できる辞書の一部です。接続が失敗した場合は関係ないため、このプロミスによってライブ接続に関するすべての情報が提供されます。

const {readable, writable, protocol, extensions} = await chatWSS.opened;

閉じられた WebSocketStream 接続に関する情報

WebSocket API の WebSocket.onclose イベントと WebSocket.onerror イベントから取得できた情報を、WebSocketStream.closed プロミスから取得できるようになりました。クリーンなクローズが行われなかった場合は Promise が拒否されます。クリーンなクローズが行われた場合、Promise はサーバーから送信されたコードと理由に解決されます。

考えられるすべてのステータス コードとその意味については、CloseEvent ステータス コードのリストをご覧ください。

const {code, reason} = await chatWSS.closed;

WebSocketStream 接続を閉じる

WebSocketStream は AbortController で閉じることができます。したがって、WebSocketStream コンストラクタに AbortSignal を渡します。

const controller = new AbortController();
const wss = new WebSocketStream(URL, {signal: controller.signal});
setTimeout(() => controller.abort(), 1000);

代わりに WebSocketStream.close() メソッドを使用することもできますが、その主な目的は、サーバーに送信されるコードと理由を指定できるようにすることです。

wss.close({code: 4000, reason: 'Game over'});

プログレッシブ エンハンスメントと相互運用性

現在、WebSocketStream API を実装しているブラウザは Chrome のみです。従来の WebSocket API との相互運用性を確保するため、受信したメッセージにバックプレッシャーを適用することはできません。送信されたメッセージにバックプレッシャーを適用することは可能ですが、WebSocket.bufferedAmount プロパティをポーリングする必要があります。これは非効率的で人間工学に適していません。

特徴検出

WebSocketStream API がサポートされているかどうかを確認するには、次のコマンドを使用します。

if ('WebSocketStream' in window) {
  // `WebSocketStream` is supported!
}

デモ

対応しているブラウザでは、埋め込まれた iframe で WebSocketStream API の動作を確認できます。また、Glitch で直接確認することもできます。

フィードバック

Chrome チームは、WebSocketStream API の使用感について、皆様のご意見をお聞かせいただきたいと考えています。

API 設計について

API が想定どおりに動作しない点はありますか?または、アイデアを実装するために必要なメソッドやプロパティが不足している場合はどうすればよいですか?セキュリティ モデルについてご質問やご意見がございましたら、対応する GitHub リポジトリで仕様に関する問題を報告するか、既存の問題にコメントを追加します。

実装に関する問題を報告する

Chrome の実装にバグを見つけましたか? それとも、実装が仕様と異なるのでしょうか?new.crbug.com でバグを報告します。できるだけ詳細な情報を含め、再現手順を簡単に説明してください。[コンポーネント] ボックスに Blink>Network>WebSockets を入力します。Glitch は、再現ケースをすばやく簡単に共有するのに適しています。

API のサポートを表示する

WebSocketStream API を使用する予定ですか?公開サポートは、Chrome チームが機能を優先順位付けするうえで役立ちます。また、他のブラウザ ベンダーに、サポートがどれほど重要であるかを示します。

ハッシュタグ #WebSocketStream を使用して @ChromiumDev にツイートを送信し、どこでどのように使用しているかをお知らせください。

関連情報

謝辞

WebSocketStream API は、Adam RiceYutaka Hirano によって実装されました。