send 的边界不会原样变成 recv 的边界,ACK 也不意味着对端已提交业务。TCP 提供有序可靠字节流,理解 Linux 实现时,先把序列、窗口、恢复和关闭方向 分开。
如果还不熟悉报文头、选项和协议层面的窗口计算,可以先读网络系列的 TCP 。本篇沿 Linux 的连接队列、收发路径与 socket 行为展开;两篇共用的序列与窗口模型用于衔接协议和实现。
查阅目标 阅读位置
路由与握手队列 第 1~2 节
字节序列、BDP 与丢包 第 3~5 节
控制算法与快路径 第 6~7 节
半关闭实验 第 8 节
1. IP 路由决定本机交付还是转发
IPv4 接收验证基本头部后,经过 PREROUTING 等处理再作路由决策。本机目标进入 LOCAL_IN/INPUT 后交给 TCP、UDP 等处理;转发路径经过 FORWARD 和出口处理。本机发送需要初始路由信息,OUTPUT 中的地址/mark 变化还可能触发重新路由,之后进入 POSTROUTING。
IPv6 有自己的输入与路由实现:基本头部没有 IPv4 头校验和,扩展头处理和分片规则也不同。IPv6 路由器不在途中代替源端分片,Path MTU Discovery 等机制因此必须正确处理,不能照搬 IPv4 函数链当成通用协议栈。
2. 三次握手建立的是连接状态
监听 socket 保持 LISTEN,握手建立连接对象
客户端
服务端监听与连接状态
SYN:seq = x
SYN + ACK:seq = y,ack = x + 1
ACK:ack = y + 1
accept 从已建立连接队列取连接;它不负责触发客户端 SYN。
普通主动打开经历 SYN_SENT,再由合法回应推进到 ESTABLISHED。服务端监听 socket 继续监听,半连接常用较轻量的 request socket 跟踪,握手完成后才进入可 accept 的连接队列;SYN cookie、TCP Fast Open 等又会改变具体资源和时序。
SYN 队列与 accept 队列是不同阶段,listen(backlog) 也不是为所有半连接直接设定同一个总数。accept 返回过慢可能堵住已完成握手的队列,不能只靠扩大 SYN 相关参数解决。
握手交换初始序列号及选项,SYN 自身消耗一个序列号。成功建立 TCP 只说明传输连接可用,不说明应用已认证或业务服务已经准备好。协议规范见 RFC 9293 。
3. 序列、ACK 与接收队列
收到的范围 接收状态 累计 ACK
[1000,2000) 已按序收到 下一字节应为 2000 2000
先到 [3000,4000) 中间仍有缺口 仍为 2000
缺口随后补齐 才能推进连续接收位置 随连续范围推进
TCP 序列号计数字节。SACK 可以另行报告离散已收范围,让发送者更准确选择重传;累计 ACK 不会跳过尚未填补的缺口。
对端确认字节进入接收协议状态,不等于对端应用已经读取,更不等于业务写入数据库已经提交。send 返回也不等于收到 ACK。应用若需要消息边界或完成确认,必须定义长度、分隔、请求 ID 和响应协议。
Linux 可以把顺序数据放入接收队列,把乱序数据保存在相应结构中;应用读取时再按语义交付有序字节流。PSH 不构成可靠的消息分界,recv(4096) 可能返回少于 4096 字节,也可能一次合并多个 send 的数据。
直接点击任意字节段制造乱序与重复到达。累计 ACK 只沿连续前缀前进;应用读取另记一笔,不能把接收端确认当成应用已经消费。
正在呈现知识画面 重新加载
后面的数据到了,ACK 为什么还停在缺口? 累计 ACK 指向下一个尚未连续收到的字节;SACK 描述后方离散范围。协议确认不等于应用消费,重复收到已知范围不再次推进 ACK。
{"id":"tcp-cumulative-ack","title":"后面的数据到了,ACK 为什么还停在缺口?","summary":"累计 ACK 指向下一个尚未连续收到的字节;SACK 描述后方离散范围。协议确认不等于应用消费,重复收到已知范围不再次推进 ACK。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><p class=\"caption\" data-i18n=\"arrive\"></p><div class=\"choices\"><button data-segment=\"0\">1<small>[1000,2000)</small></button><button data-segment=\"1\">2<small>[2000,3000)</small></button><button data-segment=\"2\">3<small>[3000,4000)</small></button><button data-segment=\"3\">4<small>[4000,5000)</small></button><button data-segment=\"4\">5<small>[5000,6000)</small></button><button data-segment=\"5\">6<small>[6000,7000)</small></button><button data-segment=\"6\">7<small>[7000,8000)</small></button><button data-segment=\"7\">8<small>[8000,9000)</small></button></div><div class=\"track-row\"><span data-i18n=\"stream\"></span><div id=\"stream\" class=\"track\"></div></div><div class=\"metrics\"><div><span data-i18n=\"ack\"></span><strong id=\"ack\"></strong></div><div><span data-i18n=\"consumed\"></span><strong id=\"consumed\"></strong></div></div><div class=\"formula\" id=\"sack\"></div><p class=\"caption\" data-i18n=\"last\"></p><div id=\"last\" class=\"readout\"></div><div class=\"actions\"><button id=\"read\" data-i18n=\"read\"></button></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"note\"></p></div><div class=\"actions\"><button id=\"reset\" data-i18n=\"reset\"></button></div><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.choices button{min-height:65px}.choices small{display:block;font:10px var(--viz-font-ui);overflow-wrap:anywhere;margin-top:5px}.choices button.received{background:var(--accent-soft)}#sack{min-height:6.5em;white-space:pre-line}.readout{min-height:3.2em}","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)));}let received=Array(8).fill(false),consumed=0,last='—';function contiguous(){const i=received.indexOf(false);return i<0?8:i;}function draw(){const n=contiguous(),sack=[];for(let i=n;i<8;){if(!received[i]){i++;continue;}let j=i+1;while(j<8&&received[j])j++;sack.push('['+(1000+i*1000)+','+(1000+j*1000)+')');i=j;}cells('stream',received.map((v,i)=>({text:String(i+1),cls:i<consumed?'gold':v?'on':'empty'})));document.querySelectorAll('[data-segment]').forEach(e=>e.classList.toggle('received',received[+e.dataset.segment]));set('ack',1000+n*1000);set('consumed',consumed*1000);set('sack','SACK\\n'+(sack.join(' ')||'—'));set('last',last);$('#read').disabled=consumed>=n;set('explanation',t('note'));}document.querySelectorAll('[data-segment]').forEach(e=>e.onclick=()=>{const i=+e.dataset.segment;last='['+(1000+i*1000)+','+(2000+i*1000)+')'+(received[i]?' ×2':'');received[i]=true;draw();});$('#read').onclick=()=>{if(consumed<contiguous())consumed++;draw();};$('#reset').onclick=()=>{received=Array(8).fill(false);consumed=0;last='—';draw();};draw();","audio":false,"strings":{"ack":"累计 ACK / 字节序号","arrive":"点击让这段数据到达","consumed":"应用已读取 / 字节","heading":"后面的数据到了,ACK 为什么还停在缺口?","intro":"点击编号让对应的 1000 字节范围到达,可重复点击模拟重复数据。按任意顺序补齐缺口,再让应用读取。","last":"最近到达的范围","note":"先点 3、4,再点 1。第 2 段未到时 ACK 仍停在 2000;补上 2 会一次跨过已经保存的后续范围。","read":"应用读取下一段","reset":"重置","scope":"八段,每段 1000 字节,初始 RCV.NXT=1000,无 SYN/FIN、序列回绕、接收窗口限制和 ACK 发包延迟。SACK 行展示逻辑区间,不模拟有限 TCP option 容量或通告顺序。应用每次消费一段连续数据。","scopeLabel":"模型范围与约定","stream":"字节流位置:蓝色已收,金色已被应用读取"}}
4. 流量控制与拥塞控制是两把尺子
接收窗口 rwnd 表示接收端还能接收多少数据;拥塞窗口 cwnd 表示发送端对网络可承载在途量的限制。两者单位在实现和显示中可能不同,例如 cwnd 常以 MSS 段计数,需要换算再比较。
粗略地说,可继续发送的新数据量受 min(cwnd_bytes, rwnd_bytes) - bytes_in_flight 限制,同时还要考虑 pacing、应用是否有数据、重传及实现细节。窗口不是发送速率的直接单位。
窗口是新发送的约束,不会抹去已经在途的数据。下面把 cwnd、rwnd 与在途量放到同一字节尺度,改变任一上限后再尝试发送。
new_data ≤ max ( 0 , min ( cwnd , rwnd ) − flight )
三个量都按字节计。此处比较当前发送额度,未模拟拥塞算法、SACK 管道估计或真实窗口更新协议。
正在呈现知识画面 重新加载
两把窗口尺子,减的是同一份在途量 新数据额度为 max(0, min(cwnd, rwnd) − flight)。窗口缩小可以使 flight 超过新上限,此时停止新增发送,等待确认释放额度。
{"id":"tcp-flight-window","title":"两把窗口尺子,减的是同一份在途量","summary":"新数据额度为 max(0, min(cwnd, rwnd) − flight)。窗口缩小可以使 flight 超过新上限,此时停止新增发送,等待确认释放额度。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><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=\"16\" value=\"8\" step=\"1\"></div><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=\"16\" value=\"12\" step=\"1\"></div><p class=\"caption\" data-i18n=\"c\"></p><div class=\"band\"><i id=\"bar-c\"></i></div><p class=\"caption\" data-i18n=\"r\"></p><div class=\"band\"><i id=\"bar-r\"></i></div><p class=\"caption\" data-i18n=\"f\"></p><div class=\"band\"><i id=\"bar-f\" class=\"second\"></i></div><div class=\"axis\"><span>0</span><span>8000</span><span>16000 B</span></div><div class=\"actions\"><button id=\"send\" data-i18n=\"send\"></button><button id=\"ack\" data-i18n=\"ack\"></button></div><div class=\"metrics\"><div><span data-i18n=\"flight\"></span><strong id=\"flight\"></strong></div><div><span data-i18n=\"allowance\"></span><strong id=\"allowance\"></strong></div></div><div class=\"formula\" id=\"arithmetic\"></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"note\"></p></div><div class=\"actions\"><button id=\"reset\" data-i18n=\"reset\"></button></div><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#arithmetic{min-height:6em}","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)));}let flight=0;function draw(){const c=+$('#cwnd').value*1000,r=+$('#rwnd').value*1000,a=Math.max(0,Math.min(c,r)-flight);set('cwnd-value',c+' B');set('rwnd-value',r+' B');for(const[k,v]of [['c',c],['r',r],['f',flight]])$('#bar-'+k).style.width=v/160+'%';set('flight',flight);set('allowance',a);set('arithmetic','max(0, min('+c+', '+r+') − '+flight+') = '+a+' B');$('#send').disabled=a<1000;$('#ack').disabled=flight===0;set('explanation',t('note'));}$('#cwnd').oninput=$('#rwnd').oninput=draw;$('#send').onclick=()=>{if(Math.min(+$('#cwnd').value,+$('#rwnd').value)*1000-flight>=1000)flight+=1000;draw();};$('#ack').onclick=()=>{flight=Math.max(0,flight-1000);draw();};$('#reset').onclick=()=>{flight=0;draw();};draw();","audio":false,"strings":{"ack":"确认 1000 B","allowance":"可新增发送 / B","c":"拥塞窗口","cwnd":"cwnd / 1000 B","f":"已发未确认","flight":"在途量 / B","heading":"两把窗口尺子,减的是同一份在途量","intro":"调整 cwnd 与 rwnd,再发送或确认 1000 字节。先发出多段再缩小窗口,观察已有在途数据不会凭空消失。","note":"蓝色是当前限制,金色是在途量。两把尺子都约束同一条连接,不是先用完 cwnd 再去使用 rwnd。","r":"接收窗口","reset":"重置","rwnd":"rwnd / 1000 B","scope":"所有量统一为字节,图中每格 1000 B,最大 16000 B。不模拟 TCP 拥塞算法、pacing、窗口缩放、重传和零窗口探测;rwnd 滑块用于比较限制快照,不建议真实接收端任意缩回已通告右边界。","scopeLabel":"模型范围与约定","send":"发送 1000 B"}}
算一笔在途量:带宽足够,窗口仍可能限制吞吐
条件或计算 结果
链路 1 Gbit/s,RTT 20 ms BDP 约 2.5 MB,即 2.38 MiB
有效在途量仅 64 KiB 远小于这个路径的 BDP
64 KiB / 0.02 s 约 3.28 MB/s 的简化吞吐上限
此处忽略其他限制。计算说明窗口不足会限制吞吐,并不说明扩大 buffer 能解决全部性能问题。
扩大 buffer 可以消除某些限制,但不能修复丢包、错误 MTU、拥塞或慢应用。接收自动调优还有上限、内存压力和用户设置等约束;设置 SO_RCVBUF 等参数也会影响自动调优行为,不能承诺“高 BDP 永远不需要检查缓冲区”。见 TCP sysctl 。
5. 丢包检测不只有超时
重复 ACK、SACK 和按时间推断的 RACK 等信息可以在 RTO 到期前发现丢失。RTO 仍是恢复的重要兜底机制,基于平滑 RTT 与波动估计,并在超时后退避。
RFC 6298 给出 RTO 计算框架;具体 Linux 最小值、路径和可配置项应对照目标版本,不能把 RFC 的推荐数值当成所有 Linux 定时器的固定值。重传后的 RTT 样本存在歧义,估算不能把任何 ACK 都直接归给最近一次发送。
定时器按目的分类:窗口、空闲与丢失
零窗口探测处理接收端窗口关闭,keepalive 检查长期空闲连接可达性,两者都不是普通丢包重传定时器的改名。TCP 重试超时也不能保证对端应用没处理过先前的数据,业务重试需要自己的去重语义。
6. CUBIC 与 BBR:比较控制目标,不背宣传结论
CUBIC 的窗口增长围绕上次拥塞前的窗口值设计,立方函数改变接近旧峰值与远离旧峰值时的增长行为;还包含 TCP-friendly 行为等细节,不能用一条立方公式替代全部算法。RFC 9438 是 CUBIC 的规范,不是 BBR 的规范。
BBR 通过交付速率和传播 RTT 等估计构建路径模型,并用 pacing 和在途限制控制发送。不同 BBR 版本的丢包、ECN、公平性及探测行为不同;不能统一说“完全不考虑丢包”或“任何拥塞链路都优于 CUBIC”。实际内核模块名也不自动证明采用了某个论文/补丁版本。
cat /proc/sys/net/ipv4/tcp_congestion_control
cat /proc/sys/net/ipv4/tcp_available_congestion_control
ss - tin
系统默认算法影响新连接,已有 socket 还可有自己的设置。比较时固定路径、RTT、流数量、接收窗口和瓶颈队列,观察吞吐与延迟及共存流表现。
7. 快速路径不是绕过协议正确性
tcp_rcv_established() 包含对常见已建立连接报文的优化处理。header prediction 等条件成立时,可减少一般状态处理开销;乱序、异常标志、窗口变化、选项等会使处理走其他分支。
不能据函数名字推导“90% 报文固定只执行几十条指令”。校验和卸载、GRO、锁竞争、socket backlog、内存分配和唤醒都会影响成本。socket 正被用户上下文占用时,报文还可能排入 backlog,随后由合适路径处理。
MSG_DONTWAIT 只控制本次读写是否等待,不能把它当成内核收包直接写入用户缓冲区的条件。源码应结合 net/ipv4/tcp_input.c、tcp.c 和 tcp_output.c 阅读。
8. 关闭一个方向,不等于关闭整个连接
情况 典型状态/行为 解释
主动发送 FIN FIN_WAIT1,FIN 被确认后 FIN_WAIT2 本端发送方向关闭,仍可接收
收到对方 FIN CLOSE_WAIT 对方不再发数据,本地应用仍可发送
被动方随后关闭 LAST_ACK 等待本端 FIN 被确认
主动方完成双方关闭 TIME_WAIT 处理迟到报文及最终 ACK 重传等协议需要
双方交叉发送 FIN 可能经过 CLOSING 不是每次都严格串行走同一路径
TIME_WAIT 与 FIN_WAIT2 是不同状态,不能认为 tcp_fin_timeout 就是通用 TIME_WAIT 时长开关。CLOSE_WAIT 长期积累常需检查应用关闭行为;大量 TIME_WAIT 则不能单凭数量判断泄漏。
可运行的半关闭例子
下面只使用本机回环和临时端口。客户端写完后关闭发送方向,服务端看到 EOF 后仍能返回已收字节数。
import socket
import threading
errors = [ ]
with socket . socket ( ) as listener :
listener . bind ( ( ' 127.0.0.1' , 0 ) )
listener . listen ( 1 )
listener . settimeout ( 3 )
def serve ( ) :
try :
conn , _ = listener . accept ( )
with conn :
conn . settimeout ( 3 )
size = 0
while data : = conn . recv ( 1024 ) :
size += len ( data )
conn . sendall ( str ( size ) . encode ( ' ascii' ) )
except Exception as exc :
errors . append ( exc )
worker = threading . Thread ( target = serve )
worker . start ( )
try :
with socket . create_connection ( listener . getsockname ( ) , timeout = 3 ) as client :
client . sendall ( b ' hello' )
client . shutdown ( socket . SHUT_WR )
parts = [ ]
while data : = client . recv ( 1024 ) :
parts . append ( data )
assert b ' ' . join ( parts ) == b ' 5'
finally :
worker . join ( timeout = 4 )
assert not worker . is_alive ( )
if errors :
raise errors [ 0 ]
print ( ' half-close: reply received after SHUT_WR' )
这个协议特意用 EOF 作为请求边界;长期复用连接一般需另设长度或帧格式。它说明 TCP 半关闭语义,不模拟断网、重传或应用幂等。