0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ブラウザの WebSocketStream API を試してみる(Node.js で用意した WebSocketサーバーとのやりとり)

Last updated at Posted at 2025-06-01

はじめに

今回の記事は、「Chrome for Developers」のページだと以下に書かれている『ブラウザでの「WebSocketStream」の話』です。

●WebSocketStream: ストリームと WebSocket API の統合  |  Capabilities  |  Chrome for Developers
https://developer.chrome.com/docs/capabilities/web-apis/websocketstream?hl=ja

image.png

2つのデバイス間で映像や音声のストリームを送るような話を調べている中で、たどり着いたものの 1つです。

ちなみに上記の調査の中では、他には WebRTC の情報が出てきたりなどしていました。それらなどを見ていた中で、今までに情報を追いかけたことがなかった「ブラウザでの WebSocketStream」がより気になって、それで今回のお試しをやってみる流れにしました。

前回の記事の内容を試した背景

JavaScript で WebSocket + ストリームを扱う話は、今回の「ブラウザでの WebSocketStream」とは異なりますが、直近で以下の記事に書いた内容で少し扱いました。

●ws の「createWebSocketStream」を用いたストリームの処理を Node.js で試す - Qiita
 https://qiita.com/youtoy/items/1db0f4280b55e9984d55

その上記の記事の中で「パッケージの ws について、情報を見ていた」ことや、その流れで「createWebSocketStream が気になった」という話を書いていました。

その前段になっていたのが、今回の「ブラウザでの WebSocketStream」の話になります。

ただブラウザの WebSocketStream よりも、上記の「ws の createWebSocketStream」のほうがサクッと試せそうだったので、まずは上記をやってみていたという状況でした。そして今回は「ブラウザでの WebSocketStream」の話です。

関連情報

まずはブラウザでの WebSocketStream について、調べて出てきたいくつかの情報を記載します。

利用可能なブラウザ

上記の Can I use... の WebSocketStream API に関するページを見ると、現状はおおまかには Chrome系のブラウザでのみ利用可能という感じのようです。

image.png

対応ブラウザが限られる点はご注意ください。

コードのサンプル

Chrome for Developers の記事によると、以下の部分で使用方法の例が書かれています。

image.png

また、上記の MDN のページでは、「完全なリスト」の部分に実装例が書かれているようです。

image.png

また MDN のページ内だと、以下にも情報があるようでした。

●WebSocketStream - Web APIs | MDN
 https://developer.mozilla.org/en-US/docs/Web/API/WebSocketStream

これらを見て、とりあえず主に以下の処理関連の部分を把握します。

  • サーバーへの接続・切断
  • ReadableStream・WritableStream の準備
  • サーバーとの間でのデータ送受信

Chrome Platform Status に書かれた情報

上記の Chrome Platform Status のページでは、概要・モチベーションについて説明が書いてあります。

image.png

モチベーションのほうを、機械翻訳してみた日本語の分を掲載してみます。

現在の WebSocket API では、受信したメッセージに対してバックプレッシャーをかけることができません。メッセージがページが処理できる速度よりも速く到着すると、レンダー プロセスはそれらのメッセージをメモリにバッファし続けるか、100% の CPU 使用率によって応答不能になるか、あるいはその両方の状況が発生します。

一方、送信するメッセージにバックプレッシャーをかけることは可能ですが、bufferedAmount プロパティをポーリングする必要があり、これは非効率で使い勝手が良くありません。

このような背景で用意されたもののようです。

さらにその下を見ると、以下のサンプルへのリンクが掲載されています。

●websocketstream-explainer/README.md at master · ricea/websocketstream-explainer
 https://github.com/ricea/websocketstream-explainer/blob/master/README.md

このあたりは、新旧の API の違いなどが書かれています。

image.png

ざっと関連情報を見ていったところで、実装を進めていきます。

実際に試す

ここから、実際に試していきます。

サーバー側

サーバー側は、前回の内容と同じものを使います。

下準備として npm i ws でパッケージをインストールしておきます。その後、以下を nodeコマンドで実行します。

import WebSocket, { WebSocketServer, createWebSocketStream } from "ws";

const wss = new WebSocketServer({ port: 8080 });

wss.on("connection", (ws) => {
  const duplex = createWebSocketStream(ws, { encoding: "utf8" });

  duplex.on("error", (err) => {
    console.error("サーバー側ストリームでエラー:", err);
  });

  duplex.pipe(duplex);

  duplex.on("data", (chunk) => {
    console.log("サーバーで受信:", chunk);
  });
});

console.log("WebSocket サーバーをポート 8080 で起動");

これで、サーバー側は準備できた状態です。

クライアント側

クライアント側は、ブラウザ上での JavaScript の処理で WebSocketStream API を扱います。

ちなみに、前回の記事で Node.js で作ったクライアント側の処理は以下でした。

import WebSocket, { createWebSocketStream } from "ws";

const ws = new WebSocket("ws://localhost:8080");

const duplex = createWebSocketStream(ws, { encoding: "utf8" });

duplex.on("error", console.error);

duplex.pipe(process.stdout);
process.stdin.pipe(duplex);

処理としては、以下を実装します。

  • サーバーへの接続
  • 読み書きの処理ができるよう準備
  • サーバーへのメッセージ送信をしつつ、送信したメッセージを出力
  • サーバーからメッセージを受信したらその内容を出力

クライアント側の実装内容

クライアント側で実装した内容は以下のとおりです。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
    <title>WebSocketStream クライアント</title>
  </head>
  <body>
    <h2>WebSocketStream Echo クライアント</h2>
    <label for="messageInput">送信メッセージ:</label>
    <input id="messageInput" type="text" placeholder="ここに文字列を入力" />
    <button id="sendButton">送信</button>

    <h3>受信メッセージ</h3>
    <pre
      id="outputArea"
      style="
        background: #f4f4f4;
        padding: 10px;
        height: 200px;
        overflow-y: scroll;
      "
    ></pre>

    <script type="text/javascript">
      (async () => {
        const wsStream = new WebSocketStream("ws://localhost:8080");

        const { readable: wsReadable, writable: wsWritable } =
          await wsStream.opened;

        const textDecoder = new TextDecoderStream();
        const textEncoder = new TextEncoderStream();

        // --- 受信側:WebSocket のバイナリ → TextDecoderStream → reader ---
        wsReadable.pipeTo(textDecoder.writable);
        const reader = textDecoder.readable.getReader();

        // --- 送信側:TextEncoderStream → WebSocket のバイナリ ---
        textEncoder.readable.pipeTo(wsWritable);
        const writer = textEncoder.writable.getWriter();

        async function receiveLoop() {
          try {
            while (true) {
              const { value, done } = await reader.read();
              if (done) {
                console.log("WebSocketStream の readable がクローズされました");
                break;
              }
              console.log("サーバーから受信:", value);
              const outputElem = document.getElementById("outputArea");
              outputElem.textContent += value + "\n";
              outputElem.scrollTop = outputElem.scrollHeight;
            }
          } catch (err) {
            console.error("受信ストリームでエラー:", err);
          }
        }
        receiveLoop();

        document
          .getElementById("sendButton")
          .addEventListener("click", async () => {
            const inputElem = document.getElementById("messageInput");
            const text = inputElem.value;
            if (text === "") return;
            try {
              console.log("サーバーへ送信:", text);
              await writer.write(text);
              inputElem.value = "";
              inputElem.focus();
            } catch (err) {
              console.error("送信でエラー:", err);
            }
          });

        window.addEventListener("beforeunload", async () => {
          try {
            await writer.close();
            await reader.cancel();
          } catch (_) {
            /* 対処できないため無視 */
          }
        });
      })();
    </script>
  </body>
</html>

クライアント側の実装の補足

クライアント側で実装した内容は少しだけ補足しておきます。

以下の部分について、これらの型は「wsReadable: ReadableStream」と「wsWritable: WritableStream」といったように、どちらもバイナリ・チャンク(Uint8Array) のストリームになるようです。

        const { readable: wsReadable, writable: wsWritable } =
          await wsStream.opened;

これについて、ブラウザ上では文字列を扱うようにしたいので、変換する処理を入れています。具体的には、以下の部分です。

        // --- 受信側:WebSocket のバイナリ → TextDecoderStream → reader ---
        wsReadable.pipeTo(textDecoder.writable);
        const reader = textDecoder.readable.getReader();

        // --- 送信側:TextEncoderStream → WebSocket のバイナリ ---
        textEncoder.readable.pipeTo(wsWritable);
        const writer = textEncoder.writable.getWriter();

上記の受信側をまずは見てみます。

        // --- 受信側:WebSocket のバイナリ → TextDecoderStream → reader ---
        wsReadable.pipeTo(textDecoder.writable);
        const reader = textDecoder.readable.getReader();

「textDecoder.writable」は、Uint8Array を書き込むための書き込み先になります。そして「wsReadable.pipeTo(textDecoder.writable)」は、WebSocket で受信したバイナリ(Uint8Array)を TextDecoderStream の書き込み側 (.writable) へ流し込んでいます。

その下の「textDecoder.readable」は、先ほど pipeTo で Uint8Array を送り込んだ結果を、内部で UTF-8 デコード済みの文字列にしていて、それを読み出せるものになります。.getReader() を呼ぶことで、読み取り用のリーダーオブジェクトを取得します。

次に送信側のほうを見てみます。

        // --- 送信側:TextEncoderStream → WebSocket のバイナリ ---
        textEncoder.readable.pipeTo(wsWritable);
        const writer = textEncoder.writable.getWriter();

上記の送信側の「textEncoder.writable」は writer.write("【何かの文字列】") のように文字列を渡すと、内部で UTF-8 バイト列 (Uint8Array) にエンコードします。そのバイナリは textEncoder.readable から取り出せます。

textEncoder.readable.pipeTo(wsWritable) の部分は、先ほどのエンコードされた UTF-8 バイト列 (Uint8Array) を、直接 wsWritable(WebSocket の送信用バイナリストリーム)に流し込みます。

これによって、writer.write("【何かの文字列】") という処理を行った時に、文字列が UTF-8バイト列に変換されて、その結果が pipeTo で WebSocket の送信ストリーム (wsWritable) に流されます。

これらの部分が、ブラウザ上で文字列を扱う処理と、ネットワーク上でバイト列が送受信される部分の間をつなぐ形になります。

表にすると、以下となります(こちらの表は、生成AI でまとめて出力してみ)。

要素 役割
wsReadable ReadableStream<Uint8Array> WebSocket から届く生のバイナリを読み込む読み取り専用ストリーム
wsWritable WritableStream<Uint8Array> バイナリを書き込むと WebSocket で送信する書き込み専用ストリーム
TextDecoderStream() オブジェクト Uint8Array を UTF-8 でデコードし、文字列を吐き出すストリーム
textDecoder.writable WritableStream<Uint8Array> デコードのための入力先(バイナリを受け取る)
textDecoder.readable ReadableStream<string> デコード済み文字列(string)を読み出せる
textDecoder.readable.getReader() ReadableStreamDefaultReader<string> 文字列チャンクを逐次的に .read() できるリーダー
TextEncoderStream() オブジェクト 文字列を UTF-8 でエンコードし、Uint8Array を吐き出すストリーム
textEncoder.writable WritableStream<string> エンコードのための入力先(文字列を受け取る)
textEncoder.readable ReadableStream<Uint8Array> エンコード済みのバイナリ(Uint8Array)を読み出せる
textEncoder.readable.pipeTo(wsWritable) メソッド エンコード済みバイナリを直接 WebSocket の送信ストリームに流す
textEncoder.writable.getWriter() WritableStreamDefaultWriter<string> 文字列を書き込むためのライターオブジェクト
writer.write("text") メソッド 文字列を TextEncoderStream に渡し → 自動でバイナリにする
wsReadable.pipeTo(textDecoder.writable) メソッド WebSocket のバイナリを TextDecoderStream に流し込む

動作確認

それでは作ったものの動作確認です。

サーバー・クライアントを起動しただけの状態は、以下のとおりです。

image.png

image.png

ここで、クライアント側から「test」という文字列を送ってみます。

以下の赤矢印で示している部分は、デバッグ用にログ出力した「クライアント側がサーバーに送信した文字列」と「サーバー側から受信した文字列」です。サーバー側は、受信した内容をそのまま返す実装なので、これらは同じ文字列になります。

また緑矢印で示している部分は、「サーバー側が受信したメッセージのログ出力」と「クライアント側が受信したメッセージを Webページ上に表示させたもの」です。

image.png

うまく、文字列のやりとりはできていそうです。

さらに、文字列をいくつか送ってみます。そうすると、以下のようになりました。

image.png

問題なく、文字列の送受信ができていることを確認できました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?