【计算基础|网络10】TCP(中):可靠传输
上篇把连接怎么建、怎么拆讲透了。但建一条空连接没用,关键是数据传过去不能丢、不能乱、不能重复、不能让接收方被撑爆。这一篇就讲 TCP 的"可靠"二字是怎么落地的:序列号怎么编号、确认号怎么算、丢包了怎么发现怎么重传、滑动窗口怎么做到"又快又不压垮对方"。拥塞控制(管的是整个网络而不是接收方)单独放下篇。
环境:Python 3.14 本地 loopback 实测粘包;抓包用
tcpdump(macOS 需sudo,Linux 同样)。概念图用 Mermaid。
一、TCP 靠哪几招做到"可靠"
结论先给,TCP 的可靠是一整套机制叠出来的:
| 机制 | 解决什么问题 | 本篇 |
|---|---|---|
| 校验和 | 数据在传输中被改坏了 | ✅ 一句话带过 |
| 序列号 + 确认应答 | 知道收没收到、按序重组 | ✅ 本篇核心 |
| 超时重传 | 丢包了怎么办 | ✅ 本篇 |
| 快速重传 | 不等超时就发现丢包 | ✅ 本篇 |
| 滑动窗口 | 别发太快,也别太慢 | ✅ 本篇 |
| 流量控制 | 接收方缓冲区别被撑爆 | ✅ 本篇 |
| 拥塞控制 | 别把中间网络设备压垮 | ⏭️ 下篇 |
一句话:TCP 给每个字节编上号,接收方每收到一段就回一个"确认号",发出去的没收到确认就重传,窗口大小决定一次能发多少未确认的数据。
二、序列号与确认号:把字节流编成连续的号
2.1 序列号是"字节"的号,不是"报文段"的号
🔴 这是最容易理解错的一点:TCP 的序列号是对字节流编号的,不是对报文段编号的。
假设客户端 ISN(初始序列号)= x,它发了一个报文段,携带 1000 字节数据:
- 这个报文段的 SEQ = x,表示"我这段数据的第一个字节是第 x 号字节"。
- 这 1000 个字节占据了
x ~ x+999这 1000 个号。 - 下一段数据的 SEQ 就从
x+1000开始。
2.2 确认号 = "我期望收到的下一个字节号"
接收方回的 ACK 号,意思是:"ack - 1 之前的字节我都收到了,下一个想要的是 ack。"
接上例,接收方完整收到了这 1000 字节(x ~ x+999),它回的确认号就是:
ACK = x + 1000
来个带数字的完整小例子(假设客户端 ISN = 1000):
| 动作 | 报文 | SEQ | 携带数据 | 对方回的 ACK |
|---|---|---|---|---|
| 握手 | SYN | 1000 | 0(SYN 占 1 个号) | ACK = 1001 |
| 发第一批数据 | Data | 1001 | 200 字节(1001~1200) | ACK = 1201 |
| 发第二批数据 | Data | 1201 | 500 字节(1201~1700) | ACK = 1701 |
2.3 两个特殊情况:SYN 和 FIN 各占一个号
🔴 SYN 和 FIN 这两个控制报文不携带数据,但要消耗一个序列号。所以:
- 客户端发 SYN,
seq=1000,服务端回ack=1001(+1 是因为 SYN 本身占了一号)。 - 关闭时发 FIN,同样占一号:发了
seq=u的 FIN,对方回ack=u+1。
这就是上篇握手/挥手里 ACK 总是 +1 的原因。
2.4 累积确认(Cumulative ACK)
TCP 的确认是累积 的:回一个 ack=1701,等于说"1701 之前我全收到了"。它不精确告诉发送方哪一段丢了------这既是优点(ACK 丢了不影响,下一个 ACK 会带上),也是缺点(中间丢了一段,后面到的都白确认),后面 SACK 就是来补这个缺点的。
三、超时重传:丢了就重发,但要等多久
3.1 RTT 和 RTO 两个数
- RTT(Round-Trip Time,往返时间):一个报文从发出去到收到它的确认,实际花了多久。
- RTO(Retransmission Timeout,超时重传时间) :发送方发出去后,等多久还没收到 ACK 就判定丢了、重传。
RTO 必须略大于 RTT。设太短:没丢包也误判重传,浪费带宽;设太长:真丢了要等很久才发现,延迟巨大。所以 RTO 不能写死,要根据实时 RTT 动态算。
3.2 RTO 怎么算:Jacobson / Karels 算法(RFC 6298)
不能拿单次 RTT 当 RTO------网络延迟会抖动,单次值太脆。RFC 6298 的做法是维护两个平滑值:
SRTT = (1 - α) × SRTT + α × RTT # 平滑 RTT(α=1/8)
RTTVAR = (1 - β) × RTTVAR + β × |RTT - SRTT| # 平滑偏差(β=1/4)
RTO = SRTT + max(G, 4 × RTTVAR) # G 是时钟粒度,一般 1ms
直觉:SRTT 是平均往返时间,RTTVAR 是这个时间抖得有多厉害,RTO = 平均 + 4×抖动。网络越抖动,RTO 放得越宽,减少误重传;网络越稳,RTO 越贴近 SRTT,丢包发现得越快。初始 RTO 按 RFC 6298 取 1 秒。
3.3 超时后指数退避
重传之后还没收到确认怎么办?每次重传,RTO 翻倍(1s → 2s → 4s → 8s ......),叫指数退避。因为连续重传说明网络可能真的堵了,你不能越重传越猛,得给自己降速的时间。重传若干次后放弃,通知应用"连接断了"。
3.4 超时重传时序
接收方 发送方 接收方 发送方 #mermaid-svg-0g6c4Ow4Jll0dDbd{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0g6c4Ow4Jll0dDbd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0g6c4Ow4Jll0dDbd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0g6c4Ow4Jll0dDbd .error-icon{fill:#552222;}#mermaid-svg-0g6c4Ow4Jll0dDbd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0g6c4Ow4Jll0dDbd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0g6c4Ow4Jll0dDbd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0g6c4Ow4Jll0dDbd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0g6c4Ow4Jll0dDbd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0g6c4Ow4Jll0dDbd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0g6c4Ow4Jll0dDbd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0g6c4Ow4Jll0dDbd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0g6c4Ow4Jll0dDbd .marker.cross{stroke:#333333;}#mermaid-svg-0g6c4Ow4Jll0dDbd svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0g6c4Ow4Jll0dDbd p{margin:0;}#mermaid-svg-0g6c4Ow4Jll0dDbd .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-0g6c4Ow4Jll0dDbd text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-0g6c4Ow4Jll0dDbd .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-0g6c4Ow4Jll0dDbd .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-0g6c4Ow4Jll0dDbd .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-0g6c4Ow4Jll0dDbd .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-0g6c4Ow4Jll0dDbd #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-0g6c4Ow4Jll0dDbd .sequenceNumber{fill:white;}#mermaid-svg-0g6c4Ow4Jll0dDbd #sequencenumber{fill:#333;}#mermaid-svg-0g6c4Ow4Jll0dDbd #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-0g6c4Ow4Jll0dDbd .messageText{fill:#333;stroke:none;}#mermaid-svg-0g6c4Ow4Jll0dDbd .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-0g6c4Ow4Jll0dDbd .labelText,#mermaid-svg-0g6c4Ow4Jll0dDbd .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-0g6c4Ow4Jll0dDbd .loopText,#mermaid-svg-0g6c4Ow4Jll0dDbd .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-0g6c4Ow4Jll0dDbd .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-0g6c4Ow4Jll0dDbd .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-0g6c4Ow4Jll0dDbd .noteText,#mermaid-svg-0g6c4Ow4Jll0dDbd .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-0g6c4Ow4Jll0dDbd .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-0g6c4Ow4Jll0dDbd .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-0g6c4Ow4Jll0dDbd .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-0g6c4Ow4Jll0dDbd .actorPopupMenu{position:absolute;}#mermaid-svg-0g6c4Ow4Jll0dDbd .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-0g6c4Ow4Jll0dDbd .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-0g6c4Ow4Jll0dDbd .actor-man circle,#mermaid-svg-0g6c4Ow4Jll0dDbd line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-0g6c4Ow4Jll0dDbd :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 这个包丢了,没收到 等 RTO,1..100 的 ACK 一直没来 Data(seq=1..100)Data(seq=101..200)ACK=101 (累积确认:只确认到 100 之前)重传 Data(seq=1..100)ACK=201 (两段都齐了)
四、快速重传:不等超时,3 个重复 ACK 就动手
4.1 为什么要快速重传
超时重传太慢了------要等一个完整 RTO(可能几百毫秒甚至几秒)。现实中很多丢包只是个别段掉了,网络其实还通着。这时候接收方每收到一个新段,就会重复发同一个 ACK (因为它一直在等那个丢了的字节)。发送方一发现自己收到了连续 3 个重复 ACK ,就知道"前面那个段八成丢了",不用等超时,立刻重传。
接收方 发送方 接收方 发送方 #mermaid-svg-0k80MmZlG6jJkhC4{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0k80MmZlG6jJkhC4 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0k80MmZlG6jJkhC4 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0k80MmZlG6jJkhC4 .error-icon{fill:#552222;}#mermaid-svg-0k80MmZlG6jJkhC4 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0k80MmZlG6jJkhC4 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0k80MmZlG6jJkhC4 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0k80MmZlG6jJkhC4 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0k80MmZlG6jJkhC4 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0k80MmZlG6jJkhC4 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0k80MmZlG6jJkhC4 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0k80MmZlG6jJkhC4 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0k80MmZlG6jJkhC4 .marker.cross{stroke:#333333;}#mermaid-svg-0k80MmZlG6jJkhC4 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0k80MmZlG6jJkhC4 p{margin:0;}#mermaid-svg-0k80MmZlG6jJkhC4 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-0k80MmZlG6jJkhC4 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-0k80MmZlG6jJkhC4 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-0k80MmZlG6jJkhC4 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-0k80MmZlG6jJkhC4 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-0k80MmZlG6jJkhC4 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-0k80MmZlG6jJkhC4 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-0k80MmZlG6jJkhC4 .sequenceNumber{fill:white;}#mermaid-svg-0k80MmZlG6jJkhC4 #sequencenumber{fill:#333;}#mermaid-svg-0k80MmZlG6jJkhC4 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-0k80MmZlG6jJkhC4 .messageText{fill:#333;stroke:none;}#mermaid-svg-0k80MmZlG6jJkhC4 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-0k80MmZlG6jJkhC4 .labelText,#mermaid-svg-0k80MmZlG6jJkhC4 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-0k80MmZlG6jJkhC4 .loopText,#mermaid-svg-0k80MmZlG6jJkhC4 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-0k80MmZlG6jJkhC4 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-0k80MmZlG6jJkhC4 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-0k80MmZlG6jJkhC4 .noteText,#mermaid-svg-0k80MmZlG6jJkhC4 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-0k80MmZlG6jJkhC4 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-0k80MmZlG6jJkhC4 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-0k80MmZlG6jJkhC4 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-0k80MmZlG6jJkhC4 .actorPopupMenu{position:absolute;}#mermaid-svg-0k80MmZlG6jJkhC4 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-0k80MmZlG6jJkhC4 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-0k80MmZlG6jJkhC4 .actor-man circle,#mermaid-svg-0k80MmZlG6jJkhC4 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-0k80MmZlG6jJkhC4 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 收到,回 ACK=101 收到 3 个重复 ACK,不等超时 Data 1(seq=1..100)Data 2(seq=101..200) ❌ 丢失Data 3(seq=201..300)重复 ACK=101 (第1次,因为 101~200 没到)Data 4(seq=301..400)重复 ACK=101 (第2次)Data 5(seq=401..500)重复 ACK=101 (第3次)立刻重传 Data 2(seq=101..200)ACK=501 (全部齐了)
4.2 为什么是 3 个,不是 1 个或 2 个
🔴 关键在区分"丢包"和"乱序":
- 网络里数据包偶尔会乱序到达(后发的先到)。接收方看到"来的比我期望的大",也会发重复 ACK。
- 1 个或 2 个重复 ACK,很可能只是乱序,重传了反而浪费。
- 连续 3 个,基本可以断定不是乱序而是真丢包了------这时候重传的收益远大于代价。
3 是工程上权衡误判率和重传及时性后定下的经验值。
五、滑动窗口:一次到底能发多少
5.1 发送窗口 = 未确认的 + 还能发的
发送方不能把所有数据一股脑全发出去(那样接收方收不下、网络也顶不住)。它维护一个发送窗口:
- 窗口左边界:第一个还没被确认的字节。
- 窗口右边界:左边界 + 窗口大小。
- 窗口内的数据 = 已经发出去但还没确认的 + 还能继续发的。
每收到一个 ACK,左边界右移,窗口跟着"滑动",所以叫滑动窗口。
#mermaid-svg-fEkb3sXD0kANtlMv{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-fEkb3sXD0kANtlMv .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-fEkb3sXD0kANtlMv .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-fEkb3sXD0kANtlMv .error-icon{fill:#552222;}#mermaid-svg-fEkb3sXD0kANtlMv .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-fEkb3sXD0kANtlMv .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-fEkb3sXD0kANtlMv .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-fEkb3sXD0kANtlMv .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-fEkb3sXD0kANtlMv .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-fEkb3sXD0kANtlMv .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-fEkb3sXD0kANtlMv .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-fEkb3sXD0kANtlMv .marker{fill:#333333;stroke:#333333;}#mermaid-svg-fEkb3sXD0kANtlMv .marker.cross{stroke:#333333;}#mermaid-svg-fEkb3sXD0kANtlMv svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-fEkb3sXD0kANtlMv p{margin:0;}#mermaid-svg-fEkb3sXD0kANtlMv .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-fEkb3sXD0kANtlMv .cluster-label text{fill:#333;}#mermaid-svg-fEkb3sXD0kANtlMv .cluster-label span{color:#333;}#mermaid-svg-fEkb3sXD0kANtlMv .cluster-label span p{background-color:transparent;}#mermaid-svg-fEkb3sXD0kANtlMv .label text,#mermaid-svg-fEkb3sXD0kANtlMv span{fill:#333;color:#333;}#mermaid-svg-fEkb3sXD0kANtlMv .node rect,#mermaid-svg-fEkb3sXD0kANtlMv .node circle,#mermaid-svg-fEkb3sXD0kANtlMv .node ellipse,#mermaid-svg-fEkb3sXD0kANtlMv .node polygon,#mermaid-svg-fEkb3sXD0kANtlMv .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-fEkb3sXD0kANtlMv .rough-node .label text,#mermaid-svg-fEkb3sXD0kANtlMv .node .label text,#mermaid-svg-fEkb3sXD0kANtlMv .image-shape .label,#mermaid-svg-fEkb3sXD0kANtlMv .icon-shape .label{text-anchor:middle;}#mermaid-svg-fEkb3sXD0kANtlMv .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-fEkb3sXD0kANtlMv .rough-node .label,#mermaid-svg-fEkb3sXD0kANtlMv .node .label,#mermaid-svg-fEkb3sXD0kANtlMv .image-shape .label,#mermaid-svg-fEkb3sXD0kANtlMv .icon-shape .label{text-align:center;}#mermaid-svg-fEkb3sXD0kANtlMv .node.clickable{cursor:pointer;}#mermaid-svg-fEkb3sXD0kANtlMv .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-fEkb3sXD0kANtlMv .arrowheadPath{fill:#333333;}#mermaid-svg-fEkb3sXD0kANtlMv .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-fEkb3sXD0kANtlMv .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-fEkb3sXD0kANtlMv .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fEkb3sXD0kANtlMv .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-fEkb3sXD0kANtlMv .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fEkb3sXD0kANtlMv .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-fEkb3sXD0kANtlMv .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-fEkb3sXD0kANtlMv .cluster text{fill:#333;}#mermaid-svg-fEkb3sXD0kANtlMv .cluster span{color:#333;}#mermaid-svg-fEkb3sXD0kANtlMv div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-fEkb3sXD0kANtlMv .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-fEkb3sXD0kANtlMv rect.text{fill:none;stroke-width:0;}#mermaid-svg-fEkb3sXD0kANtlMv .icon-shape,#mermaid-svg-fEkb3sXD0kANtlMv .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fEkb3sXD0kANtlMv .icon-shape p,#mermaid-svg-fEkb3sXD0kANtlMv .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-fEkb3sXD0kANtlMv .icon-shape .label rect,#mermaid-svg-fEkb3sXD0kANtlMv .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fEkb3sXD0kANtlMv .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-fEkb3sXD0kANtlMv .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-fEkb3sXD0kANtlMv :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-fEkb3sXD0kANtlMv .done>*{fill:#d4edda!important;color:#333!important;}#mermaid-svg-fEkb3sXD0kANtlMv .done span{fill:#d4edda!important;color:#333!important;}#mermaid-svg-fEkb3sXD0kANtlMv .done tspan{fill:#333!important;}#mermaid-svg-fEkb3sXD0kANtlMv .wait>*{fill:#fff3cd!important;color:#333!important;}#mermaid-svg-fEkb3sXD0kANtlMv .wait span{fill:#fff3cd!important;color:#333!important;}#mermaid-svg-fEkb3sXD0kANtlMv .wait tspan{fill:#333!important;}#mermaid-svg-fEkb3sXD0kANtlMv .send>*{fill:#cfe2ff!important;color:#333!important;}#mermaid-svg-fEkb3sXD0kANtlMv .send span{fill:#cfe2ff!important;color:#333!important;}#mermaid-svg-fEkb3sXD0kANtlMv .send tspan{fill:#333!important;}#mermaid-svg-fEkb3sXD0kANtlMv .block>*{fill:#e2e3e5!important;color:#999!important;}#mermaid-svg-fEkb3sXD0kANtlMv .block span{fill:#e2e3e5!important;color:#999!important;}#mermaid-svg-fEkb3sXD0kANtlMv .block tspan{fill:#999!important;} 发送缓冲区
已确认
(窗口外,可丢弃)
已发未确认
(窗口内)
未发可发
(窗口内)
未发暂不能发
(窗口外)
5.2 窗口大小听谁的:rwnd 和 cwnd 取最小
发送窗口的大小由两个值决定,取较小的那个:
实际发送窗口 = min(接收窗口 rwnd, 拥塞窗口 cwnd)
| 窗口 | 谁决定 | 管什么 |
|---|---|---|
| rwnd(接收窗口) | 接收方通过 TCP 首部"窗口大小"字段通告 | 别发太快撑爆接收方的缓冲区 |
| cwnd(拥塞窗口) | 发送方自己根据网络拥塞情况估算 | 别发太快压垮网络中间设备 |
🔴 记一句话:rwnd 管接收方肚子,cwnd 管网络路况。本篇讲 rwnd(流量控制),cwnd(拥塞控制)放下篇。
5.3 窗口关闭(Zero Window)与窗口探测
接收方缓冲区快满了,就把 rwnd 通告成 0。发送方收到后暂停发送,但会定期发一个"窗口探测"探针(Window Probe),问接收方"现在缓冲区空出来没?"------防止接收方后来通告窗口变大的那个 ACK 丢了,导致双方死等。
六、流量控制:别把接收方撑爆
6.1 一句话机制
流量控制就是接收方在每个 ACK 里通过"窗口大小"字段告诉发送方:我现在还能收多少 。发送方保证"已发未确认的数据量"不超过这个窗口大小。这就是滑动窗口在做的事------滑动窗口本身就是流量控制的实现机制,不用另搞一套。
举个数字:接收方通告 rwnd=4000,发送方当前已发未确认的有 3000 字节,那它最多还能再发 1000 字节就得停下等确认。
6.2 糊涂窗口综合征(Silly Window Syndrome)
如果窗口每次只更新一点点,发送方就会为了发几百个字节而背上整个 TCP/IP 首部(40 字节),有效载荷比例极低,这叫糊涂窗口综合征------窗口变得"碎成糊涂账"。两个方向都可能发生:
| 谁引发 | 现象 | 解法 |
|---|---|---|
| 接收方 | 缓冲区一空出一点就通告小窗口,诱导发送 | Clark 算法:缓冲区空出足够一整个 MSS 才通告窗口 |
| 发送方 | 攒了一点小数据就急着发 | Nagle 算法:小数据包先攒着,等凑够 MSS 或收到上一个 ACK 再发 |
| 双方 | ACK 也发得太碎 | 延迟 ACK(Delayed ACK):ACK 攒一会再发,捎带上顺便确认 |
🔴 工程上你会直接碰到的就是 Nagle 算法 :它默认开启,但有时会和"想立刻发小命令"打架(比如 SSH 交互、Redis 发个命令希望马上到)。这时可以用 TCP_NODELAY 关掉 Nagle:
python
cli.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
七、面向字节流:粘包是怎么回事
7.1 TCP 不保证消息边界
🔴 这是写 socket 最容易踩的坑:TCP 是面向字节流的,它不知道你应用层的"一条消息"在哪结束 。你 send() 三次,对端可能一次 recv() 全收走(粘包),也可能分好几次才收完(拆包)。TCP 只保证字节顺序和不丢,不保留消息边界。
对比:UDP 面向报文 ,你发一个 sendto(一个数据报),对端一次 recvfrom() 就拿到完整的那一个报文,边界天然在。
7.2 实测:粘包真实长什么样
我们让客户端分三次 sendall()(3 字节 + 6 字节 + 2 字节 = 11 字节),服务端按顺序 recv():
python
import socket, threading, time
PORT = 18201
def server():
s = socket.socket(); s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("127.0.0.1", PORT)); s.listen(1)
c, _ = s.accept()
time.sleep(0.5) # 等客户端三段都发完
for i in range(3):
data = c.recv(100)
print(f"服务端第 {i+1} 次 recv() 收到 {len(data)} 字节: {data}")
c.close(); s.close()
threading.Thread(target=server, daemon=True).start()
time.sleep(0.3)
cli = socket.socket(); cli.connect(("127.0.0.1", PORT))
cli.sendall(b"AAA") # 第 1 次写 3 字节
time.sleep(0.1)
cli.sendall(b"BBBBBB") # 第 2 次写 6 字节
time.sleep(0.1)
cli.sendall(b"CC") # 第 3 次写 2 字节
time.sleep(1.2)
真实输出:
服务端第 1 次 recv() 收到 11 字节: b'AAABBBBBBCC'
看清楚了:客户端分三次写 ,服务端第一次 recv() 就一次性把 11 个字节全收了 ,AAA、BBBBBB、CC 粘成了一整坨。后面再 recv() 就阻塞了,因为对端已经没有更多数据。
⚠️ 这意味着你绝不能假设"一次 recv 对应一次 send"。应用层必须自己定义消息边界,常见做法:
- 固定长度消息;
- 在消息头里带长度字段(先读 N 字节长度,再按长度读 body);
- 用特殊分隔符(如
\r\n,HTTP 就是这么干的)。
完整的粘包/分包解决方案和 socket 编程细节,留到 Socket 编程那一篇(网络 15)细讲。
八、SACK:选择性确认,只重传真正丢的
累积确认有个硬伤:接收方只能说"我收到了第 N 号之前"。如果中间丢了一段、后面的段都到了,发送方光靠累积确认不知道到底哪段丢了,只能从最早没确认的地方全部重传,浪费带宽。
**SACK(Selective Acknowledgment,选择性确认)**解决这个问题:接收方在 ACK 里额外带上"我还收到了哪些不连续的块",发送方一看就知道哪些到了、哪些没到,只重传真正丢的那几段。
握手时双方通过选项"允许 SACK(SACK Permitted)"协商开启,Linux 默认 net.ipv4.tcp_sack=1 开启。普通场景你不用管,高带宽长肥管道(BDP 大)下 SACK 收益明显。
九、用 tcpdump 抓真实的 seq/ack(思路)
理论看完,想看真实报文怎么标 seq/ack 吗?在 Linux/macOS 上用 tcpdump(需 root):
bash
# 抓 loopback 上 18201 端口的包,-nn 不解析域名/端口名
sudo tcpdump -i lo0 -nn port 18201
你会看到类似(字段顺序):
IP 127.0.0.1.55555 > 127.0.0.1.18201: Flags [S], seq 1000, win 65535, options [...]
IP 127.0.0.1.18201 > 127.0.0.1.55555: Flags [S.], seq 5000, ack 1001, win 65535, [...]
IP 127.0.0.1.55555 > 127.0.0.1.18201: Flags [.], ack 5001, win 65535, [...]
对着认:
Flags [S]= SYN,Flags [S.]= SYN+ACK,Flags [.]= 纯 ACK,Flags [F]= FIN,Flags [R]= RST。seq 1000就是序列号,ack 1001就是确认号。- 把这几行和上篇握手时序图一一对上,序列号怎么 +1 就彻底通了。
ℹ️ macOS 的 loopback 接口是
lo0,Linux 是lo;抓包需要sudo。下一篇会讲到怎么用内核参数影响这些行为。
总结:你真正需要记住的 7 件事
- 序列号是字节的号,不是报文段的号;确认号 = "我期望收到的下一个字节号",是累积确认。
- SYN 和 FIN 不携带数据,但各占一个序列号,所以 ACK 总是 +1。
- RTO 不能写死,按 Jacobson/Karels 算法用 SRTT + 4×RTTVAR 动态算(RFC 6298);超时重传后 RTO 指数翻倍。
- 收到 3 个重复 ACK 就快速重传,不用等超时;3 个是为了区分乱序和真丢包。
- 实际发送窗口 = min(rwnd, cwnd):rwnd 是接收方通告的(流量控制),cwnd 是发送方自己估的(拥塞控制,下篇讲)。
- 流量控制 = 滑动窗口;防"糊涂窗口"靠 Nagle(发送方)、Clark(接收方)、延迟 ACK。
- TCP 面向字节流,没有消息边界,会粘包/拆包;应用层必须自己定消息边界。UDP 才面向报文。
验证清单
- 能口头解释"发 1000 字节、ISN=1000,对方回的 ACK 是多少"
- 能说出为什么 SYN/FIN 要占一个序列号
- 能说清 RTO 为什么要动态计算、超时后为什么指数退避
- 能画快速重传时序,并解释为什么是 3 个重复 ACK
- 能说清 rwnd 和 cwnd 各管谁、为什么取 min
- 能复现一次粘包(分多次 send,一次 recv 全收),并写出两种解粘包思路
- 知道 Nagle 算法什么时候要
TCP_NODELAY关掉 - 能用
tcpdump -nn看到真实的 seq/ack 和 Flags
参考资源
- RFC 9293(TCP 现行标准,序列号/确认/重传):https://www.rfc-editor.org/rfc/rfc9293
- RFC 6298(TCP 重传超时 RTO 计算):https://www.rfc-editor.org/rfc/rfc6298
- RFC 2018(TCP 选择性确认 SACK):https://www.rfc-editor.org/rfc/rfc2018
- Linux man 手册
man 7 tcp(TCP_NODELAY、Nagle、SACK、滑动窗口):https://man7.org/linux/man-pages/man7/tcp.7.html - tcpdump 官方文档:https://www.tcpdump.org/manpages/tcpdump.1.html
下一篇《TCP(下):拥塞控制与参数优化》讲:慢启动/拥塞避免/快恢复四条算法怎么配合、cwnd 那条经典曲线长什么样、New Reno 和 Cubic、Google 的 BBR 凭什么在高带宽长延迟网络起飞,以及生产服务器那一堆 sysctl TCP 参数(连接队列、缓冲区、拥塞算法、keepalive)到底怎么调。