Subterranean Flower

WebTransportを用いてブラウザ上からUDP/QUICによるリアルタイム双方向通信を行う

Author
古都こと
ふよんとえんよぷよぐやま!!!

ウェブとリアルタイム通信は、もはや切り離せない関係となりました。 ロングポーリングやそれを応用したComet、チャンク通信を利用したServer-Sent Event、新しくはWebSocketという技術は、 いずれもリアルタイム通信を実現するために先人たちが築き上げてきた礎です。

それらの技術の中に、次は WebTransport が加わろうとしています。

WebSocketとTCP

WebSocketという偉大な発明は、ひとつの時代を作りました。 双方向性のリアルタイム通信がもたらした恩恵は計り知れません。

しかし一方で問題もありました。 WebSocketはTCP上に構築されたプロトコルであり、当然ながらTCPの特性による制約を受けます。 顕著な例としてTCPの到達保証や順序保証があります。TCPにおいてはパケットが途中でロスした場合は再送制御が行われます。 また、パケットには番号が振られ、送信された順番通りに組み立てなければなりません。 このおかげで「TCPを使えばパケットは必ず届くし、順番も保証できる」という高い信頼性を確保できるのですが、もちろん欠点もあります。 順序保証のため 「ひとつのパケットがロスすると後続のパケットを利用できない」 という現象が起こります。これを Head-of-Lineブロッキング と呼びます。

順序保証はTCPの大きなメリットのひとつです。しかしHoLブロッキングの影響で本来送信したかったデータの利用が遅れることは特定の分野で問題になりました。 例えば映像ストリーミングの世界では、到達保証や順序保証は必要ありません。 「現在の映像が、遅延なく届く」ことが重要であり、映像のすべてのフレームが順番通りに必ず連結できることは求めていません。 競争性の高い対戦ゲームにおいてもそうです。現在の対戦相手の座標などをリアルタイムに知ることが重要であり、数フレーム前の情報到達を保証されても、もはや何の役にも立ちません。

TCPの大きな価値となる部分が、特定の利用領域にとっては害となっていたのです。

WebRTCとUDP

信頼性に重きを置いたTCPの対照的な存在として、UDPが存在します。

UDPは「信頼性のない」通信プロトコルであり、 順序や、到達そのものを保証しない 特性があります。 何も保証せずただただ一方的にデータを送りつけるだけのプロトコルなので、仕組みが非常にシンプルで、TCPで発生する各種問題とは無縁です。

ウェブでUDP通信を行うには現在ではWebRTCを使う必要があります。WebRTCは主にビデオチャットなどに用いられています。 この話だけを聞くとWebRTCとWebSocketは相補の関係にあるように思えます。しかし実際には異なります。 WebSocketがServer-Clientでの接続方式を前提としているのに対し、WebRTCはPeer-to-Peer(サーバを介さずコンピュータ同士を直接つなぐ)方式になっています。 Peer同士の接続のためには複雑な手順を踏まねばならず、UDPを使うためだけにWebRTCをセットアップするのは労力に見合いません。

現在、ウェブにおいて気軽にServer-Client方式のUDP通信を実現する手段は存在しません。

WebTrasnportの登場

「WebSocketのように簡単に使える双方向UDP通信の仕組みが欲しい」というのがウェブ開発者の正直な気持ちでした。 そしてそれが実現しようとしています。ついに WebTransport の登場です。

WebTransportはプロトコルとして主に QUIC を用います。 QUICはUDPの上に構築された柔軟性のあるプロトコルで、UDPでありながらもTCPのように到達の保証をするという芸当もできます。

WebTransportを使えばServer-Client方式で簡単に双方向接続ができます。信頼性のある通信も信頼性のない通信も両方扱えます。 まさに夢の仕様とも言えるでしょう。

WebTransportには現在2種類のインターフェイスが存在します。 ひとつは QuicTransport です。名前の通りQUICを用いたWebTransportを実現します。接続URLには quic-transport:// を用います もうひとつは Http3Transport です。こちらは通信にHTTP/3を用いたもので、URLは https:// を用います。

実装状況

WebTransportはChrome84において QuicTransport のみ実装されています。 Chrome84時点では利用にはフラグを変更する必要があり、 chrome://flags より Experimental Web Platform features を有効化すれば使えるようになります。

他ブラウザにおいては未実装となっています。

仕様について

この記事の内容は2020/07/31時点での仕様に基づいています。 WebTransportの仕様は随時更新されていますので、変更点は適宜チェックしてください。

https://wicg.github.io/web-transport/

QUICサーバを立てる

WebTransportの利用には当然QUICに対応したサーバが必要です。 幸運なことにGoogleがQUICのサンプルサーバ実装を公開しているので、それを使いましょう。

以下のURLからダウンロードできます。

https://github.com/GoogleChrome/samples/blob/7b9ca12e0a8c7e35837ac32ab03f7f0f0c5ce8bd/quictransport/quic_transport_server.py

このサーバの実行にはPython3.6以降と aioquic というライブラリが必要になります。 aioquic のインストールにはOpenSSLのヘッダが必要になりますので、以下のREADMEを参考にインストールしてください。

https://github.com/aiortc/aioquic

OpenSSLのヘッダをインストールできたら、 aioquic をインストールします。

pip3 install aioquic

QUICはTLSが必須となっており、何かしらの証明書がないと動かないので適当に作ります。 openssl コマンドはバージョン1.1.1以上を使います。 aioquic インストールの過程でOpenSSLもインストールされているはずなのでコマンドを叩きます。 (※ macOSの場合はbrewでインストールした openssl コマンドをフルパスで実行します)

/usr/local/opt/openssl/bin/openssl req -newkey rsa:2048 -nodes -keyout certificate.key \
                   -x509 -out certificate.pem -subj '/CN=Test Certificate' \
                   -addext "subjectAltName = DNS:localhost"

以下のような出力が出ればOKです。certificate.keycertificate.pem という鍵ファイルができているはずです。

Generating a RSA private key
................................+++++
...........................+++++
writing new private key to 'certificate.key'
-----

これでサーバが起動できます。

python3 quic_transport_server.py certificate.pem certificate.key

ChromeからQuicTransportを使う

まずはChrome84以降で chrome://flags より Experimental Web Platform features を有効にします。 Chromeの再起動を促されてボタンが出てくるので、ボタンを押すとChromeが再起動します。 これで QuicTransport が使えるようになります。まだ Http3Transport は使えません。

さっき作った適当な鍵を許可するようにChromeを起動します。鍵のダイジェストを取得します。

openssl x509 -pubkey -noout -in certificate.pem |
                   openssl rsa -pubin -outform der |
                   openssl dgst -sha256 -binary | base64

以下のような出力が得られます。

ADGFAqYrog/Dv09f/5FK1H7ZkoBQ3PtL4wYWsWYBEDQ=

このダイジェストを用いてChromeを起動します。macOSなら以下のようにします。

open "/Applications/Google Chrome.app" \
  --args --origin-to-force-quic-on=localhost:4433 \
  --ignore-certificate-errors-spki-list=ADGFAqYrog/Dv09f/5FK1H7ZkoBQ3PtL4wYWsWYBEDQ=

Windowsの場合はChromeのショートカットを作って、起動コマンドを以下のようにします。

chrome.exe --origin-to-force-quic-on=localhost:4433 --ignore-certificate-errors-spki-list=ADGFAqYrog/Dv09f/5FK1H7ZkoBQ3PtL4wYWsWYBEDQ=

JavaScript側からQuicTransportを利用する

やっとJavaScriptの話です!

HTMLファイルから用意します。とりあえず動けばいいので以下のような感じでしょう:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>WebTransport</title>
    <script src="main.js" defer></script>
  </head>
  <body></body>
</html>

main.js ファイルには以下のように記入します:

(async function main() {
  // QuicTransportをつなぐ
  const transport = new QuicTransport('quic-transport://localhost:4433/counter');
  await transport.ready;

  // 信頼性のない送信経路
  // WritableStreamDefaultWriter( https://developer.mozilla.org/ja/docs/Web/API/WritableStreamDefaultWriter )
  const writer = transport.sendDatagrams().getWriter();

  // 信頼性のない受信経路
  // ReadableStreamDefaultReader( https://developer.mozilla.org/ja/docs/Web/API/ReadableStreamDefaultReader )
  const reader = transport.receiveDatagrams().getReader();

  // stringとTypedArrayとの相互変換に使用する
  const [encoder, decoder] = [new TextEncoder(), new TextDecoder()];

  // メッセージを送信する(到達するとは限らない)
  await writer.write(encoder.encode('Hello!!'));
  writer.close();

  // メッセージを受信する(受信できるとは限らない)
  const result = (await reader.read()).value;
  console.log(decoder.decode(result));
})();

これで信頼性のない通信ができます。データの到達は保証されませんが、それゆえに高速に通信することが可能です。 通信にはStreams APIを使用します。読み書きにはそれぞれ ReadableStreamWritableStream が割り当てられています。 Streams APIについては私の記事「JavaScriptのStreams APIで細切れのデータを読み書きする」でも触れているので、参考にしてください。

quic_transport_server.py は受信した文字列の長さをstringで返すサーバです。例えば 'Hello!!' を渡すと '7' が返ってきます。 今回はローカルでサーバを動かしているのでほぼ間違いなくデータの送受信ができると思います。

先ほどのHTMLファイルを、自作鍵の許可をして起動したChromeで開くと正常に実行されるはずです。コンソールに '7' と表示されたら成功です。

信頼性のある通信をする

データが確実に届く、信頼性のある通信路を確保するには async createBidirectionalStream() メソッドを使います。

(async function main() {
  // QuicTransportをつなぐ
  const transport = new QuicTransport('quic-transport://localhost:4433/counter');
  await transport.ready;

  // 信頼性のある双方向ストリーム
  const stream = await transport.createBidirectionalStream();
  const reader = stream.readable.getReader();
  const writer = stream.writable.getWriter();

  // WritableStreamが不要ならReadableStreamだけを直接取得できる
  //   const reader = transport.receiveBidirectionalStreams().getReader();

  // stringとTypedArrayとの相互変換に使用する
  const [encoder, decoder] = [new TextEncoder(), new TextDecoder()];

  // メッセージを送信する
  await writer.write(encoder.encode('Hello!!'));
  writer.close();

  // メッセージを受信する
  const result = (await reader.read()).value;
  console.log(decoder.decode(result));
})();

これで読み書きのストリームが手に入ります。

双方向ではなく書き込みだけの単方向通信が行いたい場合は、 async createSendStream() または receiveStreams() メソッドで単方向のストリームを取得できます。async createSendStream() が送信専用、 receiveStreams() が受信専用です。

(async function main() {
  // QuicTransportをつなぐ
  const transport = new QuicTransport('quic-transport://localhost:4433/counter');
  await transport.ready;

  // 信頼性のある単方向ストリーム
  const stream = await transport.createSendStream();
  const writer = stream.writable.getWriter();

  // stringとTypedArrayとの相互変換に使用する
  const [encoder, decoder] = [new TextEncoder(), new TextDecoder()];

  // メッセージを送信する
  await writer.write(encoder.encode('Hello!!'));
  writer.close();
})();

その他の細かい使い方

接続を閉じる場合は close() メソッドを呼び出します。

(async function main() {
  // QuicTransportをつなぐ
  const transport = new QuicTransport('quic-transport://localhost:4433/counter');
  await transport.ready;

  // 接続を閉じる
  transport.close();
})();

さいごに

WebTransportの実装が進めば、より気軽にUDP通信が実現できるようになるでしょう。 TCPでは難しかった、低レイテンシのリアルタイム通信が達成できます。

WebTransportはUDPだけでなくQUICやHTTP/3を用いた到達保証のある通信もできるので、WebSocketのモダンな置き換えが可能になるでしょう。互換性や企業ファイアウォールのことを考えるとまだまだWebSocketも現役ですが、将来的にはWebTransportも有力な選択肢になります。

一方でWebRTCに関しては住み分けが大事になってきます。 Server-Clientで通信したい場合はWebTransport、Peer-to-Peerで通信したい場合はWebRTC、という選択肢になります。

ウェブ技術の移り変わりは目まぐるしく、しばしば驚きを与えてくれます。 最新技術にキャッチアップしつつ、これからどんなことが可能になっていくのか、楽しみに待ちましょう。