HTTP/2は、HTTPのメソッド、ステータス、キャッシュなどのセマンティクスを維持し、フレームとストリームを用いて1つの接続内で複数の交換を表現します。異なるリクエストのレスポンスをインターリーブ(交差)して送信できるため、HTTP/1.1の「レスポンスはリクエストの順序に従わなければならない」という制限を解決します。基盤となるTCPは依然として単一の順序付きバイトストリームを提供します。
現行の基礎仕様はRFC 9113であり、RFC 7540に取って代わります。これをTCPを書き換えるトランスポートプロトコルと記述してはいけません。「1つの接続で複数のストリーム」という概念は、サイト全体で永久に1つの接続しか開かないことを意味するものではありません。RFC 9113
問題の特定 最初に確認すること
h2がネゴシエートされたのに、GETのテキストが見当たらない リクエストヘッダーはフィールドブロックとHPACKを使用している
特定のレスポンスが遅い場合、他のストリームはそれを待たなければならないか ストリームのスケジューリング、接続ウィンドウ、TCPのギャップはそれぞれ別に見る
END_HEADERSはリクエストの終了を意味するか フィールドブロックの終了と送信方向の終了は異なる
ウィンドウが大きいのに、送信が停止しているのはなぜか ストリームウィンドウと接続ウィンドウの両方を同時に確認する
圧縮テーブルが時々デコードできないのはなぜか 双方向にそれぞれ状態があり、パケットキャプチャでは履歴が欠けている可能性がある
GOAWAY後にどのリクエストを再試行できるか Last-Stream-IDとビジネスロジックの実行不確実性
接続の開始とフィールドの表現
HTTPSでは、TLS ALPNを用いてh2をネゴシエーションするのが一般的です。その後、クライアントは規定の接続プレフィックスとSETTINGSを送信し、サーバーもSETTINGSで接続プレフィックスを開始します。クライアントの24バイトの固定プレフィックスにはHTTPのテキストが含まれているように見えますが、アプリケーションに対して通常のHTTP/1.1リクエストを1回送信するものではありません。
TLSを使用しないHTTP/2は、事前の知識に基づいて起動できます。歴史的なh2c Upgradeは、すべてのHTTP/2の必須エントリポイントとして扱うべきではありません。最新の基礎仕様では、このアップグレードメカニズムは廃止されています。
通常のリクエストは、:method、:scheme、:authority、:pathなどの擬似フィールドでターゲットを表現します。レスポンスでは:statusを使用します。擬似フィールドは通常のフィールドの前に配置する必要があり、フィールド名は小文字を使用します。CONNECTなどの特定のメソッドには対応するルールがあります。HTTP/1.1のステータスライン全体をHEADERSに詰め込むことはできません。
Connection、Keep-Alive、Transfer-Encodingなどの接続専用フィールドはそのまま持ち越せません。TEにはtrailersのみを許可するという例外があります。Content-Lengthは期待されるコンテンツの長さを表現するために使用できますが、ストリームはHTTP/2のフレームとEND_STREAMで区切られるため、CL(Content-Length)を使ってストリームの状態を判断することはできません。
フレームヘッダー9バイト:長さは完全なフレーム長ではない
フィールド 長さ 意味
Length 24 bit ペイロードのバイト数のみ
Type 8 bit DATA、HEADERS、SETTINGSなど
Flags 8 bit フレームタイプに応じて解釈
Reserved + Stream ID 1 + 31 bit 予約ビットと所属ストリーム。0は接続制御に使用
受信側のデフォルトの最大フレームペイロードは16,384バイトです。SETTINGS_MAX_FRAME_SIZEによって規定の範囲内で調整可能で、最大16,777,215バイトまで拡張できます。この上限は、1つのレスポンスまたは1つのフィールドリストの総長ではありません。
以下に、ストリーム1上で5バイトの内容を含むDATAフレームを構築し、この送信方向を終了させます。完全なフレームは14バイトです。コードはフレームヘッダーと長さのみを担当し、接続状態マシンは実装しません。
def frame ( kind , flags , stream , payload ) :
assert 0 <= stream < 2 * * 31 and len ( payload ) < 2 * * 24
return ( len ( payload ) . to_bytes ( 3 , ' big' ) + bytes ( [ kind , flags ] )
+ stream . to_bytes ( 4 , ' big' ) + payload )
wire = frame ( 0 , 1 , 1 , b ' hello' )
assert wire . hex ( ) == ' 00000500010000000168656c6c6f'
assert len ( wire ) == 14
assert int . from_bytes ( wire [ : 3 ] , ' big' ) == len ( wire ) - 9
assert int . from_bytes ( wire [ 5 : 9 ] , ' big' ) & 0x 7fffffff == 1
print ( ' DATAペイロード / 完全なフレーム:' , len ( wire ) - 9 , len ( wire ) )
ストリーム状態と2つのENDフラグ
クライアントが作成したリクエストストリームは奇数IDを使用します。サーバーが開始するプッシュストリームは偶数IDを使用します。任意の双方向RPCで偶数のリクエストストリームを適当に開くことはできません。IDは接続内で単調に使用され、完了後に回収されて再利用されません。ストリーム0はビジネスリクエストストリームではありません。
END_HEADERSは、現在の圧縮フィールドブロックが完全であることを示し、END_STREAMはこの送信方向の終了を示します。GETリクエストヘッダーのみがHEADERSでこれら両方を同時に設定できますが、サーバーは同じストリームでレスポンスを送信し続けることができます。コンテンツを含むPOSTでは、まずHEADERSを送信し、次に複数のDATAを送信し、最後にDATAまたはtrailer HEADERSで終了する可能性があります。
フィールドブロックがCONTINUATIONに分割された場合、END_HEADERSに至るまで、そのストリームに対応するCONTINUATIONを連続して送信しなければなりません。他のストリームのDATAを挿入することはできません。マルチプレクシングにもプロトコルレベルでの連続領域があるため、すべてのフレームが任意に割り込めるわけではありません。
マルチプレクシング:アプリケーションのキューとTCPのヘッドオブブロックは別々に考える
フレームはインターリーブ可能だが、TCPのギャップ後のバイトは依然として待機が必要
S1 HEADERS
Stream IDでディスパッチ
S3 HEADERS
Stream IDでディスパッチ
S3 DATA
Stream IDでディスパッチ
S1 DATA
Stream IDでディスパッチ
欠落バイト
他のストリームに属するフレームでも、TCPのギャップをスキップすることはできない
図中のストリーム順序は例示です。HEADERSの未完了フィールドブロックには、連続するCONTINUATIONの要件があります。
ストリーム1のクエリがレスポンスの準備ができていない場合、ストリーム3は自分のレスポンスを先に送信できます。しかし、送信されたすべてのフレームは最終的に同じTCPシーケンス番号空間に収まります。TCPのバイトが失われると、受信側はギャップ後のバイトを、それらが別のHTTPストリームに属していても、連続ストリームとしてHTTP/2に渡すことはできません。
これは、パケットが1つ失われると、すでに配信されたすべてのストリームのデータが取り消されることを意味するものではありません。また、フレームが天然に対応するTCPパケットがあることを意味するものではありません。接続レベルの帯域幅、輻輳制御、サーバーのスケジューリング、アプリケーションのロック競合、HPACKの状態も、ストリームをまたぐ影響を引き起こす可能性があります。
二層のフロー制御:DATAのみを対象とし、輻輳制御ではない
DATAは接続ウィンドウと自身のストリームウィンドウの両方を消費する
接続送信余力:10,000 B
すべてのストリームがこの方向の割り当てを共有
ストリーム1:余力 8,000 B
まず6,000 BのDATAを送信
ストリーム3:余力 8,000 B
その後、最大4,000 BのDATAを送信可能
例ではパディングなし、フレーム長上限は十分。ストリームウィンドウにまだ余裕があっても、接続ウィンドウによってブロックされる可能性がある。
送信方向に対して、DATAは接続とストリームの両方の割り当てによって制限されます。デフォルトの初期割り当ては65,535バイトです。SETTINGS_INITIAL_WINDOW_SIZEはストリームウィンドウを変更しますが、これを使って接続ウィンドウを大きくすることはできません。接続の割り当ては、ストリーム0のWINDOW_UPDATEによって調整されます。逆方向の送信には独自の割り当てがあります。
DATAのペイロード全体はフロー制御にカウントされます。これにはパディングとその長さフィールドが含まれますが、9バイトのフレームヘッダーは含まれません。HEADERS、PING、SETTINGSなどはDATAウィンドウを消費しません。したがって、DATAのみを制限しても、相手のフィールドデコードや制御フレームのリソース消費を制限するには不十分です。
connection = 10_000
stream = { 1 : 8_000 , 3 : 8_000 }
def send ( sid , size ) :
global connection
assert 0 <= size <= min ( connection , stream [ sid ] )
connection -= size
stream [ sid ] -= size
send ( 1 , 6_000 )
assert connection == 4_000 and stream [ 1 ] == 2_000
send ( 3 , 4_000 )
assert connection == 0 and stream [ 3 ] == 4_000
stream [ 3 ] += 10_000
assert min ( connection , stream [ 3 ] ) == 0
connection += 2_000
assert min ( connection , stream [ 3 ] ) == 2_000
print ( ' 接続ウィンドウ更新後、ストリーム3で送信可能:' , min ( connection , stream [ 3 ] ) )
WINDOW_UPDATEの増分は0であってはならず、ウィンドウにも上限があります。SETTINGS_INITIAL_WINDOW_SIZEを下げると、既存のストリームウィンドウが一時的に負の値になる可能性があります。この場合、割り当てが回復するのを待つ必要があり、負の数を強制的に符号なしの大きな数に変換してはいけません。アプリケーションがタイムリーに消費して受信バッファを解放することで、フロー制御の停止を防ぐことができます。
1つのストリームに割り当てを追加したのに、なぜまだ送信できないのか?
接続の割り当てを使い果たした後、ストリーム3のWINDOW_UPDATEのみをクリックします。金色のバーが伸びますが、送信ボタンはまだ使用できません。接続の割り当てをさらに追加して初めて、送信が再開されます。この状態は、2つの「残量の最小値」の制約が過去の操作によってどのように形成されるかを示しています。
図を準備しています 再読み込み
1つのストリームに割り当てを追加したのに、なぜまだ送信できないのか? 各DATAは2つの勘定を同時に引く。ストリーム3には残高があるが、接続が0になると停止する。HEADERSはこの2つのDATA勘定には含まれないが、他のリソース制約の影響を受ける。
{"id":"http2-shared-window","title":"1つのストリームに割り当てを追加したのに、なぜまだ送信できないのか?","summary":"各DATAは2つの勘定を同時に引く。ストリーム3には残高があるが、接続が0になると停止する。HEADERSはこの2つのDATA勘定には含まれないが、他のリソース制約の影響を受ける。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"actions\"><button id=\"s1\" data-i18n=\"s1\"></button><button id=\"s3\" data-i18n=\"s3\"></button><button id=\"w1\" data-i18n=\"w1\"></button><button id=\"w3\" data-i18n=\"w3\"></button><button id=\"wc\" data-i18n=\"wc\"></button><button id=\"reset\" data-i18n=\"reset\"></button></div><section class=\"panel\"><h3 data-i18n=\"windows\"></h3><div id=\"windows\"></div></section><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 c=10000,s={1:8000,3:8000};function draw(){intervals('windows',[{start:0,end:c,label:'connection '+c},...[1,3].map(i=>({start:0,end:s[i],label:'stream'+i+' '+s[i],cls:'gold'}))],20000);set('equation','stream1 credit = min('+c+','+s[1]+')\\nstream3 credit = min('+c+','+s[3]+')');[1,3].forEach(i=>{$('#s'+i).disabled=Math.min(c,s[i])<1000;$('#w'+i).disabled=s[i]>=20000});$('#wc').disabled=c>=20000;$('#windows').dataset.snapshot=JSON.stringify({c,s})}[1,3].forEach(i=>{$('#s'+i).onclick=()=>{c-=1000;s[i]-=1000;draw()};$('#w'+i).onclick=()=>{s[i]+=1000;draw()}});$('#wc').onclick=()=>{c+=1000;draw()};$('#reset').onclick=()=>{c=10000;s={1:8000,3:8000};draw()};draw();","audio":false,"strings":{"heading":"1つのストリームに割り当てを追加したのに、なぜまだ送信できないのか?","intro":"任意のストリームから1000 BのDATAを送信するか、ストリームと接続のウィンドウを個別に補充する。","reset":"リセット","s1":"ストリーム1がDATAを送信","s3":"ストリーム3がDATAを送信","scope":"単一方向。2つのストリームの初期値はそれぞれ8000 B、接続は10000 B。パディングなし、固定1000 B、更新上限は20000の教育用値。TCPとSETTINGSによるウィンドウ縮小はシミュレートしない。","scopeLabel":"モデルの範囲と規約","w1":"ストリーム1のWINDOW_UPDATE","w3":"ストリーム3のWINDOW_UPDATE","wc":"接続のWINDOW_UPDATE","windows":"残りのDATA割り当て(B)"}}
HPACK:双方向でそれぞれ圧縮状態を維持する
HPACKは、61項目の静的テーブル、動的テーブル、およびオプションの固定Huffman符号化を使用します。リクエスト方向のエンコーダーとサーバー側のデコーダーは、対応する状態を共有しています。レスポンス方向は別の状態です。サーバーがレスポンスフィールドを格納したからといって、クライアントが次のリクエストでそれを参照できるわけではありません。
動的テーブルはエンコーディングコマンドに基づいてエントリを挿入し、後から挿入された項目が前に位置し、古い項目は容量に基づいて淘汰されます。「各HEADERSの終了後にすべてのフィールドが自動的に保存される」わけではなく、古い項目へのアクセスがLRUとして昇格されることもありません。エントリの課金は、名前バイト数 + 値バイト数 + 32で、圧縮後の線長とは異なります。
例えば、:authority: www.example.comは10+15+32=57バイトのテーブル容量を占めます。最初の動的インデックスは62です。次に新しい項目を挿入すると、古い項目のインデックスは63になります。静的インデックス2は:method: GETに対応し、1バイトのindexed表現は0x82です。インデックス62は0xbeですが、方向と履歴の状態が一致している場合にのみ、それが何を意味するかがわかります。
Never Indexedは、機密フィールドのエンコーディング制約に使用されます。フィールドを暗号化するものではなく、総長を隠すものでもなく、すべての圧縮側チャネルを排除するものでもありません。フィールドテーブルの容量制限と、デコード後のフィールドリストのサイズ制限は、異なるリソース制御です。RFC 7541:HPACK
インデックス62がなぜ次は別のフィールドを指すのか?
2つの項目を連続して挿入した後、62は後から挿入された方を指し、以前のフィールドは63になります。62を参照してもテーブルの順序に変化はありません。容量を縮小してスライダーを戻しても、すでに淘汰された項目は復活しません。容量制限と実際の履歴状態は、2つの独立した変数だからです。
図を準備しています 再読み込み
インデックス62がなぜ次は別のフィールドを指すのか? インデックスは、現在の方向の圧縮状態における位置であり、フィールドの永続的な番号ではありません。挿入により古いインデックスが後方に移動し、容量はname+value+32で課金され、読み取りはLRU昇格を実行しません。
{"id":"hpack-index-history","title":"インデックス62がなぜ次は別のフィールドを指すのか?","summary":"インデックスは、現在の方向の圧縮状態における位置であり、フィールドの永続的な番号ではありません。挿入により古いインデックスが後方に移動し、容量はname+value+32で課金され、読み取りはLRU昇格を実行しません。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"control\"><label for=\"capacity\" data-i18n=\"capacity\"></label><output for=\"capacity\" id=\"capacity-value\"></output><input id=\"capacity\" type=\"range\" min=\"0\" max=\"180\" value=\"120\" step=\"10\"></div><div class=\"actions\"><button id=\"a\" data-i18n=\"a\"></button><button id=\"b\" data-i18n=\"b\"></button><button id=\"ref\" data-i18n=\"ref\"></button><button id=\"reset\" data-i18n=\"reset\"></button></div><section class=\"panel\"><h3 data-i18n=\"entries\"></h3><div id=\"entries\"></div></section><section class=\"panel\"><h3 data-i18n=\"last\"></h3><div id=\"last\"></div></section><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#entries .tr{grid-template-columns:.6fr 1.7fr .6fr}.tr>*{min-height:5em}#last{min-height:3.6em;overflow-wrap:anywhere;font:13px/1.8 var(--viz-font-ui)}","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 entries=[],last='∅';const size=e=>e[0].length+e[1].length+32;function evict(){while(entries.reduce((s,e)=>s+size(e),0)>+$('#capacity').value)entries.pop()}function draw(){set('capacity-value',$('#capacity').value);const rows=[['index','field','B']];for(let i=0;i<5;i++)rows.push(entries[i]?[62+i,entries[i].join(': '),size(entries[i])]:['—','—','—']);table('entries',rows);set('last',last);$('#ref').disabled=!entries.length;$('#entries').dataset.values=JSON.stringify(entries);}function insert(e){entries.unshift(e);evict();draw()}$('#a').onclick=()=>insert([':authority','www.example.com']);$('#b').onclick=()=>insert(['x-id','7']);$('#ref').onclick=()=>{last='62 → '+entries[0].join(': ');draw()};$('#capacity').oninput=()=>{evict();draw()};$('#reset').onclick=()=>{entries=[];last='∅';draw()};draw();","audio":false,"strings":{"a":":authorityを挿入","b":"x-idを挿入","capacity":"動的テーブル容量(B)","entries":"現在の動的テーブル","heading":"インデックス62がなぜ次は別のフィールドを指すのか?","intro":"フィールドを挿入し、現在のインデックス62を参照し、その後テーブル容量を縮小する。参照してもエントリが最前面に移動することはない。","last":"最後の参照結果","ref":"インデックス62を参照","reset":"リセット","scope":"固定された2つのASCIIフィールド(authorityとx-id)を使用し、HPACK動的テーブルの順序と淘汰をシミュレートする。Huffman符号化や完全なフィールドブロックはエンコードしない。容量縮小による淘汰は永続的であり、拡大しても回復しない。","scopeLabel":"モデルの範囲と規約"}}
SETTINGS、優先度、リソースの境界
SETTINGSは、送信側が受け入れたい制約を記述するもので、相手側はこれに基づいて自分の送信を調整します。例えば、クライアントが最大フレーム長を宣言すると、サーバーがクライアントに送信するフレームが制限されます。これを逆に解釈して、クライアント自身の送信許可と見なしてはいけません。ACK付きのSETTINGSは設定項目を含まない。
MAX_CONCURRENT_STREAMSは、対応する方向で並列化できるストリームの数を制限するものであり、接続の履歴における累積リクエスト数ではありません。瞬時の並列数だけを制御しても、急速なストリームの作成/取消しによる作業負荷に耐えられない可能性があるため、リクエストのコスト、キュー、フィールドサイズ、取消しの伝播にも注目する必要があります。
古い優先度の依存ツリーはRFC 9113で廃止されました。RFC 9218は拡張可能な優先度を定義しており、urgencyは0から7の値を持ち、数値が小さいほど緊急度が高く、incrementalフラグは漸進的な配信に適しているかどうかを示します。これらはスケジューリングのヒントであり、固定されたスループットシェアを保証するものではなく、フロー制御ウィンドウを突破することもできません。RFC 9218
取消しと優雅な終了:RSTをトランザクションのロールバックと見なさない
RST_STREAMはストリームを終了しますが、アプリケーションがリクエストを実行していないことを証明するものではなく、データベーストランザクションを自動的にロールバックするものでもありません。実装は、接続に関連する圧縮とフロー制御の状態を維持し続ける必要があり、特定のストリームが取消されたからといって、関連するすべてのフレームを無視することはできません。
GOAWAYはLast-Stream-IDを用いて、相手側にどのストリームがすでに処理された可能性があるかを通知します。サーバーが最後に受け入れたクライアントのストリームIDが5であると仮定します。ストリーム7は新しい接続で再試行できます。ストリーム1、3、5のうち、完全なレスポンスを受け取れていないものは、実行の不確実性が残るため、メソッドとビジネスロジックに基づいて重複排除して処理する必要があります。優雅な終了では、まず新しいストリームの受信を停止し、既存のストリームの完了を待つ必要があります。GOAWAYを受信しただけで、すべてのリクエストを単純に再送信してはいけません。RFC 9113:接続の終了
Server Push:プロトコルの機能とブラウザのサポートは分けて考える
PUSH_PROMISEは、サーバーがクライアントが必要とする可能性のあるリソースを事前に約束し、送信することを可能にしました。しかし、キャッシュのヒットが不明確であることや、転送とスケジューリングの複雑さによる浪費が、その恩恵を弱める可能性があります。Chromeは106以降、HTTP/2 Server Pushのサポートを削除しましたが、これをもって「HTTP/2仕様からプッシュフレームが削除された」と記述してはいけません。Chromeの削除に関する説明
103 Early Hintsやプリロードヒントは、クライアントがリクエストを開始するかどうかを自分で決定できるようにするもので、サーバーがコンテンツを直接プッシュするものとは異なるメカニズムです。デプロイの選択は、実際のクライアントのサポートと測定結果に基づいて行うべきであり、「HTTP/2を有効にする」と「pushを有効にするべきだ」を結びつけてはいけません。
継続的な参照
HTTP/1.1 :メッセージの境界、キャッシュ、メソッドのセマンティクス。
HTTP/3 :QUICストリームとQPACKのトレードオフ。
TCP :バイトシーケンス番号、ウィンドウ、輻輳制御。