应用调用两次 send,接收方可能一次读出全部,也可能分多次才读完。TCP 保持的是有序字节流,不保留应用写入的消息边界。理解它可以沿一笔数据追踪:序号标识字节,ACK 表示接收进度,窗口约束可发送范围,丢失检测触发恢复,拥塞控制决定向网络施加多少负载。
| 需要快速定位 | 本篇重点 |
| “粘包”、短读和 ACK 的含义 | 字节流与应用交付 |
| SYN/FIN 为何让 ACK 加一 | 序号空间与连接生命周期 |
| 收到后面的数据为何还在重复 ACK | 累计 ACK、SACK 与缺口 |
| 带宽很大却发不快 | rwnd、cwnd、在途量与 BDP |
| 重传何时发生 | 重复 ACK、RACK-TLP 与 RTO |
| CUBIC 和 BBR 如何理解 | 曲线算例、模型和版本边界 |
字节流:可靠接收不等于业务完成
TCP 在连接仍可维持时通过确认、重传和有序交付恢复数据,无法在永久断网时保证最终成功。send 成功通常说明数据进入本地发送路径;TCP ACK 说明对端 TCP 已接收相应字节,不证明对端应用已读、写盘或提交事务。业务确认仍应由应用协议表达。
例如两次写入 abc 和 def,接收端可能读到 ab、cdef。长度前缀、固定长度或分隔符等 framing 才定义消息边界,PSH 不是可靠的消息分隔标志。TCP 也不自带加密或服务器身份认证,TLS 等机制承担另一层职责。RFC 9293:TCP 基础规范
头部字段怎样连接到行为
| 字段 | 位数/单位 | 实际含义 |
| 源/目的端口 | 各 16 bit | 与地址、协议及网络上下文定位端点 |
| Sequence Number | 32 bit | 当前段起始序号,含 SYN 时需考虑 SYN 占位 |
| Acknowledgment Number | 32 bit | ACK 有效时,期待下一个连续序号 |
| Data Offset | 4 bit,单位 4 B | TCP 头长,通常 20~60 B |
| 控制标志 | 如 SYN、ACK、FIN、RST、PSH、ECE、CWR | 建连、确认、关闭、复位与拥塞反馈等 |
| Window | 16 bit,经协商可缩放 | 对端通告的接收窗口,不是原样暴露全部 socket 内存 |
| Checksum | 16 bit | 覆盖伪首部、TCP 头与数据,不是密码学认证 |
| Urgent Pointer | 16 bit | 配合 URG 的历史紧急数据语义,不能当独立优先通道 |
TCP 长度需由 IP 层长度和实际头链推出,不应拿 Data Offset 当整段长度。32 bit 序号会回绕,比较应按协议的模序号规则和窗口约束处理,普通整数大小比较不能覆盖全部状态。
选项:有些只在建连时协商
| 选项 | 作用 | 边界 |
| MSS,Kind 2 | 通告本端可接收的 TCP 数据段上限 | 只在 SYN 中出现;不包含 TCP/IP 头 |
| Window Scale,Kind 3 | 本端后续窗口字段的缩放位数,最大 14 | 在 SYN 中协商,SYN 自身窗口字段不缩放 |
| SACK Permitted,Kind 4 | 表示允许对端使用 SACK | 与实际 SACK 区间报告分开 |
| SACK,Kind 5 | 报告非连续接收区间 | 并非重新定义累计 ACK |
| Timestamp,Kind 8 | 支持 RTT 测量与 PAWS 等 | 需按协商及规范使用,不能认为所有 TCP 包天然携带 |
窗口缩放是分方向的:C 通告的 shift 用于解释 C 后续发出的接收窗口,限制 S 向 C 的发送。MSS 同样约束对端发给本端的数据,且实际发送还会受 PMTU、选项、拥塞与接收窗口限制。RFC 7323:缩放与时间戳
握手:确认序号与连接状态
客户端 SYN 使用序号 1000,服务端用 ACK=1001 确认它,并提出自身 ISN=5000;客户端再确认 5001。第三步让服务端知道自己的序号已被对端确认,有助于排除旧的重复连接尝试。握手确认的是当时协议状态,不保证之后网络永远可达。
SYN 和 FIN 各占一个序号,让控制事件能够统一纳入有序确认和重传;不能反推为“不占序号的消息在任何协议里都无法重传”。纯 ACK 不占序号,也不会为每个 ACK 再专门确认一次。
常规客户端经过 SYN-SENT,服务端经过 SYN-RECEIVED,再进入 ESTABLISHED;同时打开、携带数据的扩展和异常复位另有规则。SYN cookies 是抵抗某些半连接资源压力的实现机制,有协商能力和使用条件,不能取代正常队列规划或全部防护。RFC 9293:连接建立
累计 ACK、SACK 与重传区间
假设收到 [1001,2001) 和 [3001,4001),中间 [2001,3001) 丢失。累计 ACK 只能为 2001;SACK 额外报告 [3001,4001)。补齐缺口后,连续范围到 4001,ACK 才能跳到那里。
SACK 表达接收事实,发送端还需要维护记分板、判断丢失并安排重传;“启用 SACK 就永远只重发真正丢失的一份数据”过于绝对,乱序和误判会影响恢复。接收方也有撤回已 SACK 数据的边界,因此发送方不能只因 SACK 就在累计确认之前丢掉所有重传副本。RFC 2018:SACK
丢失检测与拥塞控制是两个相关但不同的问题:前者决定哪些数据可能丢了,后者决定接下来允许的负载。经典快速重传利用重复 ACK 等信号;现代 RACK 根据发送时间与后续确认推断丢失,TLP 用尾部探测改善某些尾丢失恢复,不必都等三次重复 ACK。RFC 8985:RACK-TLP
RTO 为什么不能直接等于 RTT
RTO 要给时延变化留余量。RFC 6298 的基础估计为 SRTT + max(G,4×RTTVAR),并配合最小值与退避规则。第一次样本 R=100 ms 时,SRTT=100 ms、RTTVAR=50 ms,原始计算值 300 ms;按该 RFC 建议的 1 s 下限得到 1 s,实际内核的下限实现应另行核对。
下一样本 120 ms 时,先用旧 SRTT 更新 RTTVAR:0.75×50+0.25×20=42.5 ms;再更新 SRTT:0.875×100+0.125×120=102.5 ms。原始 RTO=272.5 ms。重传后的 ACK 可能无法确定确认的是哪一次发送,Karn 规则及时间戳用于处理相应采样歧义;超时还会触发退避。RFC 6298:重传计时
ACK 到了哪里,应用读到了哪里?
先送第三段,再送第一段,累计确认只会跨过真正连续的部分。补齐第二段时 ACK 可以一次前移两段;最后点击应用读取,观察网络确认与应用消费是两个位置。图中的 SACK 区间按教学分段列出,相邻区间可合并。
正在呈现知识画面
ACK 到了哪里,应用读到了哪里?累计 ACK 指向下一个期待字节;乱序数据可暂存,但不能跳过缺口交给字节流应用。应用读取不会重新定义 ACK。
{"id":"tcp-receive-holes","title":"ACK 到了哪里,应用读到了哪里?","summary":"累计 ACK 指向下一个期待字节;乱序数据可暂存,但不能跳过缺口交给字节流应用。应用读取不会重新定义 ACK。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"actions\"><button id=\"one\" data-i18n=\"one\"></button><button id=\"two\" data-i18n=\"two\"></button><button id=\"three\" data-i18n=\"three\"></button><button id=\"read\" data-i18n=\"read\"></button><button id=\"reset\" data-i18n=\"reset\"></button></div><section class=\"panel\"><h3 data-i18n=\"buffer\"></h3><div id=\"buffer\"></div></section><div class=\"metrics\"><div><span data-i18n=\"ack\"></span><strong id=\"ack\"></strong></div><div><span data-i18n=\"app\"></span><strong id=\"app\"></strong></div></div><p id=\"sack\" class=\"message\"></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#buffer{display:flex;gap:6px}","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,app=1001;function draw(){let n=0;while(got.has(n))n++;const ack=1001+n*1000;cells('buffer',[0,1,2].map(i=>({text:'['+(1001+i*1000)+','+(2001+i*1000)+')'+(app>1001+i*1000?' ✓':''),cls:!got.has(i)?'empty':i<n?'on':'gold'})));set('ack',ack);set('app',app);const blocks=[...got].filter(i=>i>=n).sort().map(i=>'['+(1001+i*1000)+','+(2001+i*1000)+')');set('sack','SACK ranges: '+(blocks.join(', ')||'∅')+'\\nreadable = '+(ack-app)+' B');$('#read').disabled=ack===app;$('#ack').dataset.value=ack;$('#app').dataset.value=app;}['one','two','three'].forEach((id,i)=>$('#'+id).onclick=()=>{got.add(i);draw()});$('#read').onclick=()=>{app=+$('#ack').textContent;draw()};$('#reset').onclick=()=>{got=new Set;app=1001;draw()};draw();","audio":false,"strings":{"ack":"累计 ACK","app":"应用下次读取序号","buffer":"缓冲区(蓝:连续;金:乱序)","heading":"ACK 到了哪里,应用读到了哪里?","intro":"任选数据区间递送,再让应用读取连续前缀;金色表示缺口后的暂存。","one":"递送 [1001,2001)","read":"应用读取","reset":"重置","scope":"固定三个1000 B区间,首数据序号1001;不考虑回绕、接收容量和FIN。读取按钮一次消费当前连续可读数据,不代表业务提交。","scopeLabel":"模型范围与约定","three":"递送 [3001,4001)","two":"递送 [2001,3001)"}}
流控与拥塞窗口:先扣除已在途的量
在普通、非恢复的简化模型中,发送端受接收窗口 rwnd 与拥塞窗口 cwnd 共同限制。min(rwnd,cwnd) 是在途规模约束,不是每次都能再发送这么多;还需扣除已发未确认数据,再考虑 pacing、应用可用数据与实际实现。
设 SND.UNA=1001、SND.NXT=4001,则已有 3000 B 未确认。rwnd=8000 B、cwnd=5000 B 时,简化的新数据余量为 min(8000,5000)−3000=2000 B。把 cwnd 提升到 20000 B 后,约束变为接收窗口,新余量是 5000 B。
| 限制来源 | 典型证据 | 增大另一项是否解决 |
| 接收端读取慢,rwnd 小 | 通告窗口缩小、零窗口 | 单独增大 cwnd 无效 |
| 网络拥塞或恢复,cwnd 小 | 拥塞事件、窗口变化、重传 | 单独增大接收缓冲无效 |
| 应用供数不足 | 发送队列空、应用停顿 | 增大任一窗口都可能无效 |
| pacing 或主机处理能力 | 发送节奏、CPU/队列证据 | 不能仅由窗口大小解释 |
零窗口后,发送端的 persist 机制探测窗口是否重新打开,防止丢失窗口更新导致长期停滞;它与空闲连接 keepalive、丢包重传计时器是不同用途。Nagle 则限制存在未确认数据时的小段发送,配合延迟 ACK 可能增加小交互等待;TCP_NODELAY 不会恢复应用消息边界。
带宽时延积复算
1 Gbit/s、RTT=50 ms 的 BDP 为 10⁹/8×0.05=6,250,000 B。未缩放的 65535 B 窗口,即使忽略其他限制,窗口/RTT 给出的上限也只有约 10.486 Mbit/s。scale=7 时最大表示 8,388,480 B,约 8 MiB,才可能覆盖这个 BDP;这只是必要预算,不是吞吐保证。
窗口不是还能发送的字节数
三个滑块控制不同的状态量。把在途量拖到两种窗口之间,可以看出较大的那个窗口不能补偿较小的约束;再把窗口缩到在途量以下,观察后续发送暂停,而已有在途量仍留在图中。
credit=max(0,min(rwnd,cwnd)−FlightSize)
正在呈现知识画面
窗口不是还能发送的字节数先取两种约束的较小值,再扣除已在途数据。窗口收缩到在途量以下时,新的发送额度为零,不会把已经发出的数据变成负数。
{"id":"tcp-window-budget","title":"窗口不是还能发送的字节数","summary":"先取两种约束的较小值,再扣除已在途数据。窗口收缩到在途量以下时,新的发送额度为零,不会把已经发出的数据变成负数。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"control\"><label for=\"rwnd\" data-i18n=\"rwnd\"></label><output for=\"rwnd\" id=\"rwnd-value\"></output><input id=\"rwnd\" type=\"range\" min=\"0\" max=\"10000\" value=\"8000\" step=\"500\"></div><div class=\"control\"><label for=\"cwnd\" data-i18n=\"cwnd\"></label><output for=\"cwnd\" id=\"cwnd-value\"></output><input id=\"cwnd\" type=\"range\" min=\"0\" max=\"10000\" value=\"5000\" step=\"500\"></div><div class=\"control\"><label for=\"flight\" data-i18n=\"flight\"></label><output for=\"flight\" id=\"flight-value\"></output><input id=\"flight\" type=\"range\" min=\"0\" max=\"10000\" value=\"3000\" step=\"500\"></div><div id=\"limits\" class=\"intervals\"></div><div class=\"metrics\"><div><span data-i18n=\"credit\"></span><strong id=\"credit\"></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;}));}function draw(){const r=+$('#rwnd').value,c=+$('#cwnd').value,f=+$('#flight').value,limit=Math.min(r,c),credit=Math.max(0,limit-f);for(const id of ['rwnd','cwnd','flight'])set(id+'-value',$('#'+id).value);intervals('limits',[{start:0,end:r,label:'rwnd '+r},{start:0,end:c,label:'cwnd '+c},{start:0,end:f,label:'FlightSize '+f,cls:'gold'},{start:Math.min(f,limit),end:limit,label:'credit '+credit}],10000);set('credit',credit);set('equation','SND.NXT = 1001 + '+f+' = '+(1001+f)+'\\ncredit = max(0, min('+r+', '+c+') − '+f+') = '+credit);$('#credit').dataset.value=credit;}document.querySelectorAll('input').forEach(e=>e.oninput=draw);draw();","audio":false,"strings":{"credit":"新数据发送额度(B)","cwnd":"cwnd(B)","flight":"FlightSize(B)","heading":"窗口不是还能发送的字节数","intro":"拖动接收窗口、拥塞窗口和在途字节,比较上限与余额。","reset":"重置","rwnd":"rwnd(B)","scope":"固定SND.UNA为1001,用FlightSize近似在途量;不含SACK pipe、零窗口探测、pacing、恢复算法或窗口更新合法性。每次拖动是独立快照。","scopeLabel":"模型范围与约定"}}
关闭:两个方向独立结束
A 发 FIN 后不再发送新的应用数据,但仍可接收 P 的数据。P 确认后可能停留 CLOSE-WAIT,直到应用结束自己的发送方向;随后 P 发 FIN,A 确认。ACK 与 FIN 可以合并,所以关闭并非必须正好四个包。同时关闭也会改变状态路径。
典型主动关闭方经过 FIN-WAIT-1、FIN-WAIT-2、TIME-WAIT;被动一侧经过 CLOSE-WAIT、LAST-ACK。大量 CLOSE-WAIT 应检查应用为何尚未结束连接,而不是用 TIME_WAIT 参数解释。
TIME-WAIT 为响应重传 FIN 和避免旧连接报文干扰新连接保留时间,不保证最后 ACK 一定送达。规范用 2MSL 描述,Linux 7.2 的 TCP_TIMEWAIT_LEN 为 60×HZ,即 60 s;应区分规范模型与实现计时。tcp_fin_timeout 针对相应孤儿 FIN-WAIT-2 状态,不是 TIME-WAIT 时长开关。
连接复用可降低建连/关闭频率;SO_REUSEADDR 影响符合条件的绑定,不能消除全部四元组冲突。tcp_tw_reuse 也有安全条件和版本配置含义,应先区分状态与资源瓶颈,再谈参数。RFC 9293:关闭、Linux TCP sysctl
CUBIC:从保留比例理解增长曲线
网络可以用丢包、ECN 等信号反映拥塞,不能说拥塞控制只有“猜”且网络绝不主动反馈。CUBIC 的一个核心目标曲线是:
W(t) = C×(t−K)³ + Wmax
当 β 表示拥塞后保留比例时,K = ∛(Wmax×(1−β)/C)。取 β=0.7、C=0.4、Wmax=100 段,K≈4.22 s;W(0)=70 段,W(K)=100 段。旧公式把保留比例 0.7 直接放入 K 的分子,会使 W(0) 错成 30 段。RFC 9438:CUBIC
曲线在接近旧 Wmax 时增长放缓,越过后继续探测;时间是这个函数的自变量,但实际 TCP 仍依赖 ACK、RTT、Reno 友好区域和恢复规则,不能概括为吞吐完全不受 RTT 影响。启动时的近似指数增长也受 ACK 行为和限制条件影响,不是每条连接每个 RTT 必定精确翻倍。
CUBIC 的拥塞响应并不意味着每个单独丢包都独立乘 0.7;事件归并、快速恢复与超时具有不同处理。队列大小、AQM、ECN、随机丢包和竞争流都会影响最终延迟与吞吐。
拐点前后,增长为什么先慢后快?
让时间游标经过 K,观察窗口仍在增长,但斜率先降到零再上升。改变 β 会同时移动初始窗口与 K,而 W(K) 始终回到旧 Wmax。这比只记住三次方的外形更能解释公式中的平移项。
Wcubic(t)=C(t−K)3+Wmax,K=3Wmax(1−β)/C
正在呈现知识画面
拐点前后,增长为什么先慢后快?K 把曲线的平坦区放到旧 Wmax 附近;β 是丢失后的保留比例,不是减掉的比例。三次目标曲线不等于真实发送速率。
{"id":"cubic-window-shape","title":"拐点前后,增长为什么先慢后快?","summary":"K 把曲线的平坦区放到旧 Wmax 附近;β 是丢失后的保留比例,不是减掉的比例。三次目标曲线不等于真实发送速率。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"control\"><label for=\"time\" data-i18n=\"time\"></label><output for=\"time\" id=\"time-value\"></output><input id=\"time\" type=\"range\" min=\"0\" max=\"10\" value=\"0\" step=\"0.1\"></div><div class=\"control\"><label for=\"beta\" data-i18n=\"beta\"></label><output for=\"beta\" id=\"beta-value\"></output><input id=\"beta\" type=\"range\" min=\"0.5\" max=\"0.9\" value=\"0.7\" step=\"0.05\"></div><svg id=\"curve\" class=\"diagram\" viewBox=\"0 0 360 180\" role=\"img\" data-i18n-label=\"heading\"></svg><div class=\"metrics\"><div><span data-i18n=\"window\"></span><strong id=\"window\"></strong></div><div><span data-i18n=\"slope\"></span><strong id=\"slope\"></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;}));}function draw(){const time=+$('#time').value,beta=+$('#beta').value,K=Math.cbrt(100*(1-beta)/.4),w=t=>.4*(t-K)**3+100,value=w(time),slope=1.2*(time-K)**2;set('time-value',time.toFixed(1));set('beta-value',beta.toFixed(2));const x=t=>30+t*30,y=w=>160-w*.45,points=Array.from({length:101},(_,i)=>x(i/10)+','+y(w(i/10))).join(' ');$('#curve').innerHTML='<path class=\"edge\" d=\"M30 15 V160 H340\"/><line class=\"edge\" x1=\"30\" y1=\"'+y(100)+'\" x2=\"330\" y2=\"'+y(100)+'\"/><polyline fill=\"none\" stroke=\"var(--accent)\" stroke-width=\"3\" points=\"'+points+'\"/><circle cx=\"'+x(time)+'\" cy=\"'+y(value)+'\" r=\"5\" fill=\"var(--second)\"/><text x=\"34\" y=\"'+(y(100)-7)+'\">Wmax=100</text><text x=\"280\" y=\"178\">t (s)</text><text x=\"'+x(K)+'\" y=\"155\">K</text>';set('window',value.toFixed(2));set('slope',slope.toFixed(2));set('equation','K = ∛(100×(1−'+beta.toFixed(2)+')/0.4) = '+K.toFixed(3)+' s\\nW(0) = '+(100*beta).toFixed(1)+'; W(K) = 100\\nW′(t) = 1.2(t−K)²');$('#window').dataset.value=value;}document.querySelectorAll('input').forEach(e=>e.oninput=draw);draw();","audio":false,"strings":{"beta":"保留比例 β","heading":"拐点前后,增长为什么先慢后快?","intro":"直接拖动时间与保留比例,曲线和切线斜率一起更新。","reset":"重置","scope":"Wmax=100段,C=0.4,只显示Wcubic目标与导数;省略Reno友好区、ACK驱动更新、应用受限和再次丢包。","scopeLabel":"模型范围与约定","slope":"此处斜率(段/s)","time":"距丢失时间(s)","window":"目标窗口(段)"}}
BBR:带宽与时延模型也需要验证
BBR 使用交付速率和 RTT 等观测估计路径容量,结合 pacing 和在途约束控制发送。典型过程包括快速探测容量、排空启动形成的队列、周期性探测带宽和更新最小时延估计。观测会受 ACK 聚合、应用受限、路径变化及其他流影响,模型不是直接读取真实物理瓶颈。
Linux 7.2 的 tcp_bbr.c 包含经典四模式、约 2.885 的启动 pacing gain,以及 1.25/0.75 等稳态增益;这不能推广为全部 BBR 版本的统一常数。带宽估计、cwnd 增益和 pacing gain 也不同,不能把在途量始终精确钉为一个 BDP。
Google 的 BBR 项目包含持续演进的实现与材料,应明确所比较版本。RFC 9438 定义 CUBIC;BBR 的算法与版本应查对应项目资料。不要用“BBR 从不因丢包降速”“必然无队列”或未经限定的部署百分比替代实验条件。Google BBR 项目
| 比较维度 | CUBIC | BBR 系列 |
| 核心理解入口 | 窗口增长曲线与拥塞响应 | 交付速率/时延模型与 pacing |
| 必须说明的条件 | 恢复算法、ECN/AQM、RTT 与负载 | 版本、观测偏差、丢包/ECN 处理 |
| 不能保证 | 同等带宽下永远公平或低延迟 | 始终无排队、对所有竞争流公平 |
| 验证指标 | 吞吐、RTT 分布、重传、队列与公平性 | 同样需要这些指标,而非只看单流峰值 |
本地推演:字节区间、窗口与曲线
import math
received = [(1001,2001),(3001,4001)]
def cumulative(intervals, start=1001):
ack = start
for left,right in sorted(intervals):
if left>ack:
break
ack = max(ack,right)
return ack
assert cumulative(received) == 2001
assert cumulative(received+[(2001,3001)]) == 4001
assert min(8000,5000)-(4001-1001) == 2000
assert 1_000_000_000/8*.05 == 6_250_000
assert 65535<<7 == 8_388_480
assert math.isclose(65535*8/.05/1e6,10.4856)
srtt,variation = .1,.05
sample = .12
variation = .75*variation+.25*abs(srtt-sample)
srtt = .875*srtt+.125*sample
assert math.isclose(srtt+4*variation,.2725)
beta,C,Wmax = .7,.4,100
K = (Wmax*(1-beta)/C)**(1/3)
def target(t):
return C*(t-K)**3+Wmax
assert math.isclose(target(0),70)
assert math.isclose(target(K),100)
assert target(K+1)>100
print('ACK 缺口、发送余量、BDP、RTO 与 CUBIC 边界检查通过')
这是局部数学模型,不模拟真实重传定时、内核拥塞状态或网络公平性;未修改拥塞控制和连接参数。
继续查阅