WebTransportプロトコル解説:QUICベースのブラウザリアルタイム通信

技术架构(更新: 2026年6月18日)

WebTransport アーキテクチャ概要

WebTransportはQUICプロトコル上に構築されたブラウザAPIで、低遅延・多重化された双方向通信を提供します。HTTP/3上で動作し、QUICの0-RTTハンドシェイクやヘッドオブラインブロッキング解消などの利点を継承しています。

アプリケーション層    WebTransport API
                        ↓
トランスポート層    QUIC (UDP)
                        ↓
ネットワーク層      IP

従来方式との比較:
アプリケーション層    WebSocket API
                        ↓
トランスポート層    TCP → ヘッドオブラインブロッキング
                        ↓
ネットワーク層      IP

WebSocket vs WebTransport 比較

特徴 WebSocket WebTransport
トランスポート TCP QUIC (UDP)
ハンドシェイク遅延 1-RTT (TCP) + 1-RTT (TLS) 0-RTT (QUIC)
HOL ブロッキング ✅ 影響あり ❌ 解消
多重化 ❌ 単一ストリーム ✅ 独立ストリーム
データグラム ❌ ストリームのみ ✅ データグラム + ストリーム
信頼性 ストリーム ✅ / データグラム ❌
優先度 ✅ ストリーム単位
コネクション移行 ❌ (新TCP) ✅ (コネクションID)
ブラウザ対応 全プラットフォーム Chrome 97+

2つの通信モード

データグラムモード

データグラムは非信頼・非順序のメッセージ転送を提供し、リアルタイム音声/映像、ゲームの位置同期など、パケット損失を許容できるシナリオに適しています。

const transport = new WebTransport('https://server.example.com/chat');

await transport.ready;
const datagramWriter = transport.datagrams.writable.getWriter();
const datagramReader = transport.datagrams.readable.getReader();

await datagramWriter.write(new TextEncoder().encode('ping'));

const { value, done } = await datagramReader.read();
console.log(new TextDecoder().decode(value));

双方向ストリームモード

双方向ストリームは信頼性・順序付きのバイトストリーム転送を提供し、ファイル転送、チャットメッセージ、制御コマンドなど、パケット損失を許容できないシナリオに適しています。

const transport = new WebTransport('https://server.example.com/bidi');

await transport.ready;

const bidiStream = await transport.createBidirectionalStream();
const writer = bidiStream.writable.getWriter();
const reader = bidiStream.readable.getReader();

await writer.write(new TextEncoder().encode('信頼性メッセージ'));

const { value } = await reader.read();
console.log(new TextDecoder().decode(value));

サーバー開始ストリームの受信

const transport = new WebTransport('https://server.example.com/push');
await transport.ready;

async function readIncomingStreams() {
  const reader = transport.incomingBidirectionalStreams.getReader();
  while (true) {
    const { value: stream, done } = await reader.read();
    if (done) break;

    const dataReader = stream.readable.getReader();
    const { value } = await dataReader.read();
    console.log('サーバープッシュ:', new TextDecoder().decode(value));
  }
}

readIncomingStreams();

実践:マルチユーザーリアルタイム協調編集

class CollaborativeTransport {
  constructor(url) {
    this.url = url;
    this.transport = null;
    this.streamMap = new Map();
  }

  async connect() {
    this.transport = new WebTransport(this.url);
    await this.transport.ready;
    this.listenDatagrams();
    this.listenIncomingStreams();
  }

  async listenDatagrams() {
    const reader = this.transport.datagrams.readable.getReader();
    while (true) {
      const { value, done } = await reader.read();
      if (done) break;
      this.handleCursorUpdate(value);
    }
  }

  async listenIncomingStreams() {
    const reader = this.transport.incomingBidirectionalStreams.getReader();
    while (true) {
      const { value: stream, done } = await reader.read();
      if (done) break;
      const userId = crypto.randomUUID();
      this.streamMap.set(userId, stream);
      this.readStream(userId, stream.readable);
    }
  }

  async sendEditOperation(op) {
    const datagram = new TextEncoder().encode(JSON.stringify(op));
    await this.transport.datagrams.writable.getWriter().write(datagram);
  }

  async sendReliableMessage(userId, msg) {
    const stream = this.streamMap.get(userId);
    if (!stream) return;
    const writer = stream.writable.getWriter();
    await writer.write(new TextEncoder().encode(JSON.stringify(msg)));
    writer.releaseLock();
  }

  handleCursorUpdate(data) {
    const cursor = JSON.parse(new TextDecoder().decode(data));
    document.querySelector(`[data-user="${cursor.userId}"]`)
      ?.style.transform = `translate(${cursor.x}px, ${cursor.y}px)`;
  }

  async readStream(userId, readable) {
    const reader = readable.getReader();
    while (true) {
      const { value, done } = await reader.read();
      if (done) break;
      this.applyRemoteEdit(JSON.parse(new TextDecoder().decode(value)));
    }
  }

  applyRemoteEdit(edit) {
    console.log('リモート編集を適用:', edit);
  }

  async disconnect() {
    await this.transport?.close();
    this.streamMap.clear();
  }
}

QUIC プロトコルの中核機能

0-RTT コネクション確立

初回接続(1-RTT):
Client → CH(Initial) → Server
Client ← SH, EE, CERT, CV, FIN ← Server
Client → Finished → Server

以降の接続(0-RTT):
Client → CH(Initial) + 0-RTT Data → Server
Client ← SH, EE, CERT, CV, FIN + 1-RTT Data ← Server

コネクション移行

// ネットワーク切替(Wi-Fi → 4G)時、QUICはコネクションIDで接続を維持
// TCP方式は新たに3ウェイハンドシェイク + TLSネゴシエーションが必要
// WebTransportはQUICコネクションIDに基づき、再ハンドシェイク不要

ヘッドオブラインブロッキングなし

TCP (WebSocket):
Stream A: ████░░░░░░  ← Stream Bがブロック
Stream B: ░░░░██████  ← Stream Aの紛失パケットを待機

QUIC (WebTransport):
Stream A: ████░░░░░░  ← 独立ストリーム
Stream B: ██████████  ← Stream Aの影響を受けない

サーバー実装(Node.js)

import { Http3WebTransportSession } from '@fails-components/webtransport';

const h3server = new Http3WebTransportSession({
  cert: readFileSync('cert.pem'),
  key: readFileSync('key.pem'),
  port: 4433
});

h3server.on('session', (session) => {
  console.log('WebTransportセッション確立');

  session.on('datagram', (data) => {
    const msg = JSON.parse(data.toString());
    session.sendDatagram(Buffer.from(JSON.stringify({
      type: 'ack',
      echo: msg
    })));
  });

  session.on('stream', (stream) => {
    stream.on('data', (chunk) => {
      const msg = JSON.parse(chunk.toString());
      stream.write(Buffer.from(JSON.stringify({
        type: 'response',
        data: processMessage(msg)
      })));
    });
  });
});

h3server.start();

フロー制御と優先度

const transport = new WebTransport('https://server.example.com/');

await transport.ready;

const highPriorityStream = await transport.createBidirectionalStream();
const lowPriorityStream = await transport.createBidirectionalStream();

if ('sendOrder' in highPriorityStream.writable) {
  highPriorityStream.writable.sendOrder = 100;
  lowPriorityStream.writable.sendOrder = 1;
}

エラー処理と再接続

class ResilientTransport {
  constructor(url, options = {}) {
    this.url = url;
    this.maxRetries = options.maxRetries ?? 3;
    this.retryDelay = options.retryDelay ?? 1000;
    this.transport = null;
  }

  async connect(retries = 0) {
    try {
      this.transport = new WebTransport(this.url);
      await this.transport.ready;
      console.log('WebTransport接続成功');
      this.monitorClose();
      return this.transport;
    } catch (err) {
      if (retries < this.maxRetries) {
        const delay = this.retryDelay * Math.pow(2, retries);
        console.warn(`接続失敗、${delay}ms後にリトライ (${retries + 1}/${this.maxRetries})`);
        await new Promise((r) => setTimeout(r, delay));
        return this.connect(retries + 1);
      }
      throw new Error(`WebTransport接続失敗: ${err.message}`);
    }
  }

  monitorClose() {
    this.transport.closed.then(({ closeCode, reason }) => {
      console.warn(`接続終了: code=${closeCode}, reason=${reason}`);
      this.connect();
    });
  }
}

ベストプラクティスまとめ

  1. シナリオでモードを選択:リアルタイムA/V/ゲーム→データグラム、チャット/ファイル→双方向ストリーム
  2. 優先度の階層化:制御コマンドは高優先度、メディアデータは低優先度
  3. グレースフルデグラデーション:WebTransportの可用性を検出、不可用時はWebSocketにフォールバック
  4. コネクション移行の活用:モバイルシナリオでQUICコネクション移行により再接続中断を防止
  5. フロー制御:大容量ファイル転送時はストリームの背圧に注意、メモリオーバーフローを防止

ブラウザローカルツールを無料で試す →

#WebTransport#HTTP/3#QUIC#实时通信#流式传输