UDP 保留数据报边界,但不保证交付、顺序和去重。QUIC 借助 UDP 承载自己的加密包、确认与多流机制;它提供可靠流,也可通过扩展承载不可靠数据报。选择它们时,应先明确业务需要消息还是字节流、是否允许过期丢弃、怎样确认成功,而不是把 UDP 等同于“快”、QUIC 等同于“永不断线”。
| 查阅问题 | 核心理解 |
| UDP 一次发送多大 | UDP 长度、IP 长度和路径 MTU分别约束 |
| QUIC 重传为何换包号 | 包号标识一次发送,流偏移标识数据位置 |
| 丢包是否阻塞所有流 | 流内有序,跨流没有 TCP 式统一字节缺口 |
| 0-RTT 是不是首次就零等待 | 需要恢复状态,可能被拒绝且有重放风险 |
| 换 IP 后为何可能继续 | CID、路径验证与迁移规则共同作用 |
UDP:8 B 头部之外还有使用责任
UDP 头由源端口、目的端口、Length、Checksum 四个 16 bit 字段组成。Length 包含 8 B UDP 头;普通字段最大 65535,但在无选项 IPv4 中,还需受 IP 总长度限制,最大 UDP 数据是 65535−20−8=65507 B。这个协议上界远大于常见路径可直接承载的大小,并不建议应用发送这么大的数据报。RFC 768:UDP
在 MTU=1500、无选项/扩展头时,IPv4 UDP 数据最多 1472 B,IPv6 为 1452 B;隧道和 QUIC 自身开销还会继续扣除。若一个 UDP 数据报被 IP 分片,通常需要所有片重组成功才能交付这份数据报,丢一片就可能损失整份数据。
IPv4 UDP 的零校验和字段表示未提供该校验;若实际计算结果为零,应在线上发送全 1。IPv6 UDP 默认要求校验和,特定隧道例外和 jumbogram 有独立规则,不能把例外当作普通应用默认。校验覆盖伪首部与 UDP 消息,不提供发送者身份认证。
应用还要处理丢失、乱序、重复、大小探测和拥塞控制。UDP connect 可以设置本地默认对端和过滤等 socket 行为,并不执行 TCP 式远端握手;send 成功也不证明对端接收。实时业务常宁愿丢弃已经过时的数据,但仍应控制发送负载。RFC 8085:UDP 应用准则
QUIC 的四层对象
| 对象 | 标识/边界 | 功能 |
| UDP 数据报 | UDP 长度 | 路径上的一次数据报承载,可合装多个 QUIC 包 |
| QUIC 包 | Packet Number 等 | 加密保护、确认与丢失检测的发送单位 |
| Frame | 帧类型和具体字段 | STREAM、ACK、CRYPTO、流控或路径验证等语义 |
| Stream | Stream ID 与字节 Offset | 应用可使用的独立有序字节流 |
一条流可以跨很多包,一个包也可以携带多条流的帧。丢包后重新发送需要恢复的信息,可能装进另一包并重新分组,因此不是把旧包原封不动再发一次。
长头、短头与可见信息
QUIC v1 的 Initial、Handshake、0-RTT 使用相应长头,1-RTT 使用短头;Retry 和版本协商又有特殊结构,不能用一个固定字节表覆盖所有类型。长头携带 Version、连接 ID 长度等信息;短头的目的 CID 长度依赖连接上下文,不能仅凭随手抓到的孤立包就完整解析。RFC 9000:包格式
Packet Number 在线上只编码必要的低位,并由接收方结合上下文恢复。部分头字段受头保护,不能在解保护前把它们当作明文标志直接分析。v1 基线的 QUIC Bit 规则也存在后续协商扩展,因此需结合协商结果解释该位。RFC 9287:QUIC Bit 扩展
Spin Bit 是可选的测量信号,端点可以不提供,观察还会受丢包、重排和路径影响。它不是每个端点严格按一个定时器“每 RTT 翻一次”的通用时钟,也不揭示应用请求时延。
重传的是信息,还是原来的包?
先只递送包11,流4已经有可交付数据,流0仍为空。再送包12,最后补送迟到的包10:包确认集合增加了,唯一流数据量却不再增加。这两个编号空间不能合并理解。
正在呈现知识画面
重传的是信息,还是原来的包?同一流区间可以装进新编号的包。接收去重按流偏移,包的确认按包编号;一个流的缺口不阻止另一个流交付。
{"id":"quic-packet-stream-identity","title":"重传的是信息,还是原来的包?","summary":"同一流区间可以装进新编号的包。接收去重按流偏移,包的确认按包编号;一个流的缺口不阻止另一个流交付。","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":"发送两个流的数据,任意递送原包或重传包。流偏移与包编号分开显示。","p10":"递送包 10","p11":"递送包 11","p12":"递送重传包 12","reset":"重置","scope":"仅1-RTT STREAM数据,包10/12都携带流0的[0,100),包11携带流4的[0,100)。不模拟拥塞、密钥、流控与应用协议依赖。","scopeLabel":"模型范围与约定","streams":"两个独立的流前缀","unique":"唯一流数据(B)"}}
握手与加密级别
QUIC 使用 TLS 1.3 的握手消息和密钥协商,但不把 TLS record 原样套在传输流上;CRYPTO 帧按相应加密级别承载握手字节。Initial、Handshake、0-RTT、1-RTT 的保护与使用条件不同。Initial 密钥可由公开信息派生,所以 Initial 包保护不等于已经对网络观察者保密或完成服务器认证。RFC 9001:TLS 与 QUIC
首次握手常可在一次往返后开始相应应用通信,但 Retry、证书大小、丢包和调度会改变实测耗时。服务器在尚未验证客户端地址时受反放大限制,通常最多发送收到字节数的三倍;这可能限制一次性发完握手数据。
客户端承载 Initial 的 UDP 数据报需扩展到至少 1200 B,指的是 UDP 载荷,不是加上 UDP/IP 后恰好 1200 B。无扩展头 IPv6 下对应至少 1248 B IP 包,仍需考虑真实路径。RFC 9000:数据报大小与地址验证
0-RTT:提前发送,不是提前获得全部保证
恢复连接时,客户端可依据保存的 TLS 恢复状态和允许沿用的传输参数发送 early data;首次访问不能凭空拥有这些条件。服务器可能拒绝 early data,TLS 的接受信号决定结果,QUIC 没有用于此目的的 REJECTED 帧。拒绝时,客户端按协议与应用接口处理原本的早期数据,不能把拒绝当成业务已经执行。RFC 9001:0-RTT
0-RTT 存在重放风险,应用必须判断操作语义和部署中的重放防护。HTTP 方法名不是充分证明:错误实现的 GET 也可能有副作用。AEAD 的密钥使用上限限制密码学风险,并不提供业务去重或限制攻击者的重放次数。
流编号与流控:限额是累计上限
Stream ID 的最低位表示发起端(0 客户端、1 服务端),次低位表示方向(0 双向、1 单向);余下高位是该类别的序号。因此各类 ID 每次加 4:
| 类别 | 首批 ID |
| 客户端发起双向 | 0、4、8、12 |
| 服务端发起双向 | 1、5、9、13 |
| 客户端发起单向 | 2、6、10、14 |
| 服务端发起单向 | 3、7、11、15 |
双向流的两个发送方向有各自字节偏移。MAX_STREAM_DATA 约束相应流方向的累计可发送范围,MAX_DATA 约束连接范围内相应流数据的累计使用,MAX_STREAMS 则限制可开启流数量;这些不是三个可以互换的“缓冲大小”。RFC 9000:流与流控
例如连接限额 10000 B,两条流已分别使用 4000 和 3000 B,连接还余 3000 B;若目标流自身只余 1000 B,则新增数据最多受这 1000 B 限制。重传已有字节不会因为再次发送而重复消耗新的流控额度。RESET_STREAM 的 final size 也参与相关记账,不能用不断重置绕过上限。
独立流消除了 TCP 单一有序字节流导致的跨流传输层等待,但同一流内部仍需补齐缺口。多个流共享拥塞控制、连接流控、CPU 与路径;HTTP/3 还可能有应用层依赖,所以“QUIC 完全没有队头阻塞”仍不准确。
重传为什么不再占一份流控额度?
把连接总量与目标流单独画出,才能看出瓶颈来自哪里。保持偏移不变反复重传,线速重复字节增长,流控余额却不变;这不是免费带宽,重传仍受拥塞控制等其他约束。
credit0=min(MSD0−H0,MD−i∑Hi)
正在呈现知识画面
重传为什么不再占一份流控额度?额度按各流已消耗的最大偏移累计,不按重复发出的线速字节累计。新数据同时受连接与流上限约束,重传已有区间不提高偏移。
{"id":"quic-cumulative-credit","title":"重传为什么不再占一份流控额度?","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":"重传为什么不再占一份流控额度?","intro":"改变两条流的最高已发送偏移与上限,查看连接和目标流共同留下的额度。","remaining":"流0可发新数据(B)","repeated":"重复发送 / B","reset":"重置","retry":"重传流0前100 B","s0":"流0已消耗偏移(B)","s4":"流4已消耗偏移(B)","scope":"两条普通可靠流,无RESET_STREAM和空洞发送;MAX_DATA10000、目标流MAX_STREAM_DATA5000。这里只演算流控,不等于拥塞或反放大许可。","scopeLabel":"模型范围与约定"}}
ACK 与丢失检测:确认不等于读心
Initial、Handshake、Application Data 是三个 Packet Number Space,其中 0-RTT 和 1-RTT 共用应用数据空间。包号在各自空间中递增且不复用;把流字节重新装包发送时使用新包号。ACK 报告已经收到的包号区间,并不直接列出一份已被物理证实丢失的包清单。
若包 10 未确认而较大包已确认,它可能丢失,也可能只是乱序。RFC 9002 用包阈值和时间阈值等推断:推荐包阈值为 3;时间参考 9/8×max(latest_rtt,smoothed_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 delay,连续 PTO 还有退避。
拥塞控制决定发送负载,恢复机制决定补哪些信息。RFC 9002 给出一个基于 NewReno 的示例拥塞算法,QUIC 并不要求所有实现必须使用它;CUBIC、BBR 等可在符合协议约束下由实现采用。
CID 与迁移:新路径仍要验证
接收端可用自己发放的 Connection ID 将来自新地址的包关联到连接。CID 不是永恒不变的一对编号;端点可以按规则发放、使用和退役多个 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 连接或网络迁移。
继续查阅