html5 bootstrap template

May 11, 2020

Node.jsのhttp2モジュールを使ってServer Pushの送信/受信を試す

Generic Event Delivery Using HTTP Push (RFC 8030) を読んでいて、HTTP2のServer Pushをリアルタイムなデータの配送に用いるという話があり、似たことをNode.jsでやってみました。

やりたいこと

6. Receiving Push Messages for a Subscription6.1. Receiving Push Messages for a Subscription Setの項目を見ると、Push Service(GCMやAutoPushなどブラウザベンダのサーバー)からUser Agent(ChromeやFirefoxなどのソフトウェアや端末)への通知の送信に、HTTP2のServer Pushを使うとあります。WebにおけるServer Pushの使われ方としては、HTMLのリクエストと同時に関連するアセットを送るようなものが一般的ですが、RFCにあるようにGETのリクエストをそのままにした上でServer Pushでデータを任意のタイミングで送ることができるのか、Node.jsでの実装を試してみました。ネット上にあまりサンプルがなかったので記事として残しておきます。

GitHubのリポジトリにコードを上げておいたので、実際に動かしたい方はそちらからも行えます。

サーバー側

サーバー側のコードは以下のようになりました。

const http2 = require("http2");
const fs = require("fs");
const path = require("path");

const { HTTP2_HEADER_PATH } = http2.constants;

const server = http2.createServer();

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

server.on("stream", (stream, headers) => {
  (async () => {
    let disconnected = false;
    stream.on("close", () => {
      disconnected = true;
    });
    for (let i = 1; i <= 10; i++) {
      if (disconnected || !stream.pushAllowed) {
        break;
      }
      stream.pushStream(
        { [HTTP2_HEADER_PATH]: "/random" + i },
        (err, pushStream) => {
          console.log(`push ${i}`);
          pushStream.respond({ ":status": 200 });
          pushStream.end(`push ${i}`);
        }
      );
      await new Promise((resolve) => setTimeout(resolve, Math.random() * 2000));
    }
    if (!disconnected) stream.end("end stream");
  })();
});

server.listen(3000);

詳しいことはNode.jsのHTTP/2に関するドキュメントを読むのが早いと思いますが、ServerHttp2Streamというクラスの使い方がポイントになります。pushStreamメソッドを使って新しくServer PushしたStreamを作ることができます。通常のStreamと同じように、respondend を使ってデータの送信ができます。

このコードでは、0秒から2秒の間のランダムなタイミングでServer Pushでデータを10回送り、最後に元となったリクエストにデータを返しています。

クライアント側

Server Pushをハンドルするクライアント側のコードは以下のようになります。

const http2 = require("http2");

const client = http2.connect("http://localhost:3000");

client.on("stream", (pushedStream, requestHeaders) => {
  pushedStream.on("push", (responseHeaders) => {
    console.log("push", responseHeaders);
  });
  let data = "";
  pushedStream.on("data", (chunk) => {
    data += chunk;
  });
  pushedStream.on("end", () => {
    console.log(`Pushed: ${data}`);
  });
});

const req = client.request({ ":path": "/" });
req.setEncoding("utf8");

let data = "";
req.on("data", (chunk) => {
  data += chunk;
});

req.on("end", () => {
  console.log(`finish: ${data}`);
  client.close();
});
req.end();

サーバー側のServerHttp2Streamに対応するようにクライアント側にはClientHttp2Streamがあり、これのPushイベントをlistenすれば良いと最初は思ったのですが、勘違いでした。

ここでやりたいことは、Push streams on the clientに書いてあります。具体的には、ClientHttp2Sessionstreamイベントを使うことでサーバー側からプッシュされた通信をStreamとして取得でき、そのdataendイベントをlistenすることでデータを取得できます。

動作

実際の動作は以下のようになります(GIFのサイズが大きくてごめんなさい)。

TODO

Expressのようなフレームワークとどう組み合わせられるのか分かっていないです。HTTP2のCompativility APIの項目を見ると、req, resを引数にとるハンドラの場合はHttp2ServerResponseがresに渡されるとあり、これの stream を使えばHttp2ServerStreamが取れるようなので、そこからpushStreamを呼べるかもしれないです。