04. セッションとストリームの使い方
🎯 この章のゴール
- MOQT セッションが、制御ストリーム(単方向×2)、リクエストストリーム(双方向)、データストリーム(単方向)でできていることを理解する
- SETUP によるセッション初期化と拡張のネゴシエーションを知る
- Request ID、リクエストのキャンセル、GOAWAY によるセッション移行を説明できる
💡 ひとことで言うと
1 リクエストにつき 1 本の双方向ストリームを使います。HTTP/3 と同じ発想で、リクエストごとに専用の電話回線を引き、用が済んだら切ります。
ストリームの 3 つの役割
draft-22 のセッションでは、QUIC(または WebTransport)のストリームを次の 3 種類に使い分けます。
| ストリーム | 向き | 先頭 | 中身 |
|---|---|---|---|
| 制御ストリーム | 単方向(各ピア 1 本) | SETUP | SETUP、GOAWAY。セッション中は閉じてはいけない |
| リクエストストリーム | 双方向(リクエストごと) | リクエストメッセージ | リクエスト・応答・更新・終了通知 |
| データストリーム | 単方向 | SUBGROUP_HEADER / FETCH_HEADER | Object の本体(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_UPDATES | 1 リクエストあたりの未応答 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 バイト)とは別の形式です。
| 先頭ビット | バイト数 | 範囲 |
|---|---|---|
0 | 1 | 0〜127 |
10 | 2 | 0〜16,383 |
110 | 3 | 0〜2,097,151 |
| … | … | … |
11111110 | 8 | 0〜2^56−1 |
11111111 | 9 | 0〜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 でセッション(またはリクエスト単位)を別サーバーへ穏やかに移せる
参考
- draft-ietf-moq-transport-22 §6.3 Session initialization / §6.4 Stream Usage
- §8 Notational Conventions / §9.1 SETUP / §9.2 GOAWAY
✅ 理解度クイズ
各ピアが SETUP から始まる単方向の制御ストリームを 1 本ずつ開きます。双方向 1 本ではなく単方向 2 本にすることで、0-RTT の有無にかかわらず、どちらも準備ができ次第送信を始められます。
draft-22 では、TRACK_STATUS・SUBSCRIBE・PUBLISH・FETCH・PUBLISH_NAMESPACE・SUBSCRIBE_NAMESPACE・SUBSCRIBE_TRACKS の 7 種類が新しい双方向リクエストストリームの先頭で送られます。応答や更新も同じストリーム上でやり取りします。
クライアントは 0 から始まる偶数、サーバーは 1 から始まる奇数を使い、それぞれ 2 ずつ増やします。下位ビットが送信者と合わない、または重複した Request ID を受け取ったら INVALID_REQUEST_ID でセッションを閉じます。
GOAWAY は「もうすぐ閉じるので移ってほしい」という合図です。新しい URI とタイムアウトを含められ、クライアントは裏で新しいセッションを張って購読を移します。リクエストストリーム上で送れば、そのリクエストだけを移すこともできます。
draft-22 の vi64 は先頭の 1 ビットの数で長さが決まり、0xxxxxxx なら 1 バイト(0〜127)、11111111 なら 9 バイトで 64 ビットすべてを表せます。QUIC の varint とは別物なので注意。