UDP はデータグラムの境界を保持するが、配信、順序、重複排除を保証しない。QUIC は UDP を介して独自の暗号化パケット、確認応答、マルチストリーム機構を伝達し、信頼できるストリームを提供するほか、拡張により信頼性のないデータグラムも伝達できる。これらを選択する際は、UDP を「高速」、QUIC を「常に接続されている」と安易に考えず、メッセージかバイトストリームのどちらが必要か、期限切れの破棄が許容されるか、成功をどう確認するかといったビジネス要件を明確にするべきである。
参照すべき問題 核心的な理解
UDP で一度に送信できるサイズは? UDP 長、IP 長、パス MTU がそれぞれ制約する
QUIC で再送時にパケット番号が変わるのはなぜ? パケット番号は 1 回の送信を識別し、ストリームオフセットはデータの位置を識別する
損失がすべてのストリームをブロックするか? ストリーム内は順序どおりだが、ストリーム間で TCP 的な統一バイトギャップはない
0-RTT は初回接続で常に待たないのか? 状態の回復が必要であり、拒否される可能性やリプレイ攻撃のリスクがある
IP を変更後も接続が維持されるのはなぜ? CID、パス検証、移行ルールが複合的に作用するため
UDP:8 バイトのヘッダの先に利用責任がある
UDP ヘッダは、ソースポート、宛先ポート、Length(長さ)、Checksum(チェックサム)の 4 つの 16 ビットフィールドで構成される。Length には 8 バイトの UDP ヘッダが含まれる。通常のフィールドの最大値は 65535 だが、オプションなしの IPv4 では IP 総長の制約も受けるため、最大 UDP データは 65535−20−8=65507 バイトとなる。このプロトコル上の上限は、一般的なパスで直接伝達可能なサイズよりもはるかに大きいため、アプリケーションがこれほど大きなデータグラムを送信することは推奨されない。RFC 768:UDP
MTU=1500、オプション/拡張ヘッダなしの場合、IPv4 UDP データは最大 1472 バイト、IPv6 は 1452 バイトとなる。トンネリングや QUIC 自体のオーバーヘッドによりさらに削減される。もし 1 つの UDP データグラムが IP 断片化された場合、通常、すべての断片が正常に再構成されて初めてこのデータグラムが配信されるため、1 つでも断片が欠けるとデータグラム全体が失われる可能性がある。
IPv4 UDP のゼロチェックサムフィールドは、チェックサムが提供されていないことを示す。実際の計算結果がゼロの場合、オンラインではすべて 1 を送信する必要がある。IPv6 UDP ではデフォルトでチェックサムが要求され、特定のトンネル例外やジャンボグラムには独立したルールがあるため、例外を一般的なアプリケーションのデフォルトと見なしてはならない。チェックサムは擬似ヘッダと UDP メッセージをカバーするが、送信者の身元認証は行わない。
アプリケーションは、損失、順序入れ替え、重複、サイズ探査、輻輳制御を処理する必要がある。UDP の connect はローカルのデフォルト相手先やフィルタリングなどのソケット動作を設定できるが、TCP 的なリモートハンドシェイクは実行しない。send の成功は、相手先が受信したことを証明するものではない。リアルタイム業務では、期限切れのデータをあえて破棄することもあるが、それでも送信負荷は制御する必要がある。RFC 8085:UDP アプリケーションガイドライン
QUIC の 4 層オブジェクト
UDP データグラム、QUIC パケット、フレーム、アプリケーションストリームを混同しない
1 つの UDP データグラム:8 バイトの UDP ヘッダの後にペイロードが続く
QUIC パケット A:独自のヘッダと保護
CRYPTO フレーム
ハンドシェイクバイト
ACK フレーム
確認済みパケット番号
QUIC パケット B:独自のヘッダと保護
フレームの集合
パケットタイプと暗号化レベルに応じて許可されるフレーム
図はパケットの結合階層を示すが、各パケットに必須のフレームを規定するものではない。ショートヘッダパケットには独立した Length がなく、結合時にはデータグラムの末尾に位置する。
オブジェクト 識別子/境界 機能
UDP データグラム UDP 長さ パス上の 1 つのデータグラム伝達。複数の QUIC パケットをカプセル化できる
QUIC パケット パケット番号など 暗号化保護、確認応答、損失検知の送信単位
フレーム フレームタイプと具体的なフィールド STREAM、ACK、CRYPTO、フロー制御、またはパス検証などの意味
ストリーム ストリーム ID とバイトオフセット アプリケーションが使用できる独立した順序付きバイトストリーム
1 つのストリームが多数のパケットに跨ることもあれば、1 つのパケットが複数のストリームのフレームを運ぶこともある。損失発生後の再送では、回復に必要な情報を別のパケットに詰め直して再分割する必要があるため、古いパケットをそのまま再送するわけではない。
ロングヘッダ、ショートヘッダと可視情報
QUIC v1 では、Initial、Handshake、0-RTT はそれぞれロングヘッダを使用し、1-RTT はショートヘッダを使用する。Retry やバージョン交渉には特殊な構造があり、固定のバイトテーブルで全タイプを覆うことはできない。ロングヘッダには Version、接続 ID 長さなどの情報が含まれる。ショートヘッダの宛先 CID 長さは接続コンテキストに依存するため、単独でキャプチャされた孤立したパケットだけでは完全に解析できない。RFC 9000:パケットフォーマット
パケット番号はオンラインでは必要な下位ビットのみが符号化され、受信側がコンテキストと組み合わせて回復する。一部のヘッダフィールドはヘッダ保護を受けており、保護解除前にそれらを平文のフラグとして直接分析することはできない。v1 ベースラインの QUIC Bit ルールにも後続の交渉拡張が存在するため、そのビットの解釈には交渉結果を組み合わせる必要がある。RFC 9287:QUIC Bit 拡張
Spin Bit はオプションの測定信号であり、エンドポイントが提供しない場合もあり、観察は損失、順序入れ替え、パスの影響を受ける。それは、各エンドポイントが厳密に 1 つのタイマーに従って「RTT ごとに 1 回反転する」汎用クロックではなく、アプリケーションのリクエスト遅延も明らかにしない。
再送するのは情報か、それとも元のパケットか?
まずパケット 11 のみを配信し、ストリーム 4 には配信可能なデータがあるが、ストリーム 0 は空のままである。次にパケット 12 を送り、遅延していたパケット 10 を最後に補送する:パケット確認セットは増加するが、一意のストリームデータ量は増加しない。この 2 つの番号空間を統合して理解してはならない。
図を準備しています 再読み込み
再送するのは情報か、それとも元のパケットか? 同じストリーム範囲は、新しい番号のパケットに詰められる可能性がある。受信側の重複排除はストリームオフセットで行われ、パケットの確認はパケット番号で行われる。1 つのストリームのギャップは、別のストリームの配信を妨げない。
{"id":"quic-packet-stream-identity","title":"再送するのは情報か、それとも元のパケットか?","summary":"同じストリーム範囲は、新しい番号のパケットに詰められる可能性がある。受信側の重複排除はストリームオフセットで行われ、パケットの確認はパケット番号で行われる。1 つのストリームのギャップは、別のストリームの配信を妨げない。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"actions\"><button id=\"p10\" data-i18n=\"p10\"></button><button id=\"p11\" data-i18n=\"p11\"></button><button id=\"p12\" data-i18n=\"p12\"></button><button id=\"reset\" data-i18n=\"reset\"></button></div><section class=\"panel\"><h3 data-i18n=\"streams\"></h3><div id=\"streams\"></div></section><div class=\"metrics\"><div><span data-i18n=\"acked\"></span><strong id=\"acked\"></strong></div><div><span data-i18n=\"unique\"></span><strong id=\"unique\"></strong></div></div><p id=\"mapping\" class=\"message\">PN10 → S0 [0,100)\nPN11 → S4 [0,100)\nPN12 → S0 [0,100)</p><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px var(--viz-font-ui);padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 var(--viz-font-ui);overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n#streams{display:flex;gap:8px}.metrics strong{font-size:18px}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}let got=new Set;function draw(){const a=got.has(10)||got.has(12),b=got.has(11);cells('streams',[{text:'S0 [0,'+(a?100:0)+')',cls:a?'on':'empty'},{text:'S4 [0,'+(b?100:0)+')',cls:b?'on':'empty'}]);set('acked',fmt(got));set('unique',100*(a+b));$('#unique').dataset.value=100*(a+b);}for(let n of [10,11,12])$('#p'+n).onclick=()=>{got.add(n);draw()};$('#reset').onclick=()=>{got=new Set;draw()};draw();","audio":false,"strings":{"acked":"受信済みパケット番号","heading":"再送するのは情報か、それとも元のパケットか?","intro":"2 つのストリームのデータを送信し、元のパケットまたは再送パケットを任意に配信する。ストリームオフセットとパケット番号は別に表示する。","p10":"パケット 10 を配信","p11":"パケット 11 を配信","p12":"再送パケット 12 を配信","reset":"リセット","scope":"1-RTT STREAM データのみ。パケット 10/12 はどちらもストリーム 0 の [0,100) を運び、パケット 11 はストリーム 4 の [0,100) を運ぶ。輻輳、鍵、フロー制御、アプリケーションプロトコルの依存関係をシミュレートしない。","scopeLabel":"モデルの範囲と規約","streams":"2 つの独立したストリームプレフィックス","unique":"一意のストリームデータ(B)"}}
ハンドシェイクと暗号化レベル
QUIC は TLS 1.3 のハンドシェイクメッセージと鍵交渉を使用するが、TLS レコードを伝送ストリームにそのまま適用するものではない。CRYPTO フレームは、対応する暗号化レベルでハンドシェイクバイトを伝達する。Initial、Handshake、0-RTT、1-RTT の保護と使用条件は異なる。Initial 鍵は公開情報から派生できるため、Initial パケットの保護は、ネットワーク観察者に対してすでに機密である、またはサーバー認証が完了したことを意味しない。RFC 9001:TLS と QUIC
初回のハンドシェイクでは、往復 1 回で対応するアプリケーション通信を開始できる場合が多いが、Retry、証明書サイズ、損失、スケジューリングにより実測時間が変化する。サーバーはクライアントのアドレスを検証する前に増幅攻撃防止の制限を受け、通常、受信したバイト数の 3 倍までしか送信できない。これにより、ハンドシェイクデータを一度に送信し尽くすことが制限される可能性がある。
クライアントが Initial を運ぶ UDP データグラムは、少なくとも 1200 バイトに拡張する必要がある。これは UDP ペイロード を指しており、UDP/IP を加えてちょうど 1200 バイトになることを意味するものではない。オプションなしの IPv6 では、対応する IP パケットは少なくとも 1248 バイトとなり、実際のパスも考慮する必要がある。RFC 9000:データグラムサイズとアドレス検証
0-RTT:早期送信は、早期の完全な保証の獲得ではない
接続を再開する際、クライアントは保存された TLS 回復状態と、沿用が許可された伝送パラメータに基づいて早期データを送信できる。初回アクセスでは、これらの条件を凭空に持つことはできない。サーバーは早期データを拒否する可能性があり、TLS の受領信号が結果を決定する。QUIC にはこの目的のための REJECTED フレームはない。拒否された場合、クライアントはプロトコルとアプリケーションインターフェースに従って元の早期データを処理する必要があり、拒否をビジネスがすでに実行されたものと見なしてはならない。RFC 9001:0-RTT
0-RTT にはリプレイ攻撃のリスクがあり、アプリケーションは操作のセマンティクスとデプロイメントにおけるリプレイ防止を判断する必要がある。HTTP のメソッド名だけでは不十分である。誤実装された GET でも副作用が発生する可能性がある。AEAD の鍵使用上限は暗号学的リスクを制限するが、ビジネスの重複排除や攻撃者のリプレイ回数の制限を提供するものではない。
ストリーム番号とフロー制御:制限は累積上限
ストリーム ID の最下位ビットは開始エンドポイント(0: クライアント、1: サーバー)を表し、下から 2 番目のビットは方向(0: 双方向、1: 単方向)を表す。残りの上位ビットはこのカテゴリのシーケンス番号である。したがって、各カテゴリの ID は 4 ずつ増加する。
カテゴリ 最初の ID
クライアント開始双方向 0、4、8、12
サーバー開始双方向 1、5、9、13
クライアント開始単方向 2、6、10、14
サーバー開始単方向 3、7、11、15
双方向ストリームの 2 つの送信方向には、それぞれ独自のバイトオフセットがある。MAX_STREAM_DATA は対応するストリーム方向の累積送信範囲を制約し、MAX_DATA は接続範囲内の対応するストリームデータの累積使用を制約し、MAX_STREAMS は開けることができるストリームの数を制限する。これらは交換可能な「バッファサイズ」3 つではない。RFC 9000:ストリームとフロー制御
例えば、接続の制限が 10000 バイトで、2 つのストリームがそれぞれ 4000 バイトと 3000 バイトを使用している場合、接続にはまだ 3000 バイトの余裕がある。もし対象ストリーム自体に 1000 バイトの余裕しかない場合、新しいデータは最大でもこの 1000 バイトによって制限される。既存のバイトの再送により、新しいフロー制御クォータが重複して消費されることはない。RESET_STREAM の final size も関連する帳簿計算に参加するため、上限を回避するためにリセットを繰り返すことはできない。
ストリーム間の独立した配信:ネットワークリソースを共有しながら、それぞれに順序付きのギャップがある
ストリーム 0
ストリーム 4
0~999 到着済み
1000~1999 欠落
2000~2999 到着済み
0~999 到着済み
1000~1999 到着済み
2000~2999 到着済み
ストリーム 4 は連続して配信可能。ストリーム 0 の後半部は埋め合わせを待っている
同じ損失パケットが 2 つのストリームのデータを同時に運んでいる場合、両方のストリームに影響が出る。接続の輻輳ウィンドウと接続フロー制御は共有されたままとなる。
独立したストリームは、TCP の単一順序付きバイトストリームに起因するストリーム間の伝送層待機を解消したが、同じストリーム内ではギャップの埋め合わせが必要である。複数のストリームは輻輳制御、接続フロー制御、CPU、パスを共有する。HTTP/3 ではアプリケーションレベルの依存関係もあるため、「QUIC にはヘッドオブブロックが全くない」という記述は依然として正確ではない。
なぜ再送はフロー制御クォータを 1 つ占有しないのか?
接続の総量と対象ストリームを別々に描画して初めて、ボトルネックがどこにあるかを見ることができる。オフセットを維持したまま再送を繰り返すと、線速での重複バイトは増加するが、フロー制御の残高は変わらない。これは無料の帯域幅ではなく、再送も輻輳制御などの他の制約を受ける。
cre d i t 0 = min ( MS D 0 − H 0 , M D − i ∑ H i )
図を準備しています 再読み込み
なぜ再送はフロー制御クォータを 1 つ占有しないのか? クォータは各ストリームで消費された最大オフセットに基づいて累積され、重複して送信された線速バイトに基づいて累積されるものではない。新しいデータは接続とストリームの上限の両方によって制約され、既存の範囲の再送はオフセットを増加させない。
{"id":"quic-cumulative-credit","title":"なぜ再送はフロー制御クォータを 1 つ占有しないのか?","summary":"クォータは各ストリームで消費された最大オフセットに基づいて累積され、重複して送信された線速バイトに基づいて累積されるものではない。新しいデータは接続とストリームの上限の両方によって制約され、既存の範囲の再送はオフセットを増加させない。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"control\"><label for=\"s0\" data-i18n=\"s0\"></label><output for=\"s0\" id=\"s0-value\"></output><input id=\"s0\" type=\"range\" min=\"100\" max=\"5000\" value=\"4000\" step=\"100\"></div><div class=\"control\"><label for=\"s4\" data-i18n=\"s4\"></label><output for=\"s4\" id=\"s4-value\"></output><input id=\"s4\" type=\"range\" min=\"0\" max=\"5000\" value=\"3000\" step=\"100\"></div><div class=\"actions\"><button id=\"retry\" data-i18n=\"retry\"></button><button id=\"reset\" data-i18n=\"reset\"></button></div><div id=\"used\" class=\"intervals\"></div><div class=\"metrics\"><div><span data-i18n=\"remaining\"></span><strong id=\"remaining\"></strong></div><div><span data-i18n=\"repeated\"></span><strong id=\"repeated\"></strong></div></div><p id=\"equation\" class=\"formula\"></p><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px var(--viz-font-ui);padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 var(--viz-font-ui);overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}let repeated=0;function draw(){const a=+$('#s0').value,b=+$('#s4').value,r=Math.min(5000-a,10000-a-b);set('s0-value',a);set('s4-value',b);intervals('used',[{start:0,end:a,label:'S0: '+a+' / 5000'},{start:0,end:a+b,label:'connection: '+(a+b)+' / 10000',cls:'gold'}],10000);set('remaining',r);set('repeated',repeated);set('equation','credit = min(5000 − '+a+', 10000 − '+a+' − '+b+') = '+r);$('#remaining').dataset.value=r;}$('#retry').onclick=()=>{repeated+=100;draw()};$('#reset').onclick=()=>{repeated=0;draw()};document.querySelectorAll('input').forEach(e=>e.oninput=()=>{repeated=0;draw()});draw();","audio":false,"strings":{"heading":"なぜ再送はフロー制御クォータを 1 つ占有しないのか?","intro":"2 つのストリームの最高送信オフセットと上限を変更し、接続と対象ストリームが残したクォータを確認する。","remaining":"ストリーム 0 で送信可能な新しいデータ(B)","repeated":"重複送信 / B","reset":"リセット","retry":"ストリーム 0 の先頭 100 B を再送","s0":"ストリーム 0 の消費済みオフセット(B)","s4":"ストリーム 4 の消費済みオフセット(B)","scope":"2 つの通常の信頼できるストリーム。RESET_STREAM やホール送信なし。MAX_DATA10000、対象ストリーム MAX_STREAM_DATA5000。ここではフロー制御のみを計算し、輻輳や増幅攻撃防止の許可を意味するものではない。","scopeLabel":"モデルの範囲と規約"}}
ACK と損失検知:確認応答は読み取りではない
Initial、Handshake、Application Data は 3 つのパケット番号空間であり、0-RTT と 1-RTT はアプリケーションデータ空間を共有する。パケット番号は各自の空間で増加し、再利用されない。ストリームバイトを再パケット化して送信する際には、新しいパケット番号が使用される。ACK は受信済みパケット番号範囲を報告するが、物理的に確認済みで失われたパケットのリストを直接列挙するものではない。
パケット 10 が確認されていないが、より大きなパケットが確認されている場合、それは損失した可能性があるか、単に順序入れ替えしただけの可能性がある。RFC 9002 は、パケット閾値と時間閾値などを使用して推論する。推奨されるパケット閾値は 3 である。時間の参照は 9/8×max(最新_rtt,平滑_rtt) であり、クロック粒度などの条件を考慮し、新しいサンプルと平滑化推定が共同で判定を制約する。判断には、より遅く送信されたパケットの確認など、コンテキストも必要である。RFC 9002:損失検知
PTO(プローブタイムアウト)の期限が到来すると、プローブが手配され、未確認パケットがすべて失われたと直接宣言されるわけではない。アプリケーションデータ空間の簡易推定を例にとると、SRTT=40 ms、RTTVAR=5 ms、max_ack_delay=25 ms、粒度 1 ms の場合、PTO の基本値は 40+max(20,1)+25=85 ms となる。ハンドシェイク空間では同様の ACK 遅延をそのまま適用せず、連続する PTO にはバックオフがある。
輻輳制御は送信負荷を決定し、回復メカニズムはどの情報を補完するかを決定する。RFC 9002 は NewReno に基づく例示の輻輳アルゴリズムを提供しているが、QUIC はすべての実装がそれを使用することを要求しない。CUBIC、BBR などは、プロトコルの制約に準拠する限り、実装によって採用できる。
CID と移行:新しいパスでも検証が必要
受信側は、自身が発行した Connection ID を使用して、新しいアドレスからのパケットを接続に関連付けることができる。CID は永遠不変の 1 ペアの番号ではない。エンドポイントはルールに従って複数の CID を発行、使用、退役でき、ショートヘッダがソース/宛先の両方の CID を同時に運ぶわけではない。ゼロ長さ CID などのデプロイメントの選択は、移行能力を制限する。
クライアントがネットワークを切り替えた後、新しいパス上の保護済みパケットでも、接続認証とパス検証を経る必要がある。PATH_CHALLENGE のランダムデータは PATH_RESPONSE によってエコーされ、帰路の到達可能性を確認するのに役立つ。NAT ポート再バインディングと能動的移行は異なる背景を持つため、能動的移行を禁止するパラメータを、すべてのアドレス変化を拒否するものと単純に理解してはならない。RFC 9000:移行と検証
パスには新しい RTT、容量、MTU がある可能性があり、関連する制御状態はルールに従って処理する必要があり、ビジネスは一時停止または失敗する可能性がある。CID が一致するだけでは、「アプリケーションが中断を全く見えない」と約束するものではない。新しいパスが UDP をサポートしていない、鍵状態が失われた、検証がタイムアウトしたなど、接続が続行できない理由があり得る。
信頼できる STREAM と信頼性のない DATAGRAM
QUIC DATAGRAM 拡張は、アプリケーションデータグラムの境界を保持し、損失データに対して STREAM 的な再送を行わず、STREAM フロー制御も使用しない。接続の輻輳制御や交渉されたサイズなどの制限は依然として適用される。それを含むパケットの ACK を受信しても、アプリケーションがそのメッセージを処理したことを意味しない。リアルタイムデータはこれに基づいて期限切れ破棄のセマンティクスを選択できるが、QUIC 暗号化が含まれていれば確実に信頼できる配信が得られるとは限らない。RFC 9221
HTTP/3、DoQ などは QUIC に対してそれぞれアプリケーションマッピングを定義している。実際の選択では、UDP の到達可能性、実装の成熟度、観測可能性、リソース消費も考慮する必要がある。ユーザー空間の実装は進化的に容易であるが、それが自動的にカーネル TCP よりも CPU を節約するわけではない。
ローカル再計算:番号、フロー制御、プロービング予算
from math import isclose
def stream_id ( index , server = False , unidirectional = False ) :
return ( index < < 2 ) | int ( server ) | ( int ( unidirectional ) < < 1 )
assert [ stream_id ( i ) for i in range ( 4 ) ] == [ 0 , 4 , 8 , 12 ]
assert [ stream_id ( i , True , True ) for i in range ( 4 ) ] == [ 3 , 7 , 11 , 15 ]
assert 65535 - 20 - 8 == 65507
assert 1200 + 8 + 40 == 1248
assert min ( 10000 - 4000 - 3000 , 1000 ) == 1000
assert 1200 * 3 == 3600
srtt ,latest ,rttvar ,ack_delay ,granularity = . 040 ,. 050 ,. 005 ,. 025 ,. 001
loss_delay = max ( granularity , 9 / 8 * max ( srtt , latest ) )
pto = srtt + max ( 4 * rttvar , granularity ) + ack_delay
assert isclose ( loss_delay , . 05625)
assert isclose ( pto , . 085)
sent = { 10 : ( 0 , 1000 , 500 ) , 14 : ( 0 , 1000 , 500 ) }
assert sent [ 10 ] == sent [ 14 ] and 10 != 14
print ( ' UDP/Initial サイズ、ストリーム ID、クォータ、閾値、PTO 計算例が通過' )
これらのアサーションは、局所的なフィールドと予算を検証するが、完全なパケット保護、ハンドシェイク、または損失検知ステートマシンは実装しない。実際の QUIC 接続やネットワーク移行は開始されていない。
継続的な参照