QUICのストリームと接続移行
一つの接続で欠損の影響範囲と経路変更の条件を確認します。
説明
QUICはUDP上で暗号化された接続と複数のストリームを提供します。ストリームごとの順序制御、パケット番号、Connection IDを使うため、TCPのバイト列一つとは待ちの範囲や接続の識別が異なります。
ここでは復号後のSTREAMフレームを整理した教材記録をPythonで処理します。一つのストリームに穴があるときの配送、別パケットでの回復、接続IDと経路検証、0-RTTの制約を順に確認します。
1パケット番号とストリーム内の位置
同じ1-RTTパケット番号空間で、受信したフレームをPN・stream ID・offset・dataの順に並べます。PN2に入っていたstream0のoffset4からの4バイトは未着です。
frames=復号後のSTREAM記録[(1,0,0,b"ABCD"),(3,0,8,b"IJKL"),(4,4,0,b"xyz")]; frames番号・ストリーム・位置・データPN1・3・4を受信し、stream0とstream4のデータがあります。パケット番号はパケットの識別、offsetはストリーム内のバイト位置です。stream ID0と4は、ともにクライアント開始の双方向ストリームに使える番号です。
2先頭から連続した部分だけ取り出す
この例のフレームは重複も重なりもないため、offsetをキーにした小さなモデルで連続範囲を求めます。
def contiguous(stream_id,rows):ストリーム別の連続範囲
pieces=offsetとデータの対応{offset:data for pn,sid,offset,data in rows if sid==stream_id}
position=先頭から開始0; result=b""
while position in pieces:次の位置があれば連結
chunk=pieces[position]; result+=chunk; position+=len(chunk)
return result配送できる範囲stream0とstream4を別々に処理します。この関数は重複・重なり・途中からの配送を扱う実用実装ではなく、この固定記録の待ち範囲を確認するためのものです。
3一方が欠けても他方は進む
二つのストリームの連続範囲を求めます。
(contiguous(0,frames)穴のあるstream0,contiguous(4,frames)独立したstream4)stream0はABCDまでで、IJKLは穴が埋まるまで待ちます。一方、stream4のxyzは配送できます。TCPの同じバイト列に複数の応答を載せる場合とは、トランスポート層で順序を待つ範囲が異なります。
4同じデータを新しいパケット番号で届ける
offset4のEFGHがPN5で届いた記録を追加します。
frames.appendPN5で欠けたデータが到着((5,0,4,b"EFGH")); (contiguous回復後のデータとPN(0,frames),[pn for pn,sid,offset,data in frames])stream0がABCDEFGHIJKLへつながります。失われたPN2をそのまま再利用せず、必要な情報を新しいパケットで運びます。QUICのACK範囲が示すパケット番号と、ストリームデータのoffsetは混同しないようにします。
5独立したストリームでも共有する制限
ストリーム順序は別でも、同じ経路の輻輳制御や接続全体の流量制御は共有します。現在のcwnd12000、飛行中12000、接続の残り許容量5000を使います。
cwnd=経路の輻輳窓12000; in_flight=飛行中の量12000; connection_credit=接続の残り許容量5000; max両制限を適用(0,min(cwnd-in_flight,connection_credit))新規送信余裕は0です。別ストリームがあっても混雑による制限を無視して進めることはできません。ストリーム間の順序待ちを減らすことと、損失や輻輳の影響が完全に独立することは違います。
6送信元IPが変わっても接続を識別する
接続k1に相手が発行した二つのConnection IDがあるモデルです。新しい経路で別の有効なIDを使う場合を照合します。
cid_table=有効なIDと接続の対応{"a1":"k1","b2":"k1"}; paths=旧経路と新経路[("192.0.2.10",50000,"a1"),("198.51.100.10",51000,"b2")]; [(ip,port同じ接続への照合,cid_table[cid]) for ip,port,cid in paths]IP・ポート・CIDが変わっても、既に登録されたIDから同じ接続k1へ対応できます。実際のConnection IDは任意の文字列照合だけで認証するものではなく、パケット保護やプロトコル状態も必要です。移行の可否には相手の設定や接続状態も関係します。
7新しい経路を検証する
新経路で送ったPATH_CHALLENGEの8バイトと、返ったPATH_RESPONSEの内容を照合します。
challenge=8バイトの検証値bytes.fromhex("0123456789abcdef"); responses=応答候補[bytes.fromhex("0123456789abcdef"),bytes.fromhex("0123456789abcdee")]; [r==challengeチャレンジとの一致 for r in responses]一致する応答で、そのチャレンジに対応する新経路の到達性を確認します。IDを知っているだけで任意の経路へ無制限に送信するわけではありません。経路変更後にはRTTや輻輳制御の条件も見直す必要があります。
80-RTTで先に送る処理の選択
再開接続の0-RTTは往復を待たずにデータを送れる場合がありますが、リプレイの可能性があります。ここでは副作用なしの参照と、重複実行で課金される処理を分類します。
operations=副作用の条件を明示[("read_public_catalog",False),("charge_without_deduplication",True)]; [(name早期送信の候補を分類,"wait for handshake" if changes_state else "candidate for early data") for name,changes_state in operations]重複を防ぐ仕組みのない課金はハンドシェイク後まで待つ方針にします。公開一覧の参照も、実際にはアプリケーションの性質を確認して採否を決めます。0-RTTは常に受理されるとは限らず、初回接続が無条件に0往復になるという意味でもありません。
まとめ
パケット番号とストリームoffsetは別で、欠けた情報は新しいパケットで回復します。
接続移行には有効な接続IDと経路検証、0-RTTにはリプレイを考慮した処理の選択が必要です。
操作履歴と解説を振り返れます。
Python 3
frames=[(1,0,0,b"ABCD"),(3,0,8,b"IJKL"),(4,4,0,b"xyz")]; frames[(1, 0, 0, b'ABCD'), (3, 0, 8, b'IJKL'), (4, 4, 0, b'xyz')]
def contiguous(stream_id,rows):
pieces={offset:data for pn,sid,offset,data in rows if sid==stream_id}
position=0; result=b""
while position in pieces:
chunk=pieces[position]; result+=chunk; position+=len(chunk)
return result(contiguous(0,frames),contiguous(4,frames))(b'ABCD', b'xyz')
frames.append((5,0,4,b"EFGH")); (contiguous(0,frames),[pn for pn,sid,offset,data in frames])(b'ABCDEFGHIJKL', [1, 3, 4, 5])
cwnd=12000; in_flight=12000; connection_credit=5000; max(0,min(cwnd-in_flight,connection_credit))0
cid_table={"a1":"k1","b2":"k1"}; paths=[("192.0.2.10",50000,"a1"),("198.51.100.10",51000,"b2")]; [(ip,port,cid_table[cid]) for ip,port,cid in paths][('192.0.2.10', 50000, 'k1'), ('198.51.100.10', 51000, 'k1')]challenge=bytes.fromhex("0123456789abcdef"); responses=[bytes.fromhex("0123456789abcdef"),bytes.fromhex("0123456789abcdee")]; [r==challenge for r in responses][True, False]
operations=[("read_public_catalog",False),("charge_without_deduplication",True)]; [(name,"wait for handshake" if changes_state else "candidate for early data") for name,changes_state in operations][('read_public_catalog', 'candidate for early data'), ('charge_without_deduplication', 'wait for handshake')]演習完了