04. セッションとストリームの使い方

🎯 この章のゴール

  • MOQT セッションが、制御ストリーム(単方向×2)、リクエストストリーム(双方向)、データストリーム(単方向)でできていることを理解する
  • SETUP によるセッション初期化と拡張のネゴシエーションを知る
  • Request ID、リクエストのキャンセル、GOAWAY によるセッション移行を説明できる

💡 ひとことで言うと

1 リクエストにつき 1 本の双方向ストリームを使います。HTTP/3 と同じ発想で、リクエストごとに専用の電話回線を引き、用が済んだら切ります。

ストリームの 3 つの役割

draft-22 のセッションでは、QUIC(または WebTransport)のストリームを次の 3 種類に使い分けます。

クライアントサーバー制御ストリーム(単方向): SETUP, GOAWAY制御ストリーム(単方向): SETUP, GOAWAYリクエストストリーム(双方向): SUBSCRIBE ⇄ SUBSCRIBE_OK …リクエストストリーム(双方向): FETCH ⇄ FETCH_OK …データストリーム(単方向): SUBGROUP_HEADER + Objectsデータストリーム(単方向): FETCH_HEADER + Objects
MOQT セッションのストリーム構成
ストリーム向き先頭中身
制御ストリーム単方向(各ピア 1 本)SETUPSETUP、GOAWAY。セッション中は閉じてはいけない
リクエストストリーム双方向(リクエストごと)リクエストメッセージリクエスト・応答・更新・終了通知
データストリーム単方向SUBGROUP_HEADER / FETCH_HEADERObject の本体(08 章)

制御ストリームを単方向 2 本にしているのは、どちらのピアも準備ができ次第すぐ送り始められるようにするためです。0-RTT が使えればクライアントが先に、そうでなければサーバーが先に送れることもあります。

セッションの初期化: SETUP

各ピアは自分の制御ストリームの最初に SETUP を送ります。中身は Setup Option というキーと値のペアの列です。

Setup Option内容
PATH / AUTHORITYネイティブ QUIC のとき、URI の path(+query)と authority を伝える(クライアントのみ)
AUTHORIZATION TOKENセッション確立用の認可トークン
MAX_AUTH_TOKEN_CACHE_SIZEトークンのエイリアス登録に使えるキャッシュの上限
MOQT_IMPLEMENTATION実装名とバージョン(相互接続のデバッグ用)
MAX_FILTER_RANGES受け付けるレンジフィルタ数の上限(0 ならフィルタ不可)
MAX_REQUEST_UPDATES1 リクエストあたりの未応答 REQUEST_UPDATE の上限

未知の Setup Option は無視しなければなりません(MUST)。拡張機能のサポート表明もこの仕組みで行います。両方の SETUP を送受信した時点で、使える拡張が確定します。

相手の SETUP を待たずにリクエストを送ることもできるので、クライアントはセッション開始、購読、配信開始を同時に始められます。

sequenceDiagram
  participant C as クライアント
  participant S as サーバー
  Note over C,S: QUIC / WebTransport 確立
  C->>S: 制御ストリーム: SETUP (PATH, MOQT_IMPLEMENTATION, …)
  S->>C: 制御ストリーム: SETUP (MAX_REQUEST_UPDATES, …)
  C->>S: 新しい双方向ストリーム: SUBSCRIBE (Request ID = 0)
  S-->>C: 同じストリーム: SUBSCRIBE_OK
  S->>C: 単方向ストリーム: SUBGROUP_HEADER + Objects…

リクエストストリーム

双方向ストリームの先頭に置けるのは次の 7 種類のリクエストだけです(ネゴシエーションした拡張を除く)。

TRACK_STATUS / SUBSCRIBE / PUBLISH / FETCH / PUBLISH_NAMESPACE / SUBSCRIBE_NAMESPACE / SUBSCRIBE_TRACKS

応答は多くの場合、共通の REQUEST_OK か REQUEST_ERROR で返します。仕様では文脈に応じて PUBLISH_OK(PUBLISH への REQUEST_OK)や SUBSCRIBE_ERROR(SUBSCRIBE への REQUEST_ERROR)のような略称で呼びます。SUBSCRIBE と FETCH だけは専用の SUBSCRIBE_OK / FETCH_OK で応答します。

Request ID

リクエストメッセージには Request ID が入ります。

  • クライアントは 0 から始まる偶数、サーバーは 1 から始まる奇数を使い、2 ずつ増やす
  • 応答メッセージは同じストリームで返るので Request ID を持たない
  • 下位ビットが送信者と合わない、または重複していたら INVALID_REQUEST_ID でセッションを閉じる

終わらせ方: FIN とキャンセル

  • FIN は「この方向にはもう送らない」という意味で、キャンセルではありません。応答側が必要なメッセージ(例: 購読なら最後の PUBLISH_DONE)を送り終えてから FIN すると、そのリクエストは完了です
  • キャンセルするときは、送信方向を RESET_STREAM、受信方向を STOP_SENDING で打ち切る
  • 何も処理せずに断るときは REQUEST_ERROR を送って FIN します

REQUEST_ERROR には Retry Interval(再試行までの最小ミリ秒 + 1。0 なら再試行しない)や、エラーコード REDIRECT のときの転送先(別の URI やトラック名)を含められます。

📜 ワイヤーフォーマットの基本

制御メッセージの形

制御ストリームとリクエストストリーム上のメッセージはすべて同じ形です。

MOQT Control Message {
  Message Type (vi64),
  Message Length (16),   ← 16 ビット固定。メッセージは最大 65,535 バイト
  Message Body (..),
}

未知のメッセージタイプを受け取ったらセッションを閉じます。長さフィールドはパースを楽にするためのもので、未知のメッセージを読み飛ばすためのものではありません。

可変長整数 vi64

MOQT 独自の可変長整数で、先頭バイトの上位から続く 1 のビット数で長さを表します。QUIC の varint(先頭 2 ビットで 1/2/4/8 バイト)とは別の形式です。

先頭ビットバイト数範囲
010〜127
1020〜16,383
11030〜2,097,151
………
1111111080〜2^56−1
1111111190〜2^64−1

例: 0x25 = 37、0x8025 も 37(最短でなくてもよい)、0xbbbd = 15,293。

Key-Value-Pair

Setup Option やプロパティは Key-Value-Pair で表します。Type が偶数なら値は vi64 1 個、奇数なら Length とバイト列です。Type は直前の Type からの差分(Delta Type)で書くので、昇順に並べると小さく済みます。

SUBSCRIBE などに付く Message Parameter も差分符号化した Type を使いますが、値の形式はパラメータごとに定義されます。未知のパラメータを受け取ったら PROTOCOL_VIOLATION でセッションを閉じます。読み飛ばす必要がないので、範囲は長さではなく個数で区切ります。未知を無視する Setup Option とは扱いが逆です。

0-RTT

ネイティブ QUIC では 0-RTT で最初のパケットから SUBSCRIBE などを送れます。MOQT のメッセージは基本的にリプレイされても安全(SUBSCRIBE はトラックの中身を変えない、重複 Object は ID で捨てられる)ですが、リレーに無駄な上流購読をさせる恐れがあるため、リレーはハンドシェイク完了まで上流購読を遅らせたり 0-RTT を拒否したりしてもかまいません。なお WebTransport は CONNECT が安全なメソッドでないため、0-RTT は使わない想定です。

セッションの終了と移行

  • 終了: ネイティブ QUIC なら CONNECTION_CLOSE、WebTransport なら CLOSE_WEBTRANSPORT_SESSION。エラーコードは NO_ERROR、PROTOCOL_VIOLATION、GOAWAY_TIMEOUT など
  • GOAWAY: 長時間のライブでは、全セッションが自然に終わるのを待つと何日もかかります。サーバーは GOAWAY で新しい URI(省略時は同じ URI)とタイムアウトを伝え、クライアントは裏で新セッションを張って購読や名前空間の公開を移します
sequenceDiagram
  participant C as クライアント
  participant R1 as リレー A(停止予定)
  participant R2 as リレー B
  R1->>C: GOAWAY (New Session URI = moqt://b.example, Timeout = 30000)
  C->>R2: 新しいセッション + SUBSCRIBE(同じトラック)
  R2-->>C: SUBSCRIBE_OK
  C->>R1: 旧購読をキャンセル
  C->>R1: セッションを NO_ERROR で閉じる

GOAWAY はリクエストストリーム上で送ることもでき、その場合はそのリクエストだけを移します(例: 同じトラックのパブリッシャとサブスクライバを同じリレーに寄せる)。

🧪 考えてみよう

以前のドラフトは制御メッセージを 1 本の双方向ストリームにまとめて流していました。draft-22 がリクエストごとにストリームを分けたのはなぜでしょう? 1 本のストリームだと、ある応答のパケットロスが無関係なリクエストまで止めてしまいます(HoL ブロッキング)。ストリームを分ければ、リクエストの終了も FIN / RESET_STREAM という QUIC の仕組みで自然に表現できます。

📝 まとめ

  • ストリームは、制御ストリーム(単方向×2、SETUP から始まる)、リクエストストリーム(双方向、1 リクエスト 1 本)、データストリーム(単方向)の 3 種類
  • Request ID はクライアント偶数・サーバー奇数。応答は REQUEST_OK / REQUEST_ERROR が基本
  • 可変長整数 vi64 は先頭の 1 の数で長さを決める MOQT 独自形式
  • GOAWAY でセッション(またはリクエスト単位)を別サーバーへ穏やかに移せる

参考

✅ 理解度クイズ

Q1
draft-22 で、制御メッセージ(SETUP や GOAWAY)を交換するのに使うストリームは?
Q2
SUBSCRIBE や FETCH などのリクエストはどこで送る?
Q3
クライアントが発行する Request ID の並びとして正しいものは?
Q4
サーバーがメンテナンスのため、セッションを穏やかに別サーバーへ移したい。送るメッセージは?
Q5
MOQT の可変長整数(vi64)の説明として正しいものは?