HTTP/2 保留 HTTP 的方法、状态、缓存等语义,用帧和流表达一次连接中的多个交换。它允许不同请求的响应交错发送,解决 HTTP/1.1 响应必须依请求顺序排列的限制;底层 TCP 仍提供单一有序字节流。
现行基础规范是 RFC 9113,替代 RFC 7540。不要把它描述成重写 TCP 的传输协议,也不能把“单连接多流”理解成全站永久只开一条连接。RFC 9113
| 定位问题 | 先检查 |
| 已协商 h2,为什么没看到 GET 文本 | 请求头使用字段块与 HPACK |
| 某个响应慢,其他流是否必须等它 | 流调度、连接窗口和 TCP 缺口分别看 |
| END_HEADERS 是否表示请求结束 | 字段块结束与发送方向结束不同 |
| 窗口很大,为什么发送停住 | 同时看流窗口和连接窗口 |
| 压缩表为何偶尔无法解码 | 两个方向各有状态,抓包可能缺历史 |
| GOAWAY 后哪些请求可以重试 | Last-Stream-ID 与业务执行不确定性 |
连接启动与字段表达
HTTPS 常用 TLS ALPN 协商 h2,之后客户端发送规定的连接前言及 SETTINGS;服务端也以 SETTINGS 开始其连接前言。客户端的24字节固定前言虽然看起来包含 HTTP 文本,但并不是向应用发送一次普通 HTTP/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 替代流状态判断。
帧头 9 B:长度不是完整帧长度
| 字段 | 长度 | 含义 |
| Length | 24 bit | 仅 payload 字节数 |
| Type | 8 bit | DATA、HEADERS、SETTINGS 等 |
| Flags | 8 bit | 按帧类型解释 |
| Reserved + Stream ID | 1 + 31 bit | 保留位与所属流,0 用于适用的连接控制 |
接收方默认最大帧 payload 为16,384 B,可通过 SETTINGS_MAX_FRAME_SIZE 在规定范围内调整,最大为16,777,215 B。这个上限不是一条响应或一个字段列表的总长度。
下面构造流1上携带5 B内容并结束本方向的 DATA 帧;完整帧14 B。代码只负责帧头和长度,不实现连接状态机。
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') & 0x7fffffff == 1
print('DATA payload / 完整帧:', len(wire) - 9, len(wire))
流状态与两种 END 标志
客户端创建的请求流使用奇数ID;服务端发起的推送流使用偶数ID,不是任意双向 RPC 都可以随便开一个偶数请求流。ID 在连接内单调使用,不会完成后回收复用;流0不是业务请求流。
END_HEADERS 表示当前压缩字段块完整,END_STREAM 表示该发送方向结束。只有请求头的 GET 可以在 HEADERS 上同时设置两者,但服务端仍能在同一流发送响应。带内容的 POST 则可能先发 HEADERS,再发多个 DATA,最后用 DATA 或 trailer HEADERS 结束。
若字段块拆到 CONTINUATION,直到 END_HEADERS 之前必须连续发送该流的对应 CONTINUATION,不能穿插其他流的 DATA。多路复用因此也有协议级连续区域,不能简单理解为任何帧都能任意插队。
多路复用:应用排队与 TCP 队头阻塞分开看
流1的慢查询尚未准备好响应时,流3可以先发送自己的响应;但发出的所有帧最终都落入同一个 TCP 序号空间。某段 TCP 字节丢失后,接收端不能把缺口之后的字节提前作为连续流交给 HTTP/2,即使它们来自另一 HTTP 流。
这不表示丢一个包会撤销所有流已经交付的数据,也不意味着帧天然对应某个 TCP 包。连接级带宽、拥塞控制、服务器调度、应用锁竞争和 HPACK 状态,也可能形成跨流影响。
双层流控:只处理 DATA,不是拥塞控制
对一个发送方向,DATA 同时受连接与流额度限制。默认初始额度为65,535 B;SETTINGS_INITIAL_WINDOW_SIZE 修改流窗口,不能用它增大连接窗口,连接额度通过流0 WINDOW_UPDATE 调整。反向发送有自己的额度。
DATA 的整个 payload 计入流控,包括 padding 及其长度字段,但不包括9 B帧头;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 增量不能为零,窗口也有上限。调低 SETTINGS_INITIAL_WINDOW_SIZE 还可能使已有流窗口暂时为负,此时需要等待额度恢复,而不是把负数强制转成无符号大数。应用及时消费并释放接收预算,才能避免流控停顿。
只给一条流补额度,为什么还是发不出去?
把连接额度用尽后,只点击流3的WINDOW_UPDATE:金色条增长,发送按钮仍不可用。再补连接额度,发送才恢复。这个状态展示了两个“剩余量取最小值”的约束如何在历史操作中形成。
正在呈现知识画面
只给一条流补额度,为什么还是发不出去?每个DATA同时扣两本账。流3仍有余额,连接归零也会暂停;HEADERS不在这两本DATA账里,但仍受其他资源约束。
{"id":"http2-shared-window","title":"只给一条流补额度,为什么还是发不出去?","summary":"每个DATA同时扣两本账。流3仍有余额,连接归零也会暂停;HEADERS不在这两本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":"只给一条流补额度,为什么还是发不出去?","intro":"任选流发送1000 B DATA,或单独补充流与连接窗口。","reset":"重置","s1":"流1发送 DATA","s3":"流3发送 DATA","scope":"单方向,两流初始各8000 B、连接10000 B;无padding,固定每次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 B表容量;第一项动态索引为62。下一次插入新项,旧项索引就变成63。静态索引2对应 :method: GET,单字节 indexed 表达为0x82;索引62对应0xbe,但只有方向和历史状态匹配时才知道它代表什么。
Never Indexed 用于敏感字段的编码约束;它不加密字段,不隐藏总长度,也不消除所有压缩侧信道。字段表容量限制与解码后字段列表大小限制是不同的资源控制。RFC 7541:HPACK
索引62为什么下一次指向别的字段?
连续插入两项后,62指向后插入者,先前的字段变成63。再引用62并观察表顺序没有变化。缩小容量后回调滑块,已经淘汰的项不会复活,因为容量限制与实际历史状态是两个变量。
正在呈现知识画面
索引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":"固定两个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”绑在一起。
继续查阅