写在前面
上一篇把 TCP 的连接生命周期走了一遍:握手建状态、挥手拆状态、中间一张 11 状态的状态机。但建立连接只是搭好了一座桥,真正的难题在桥上:IP 层是不可靠的——包会丢、会乱序、会重复、会损坏。TCP 怎么在这堆"不靠谱"之上,给应用层交付一条按顺序、不丢、不重、不出错的字节流?
这一篇就来拆 TCP 最核心的能力——可靠性。它靠三件事撑起来:给每个字节编号(序号)、收到就回执(确认)、丢了就补(重传) 。围绕这三件套,再配上滑动窗口让"等确认"这件事可以批量进行,TCP 才既可靠又不会慢到没法用。
文末会把流量控制讲清——它是收发两端之间"别发太快压垮我"的协调;和下一篇要讲的拥塞控制(“别发太快压垮网络”)是两回事,这一篇先把边界划清楚。
一、不可靠的 IP 之上,如何造出可靠
IP 层是"尽力而为"(best-effort)的:路由器拥塞了就丢包、链路抖动了就乱序,IP 自己一概不负责。TCP 要做的,就是在应用看来"假装"这些都没发生。它的工具箱只有三件:
- 序号(Sequence Number):给发送的每个字节编个号,接收端据此去重、排顺;
- 确认(Acknowledgment):接收端收到数据后回个 ACK,告诉发送端"我到这里了";
- 重传(Retransmission):发送端等不到确认就再发一次。
这套思路有个学名:ARQ(Automatic Repeat reQuest,自动重传请求) 。下面先看序号和确认怎么用,再看 ARQ 有哪几种、TCP 选了哪种。
二、字节流、序号与确认号
TCP 传的不是"一条条消息",而是连续的字节流。序号针对的是字节,不是报文段:
- 发送端把自己要发的数据看成一长串字节,给每个字节一个编号;TCP 头里的序号字段,表示这个报文段里第一个字节的编号。
- 初始序号(ISN)不是 0,而是握手时协商的随机值——上一篇文章里那个
seq=x。
举个例子,假设 ISN = 1000:
| |
确认号:累计确认
TCP 的确认号含义是:我下一个期望收到的字节编号。换句话说,“这个编号之前的所有字节我都收到了”——这叫累计确认(cumulative ACK)。
延续上例,接收端收到 seq=1000、长度 1024 字节的段后,回:
| |
累计确认的好处:中间丢一个,后面的 ACK 照样能回来,发送端看确认号就能推断出哪里断了;坏处:接收端乱序到达的段没法直接确认(ACK 还是只回"按顺序连续到哪了"),这部分后面 SACK 再补。
SYN 和 FIN 虽然不携带用户数据,但各占一个序号(上一篇握手 / 挥手里
ack=x+1、ack=u+1就是这个原因),这样它们也能被确认、能被重传,语义统一。
三、ARQ 的三种思想:停等 → 回退 N 步 → 选择重传
“发一段、等确认、丢了重发"这件事,可以做得粗,也可以做得细。经典的三种思想:
| 策略 | 发多少再等 | 丢了怎么办 | 信道利用 |
|---|---|---|---|
| 停等(Stop-and-Wait) | 发 1 段,等 ACK | 重发那 1 段 | 极低(一来一回只能发一段) |
| 回退 N 步(GBN) | 一次发 N 段(窗口) | 从丢失段起,后面全部重发 | 较高,但重发浪费 |
| 选择重传(SR) | 一次发 N 段 | 只重发丢失的那段 | 最高,但实现复杂 |
- 停等是教科书的起点,但一次只在途一段,在跨地域高延迟链路上等于把带宽饿死。
- 回退 N 步允许"一次在途 N 段”,配合累计确认很自然;代价是乱序或丢一段时,已经发出去但未确认的整批都得重来。
- 选择重传只补丢的那段,需要接收端能缓存乱序段、发送端能选择性重发,实现更复杂,要靠 SACK(Selective ACK,RFC 2018)选项 支持。
TCP 选了什么
TCP 的累计确认与 Go-Back-N 有相似之处,但真实 TCP 不能简单归类为标准的 Go-Back-N:超时重传、快速重传、Reno / NewReno 恢复和 SACK 会共同决定补传哪些数据。累计 ACK 只能指出“下一个期望字节”;如果两端协商了 SACK(多数现代系统默认开启),接收端还能报告已经收到的非连续区间,发送端据此更精确地选择补传内容。
一句话总结:TCP 是“滑动窗口 + 累计确认 + 快速 / 超时重传 + 可选 SACK”的工程组合,不是某种单一 ARQ 模型的纯实现。
四、重传超时 RTO 与 RTT 估算
“等多久还没收到 ACK 就算丢了”——这个阈值叫 RTO(Retransmission Timeout) 。设长了,真丢包时干等、延迟飙升;设短了,包没丢只是慢了就误重传,反而拥塞。理想 RTO 应该紧跟着网络实际的往返时延 RTT。
RTT 怎么测、RTO 怎么算
TCP 会持续测量报文段的往返时间(SampleRTT = 收到 ACK 的时刻 − 发送时刻),但单次样本抖动大,所以要平滑。RFC 6298 给的经典做法是两个 EWMA(指数加权移动平均):
| |
工程实现里还会给 RTO 加下限(比如至少 1 秒)并加上退避:每次重传后 RTO 翻倍(指数退避),避免重传把拥塞的网络越搞越糟。
Karn 算法:重传过的段别用来测 RTT
这里有个歧义:一段发出后被重传过,最后收到的 ACK 到底是确认"第一次"还是"重传那一次"?分不清就会把 RTT 测错。Karn 算法规定:重传过的报文段,其 ACK 不参与 RTT 采样;同时配合上面的指数退避。简单粗暴但有效。
五、滑动窗口:让"等确认"可以批量进行
如果发一段就停下等一个 ACK(停等),在长肥管道(高带宽 × 高延迟)上吞吐会很惨。TCP 的解法是滑动窗口:允许发送端在还没收到确认的情况下,连续发出一段范围内的数据。这个"范围"就是窗口。
发送缓冲区的四段划分
把发送缓冲区从左到右切成四段:
| |
- 已发送且已确认:可以回收缓冲区了;
- 已发送未确认:还在路上,丢了要重传;
- 可发送但未发送:在窗口内、应用层可以立刻塞进来发;
- 超出窗口、暂不可发:受窗口限制,得等窗口右移。
随着 ACK 到来,左边界(= 已确认位置)右移,整窗口向右"滑动",于是新的字节不断进入"可发送"区——这就是"滑动窗口"名字的由来。
发送窗口的实际大小 =
min(rwnd, cwnd)。rwnd是接收端告知的接收窗口(这一篇的主题,流量控制);cwnd是发送端自己估的拥塞窗口(下一篇的主题,拥塞控制)。两个约束谁小听谁的。
接收窗口
接收端这边也有一个窗口,本质是"接收缓冲区还剩多少空间"。它把这个值写进每个 ACK 的窗口字段回传给发送端——这就是 rwnd(receive window)。发送端据此调整自己的发送窗口,永远不让在途数据超过接收端能存下的量。
六、零窗口与窗口探测
接收端处理慢、缓冲区满时,会回一个 rwnd = 0 的 ACK,告诉发送端"先别发了"。发送端就停。
这里有个死锁风险:接收端后来腾出空间、发了个"窗口更新"的 ACK 想让发送端继续,但这个 ACK 如果丢了,双方就永久卡住——发送端在等"窗口打开",接收端在等"数据进来"。
TCP 用 持久计时器(persist timer)+ 窗口探测(window probe) 破局:发送端收到 0 窗口后启动持久计时器,到点就发一个只含 1 字节数据的探测段,逼接收端回一个带最新 rwnd 的 ACK。探测按指数退避反复进行(上限约 60 秒一次),直到窗口重新打开。这样即便窗口更新丢了,连接也不会永久挂死。
顺带:糊涂窗口综合征(SWS)
反过来,如果接收端每次只腾出几字节就急着通告、发送端也每次只发几字节,TCP 头的开销就会把有效吞吐吃光——这叫糊涂窗口综合征(Silly Window Syndrome) 。接收端和发送端各有 Clark 算法做规避:接收端在缓冲区没腾出"够大"的空间前,继续通告 rwnd=0;发送端也攒够一段才发。这些细节 TCP 实现都自动处理,应用层一般无感。
七、流量控制:收发两端之间的限速
把上面几节串起来,流量控制(flow control) 的全貌就清楚了:它是接收端用来限制发送端的机制,目的是"别发得比我处理得快,免得压垮我的缓冲区"。手段就是滑动窗口 + rwnd:
- 接收端通过 ACK 的窗口字段实时通报自己还剩多少缓冲;
- 发送端据此调整在途数据上限,永远不超过;
- 窗口为 0 时发送端停下,靠窗口探测等重新开放。
流量控制 vs 拥塞控制:别搞混
这俩名字像、都限制发送速率,但作用对象完全不同,是 TCP 窗口那 min(rwnd, cwnd) 里的两个独立分量:
| 流量控制(Flow Control) | 拥塞控制(Congestion Control) | |
|---|---|---|
| 保护谁 | 接收端(别压垮对端缓冲) | 网络(别压垮路由器 / 链路) |
| 谁感知 | 接收端(缓冲剩余空间) | 发送端(靠丢包 / 延迟推断) |
| 靠什么限速 | rwnd(ACK 里带) | cwnd(发送端内部维护) |
| 作用范围 | 点对点,两个端之间 | 整条路径上的所有连接共享 |
一句话:流量控制是"接收端喊停",拥塞控制是"发送端自律" 。下一篇就专门讲拥塞控制——TCP 怎么在没有网络层任何反馈的情况下,靠"试"和"退"把全网吞吐调到尽量高又不崩溃。
小结
- TCP 的可靠性三板斧是序号、确认、重传;序号针对字节,确认是累计确认(“下一个期望的字节”),SYN/FIN 各占一个序号。
- ARQ 有三种思想——停等、回退 N 步、选择重传;TCP 是"回退 N 的窗口 + 累计确认 + 快速 / 超时重传 + 可选 SACK"的工程混合体。
- RTO 紧跟 RTT 动态估算(RFC 6298 的 EWMA + 4 倍偏差),重传段不参与采样(Karn 算法),重传后 RTO 指数退避。
- 滑动窗口让"等确认"可以批量进行:发送窗口 =
min(rwnd, cwnd),随 ACK 到来向右滑动。 - 零窗口靠持久计时器 + 1 字节窗口探测破解死锁;糊涂窗口综合征靠 Clark 算法规避。
- 流量控制(收发之间,靠 rwnd)与拥塞控制(作用于网络,靠 cwnd)是两件事,分别约束窗口的两个分量。
下一篇:TCP/IP(八):TCP 拥塞控制 —— 慢启动、拥塞避免、快速重传 / 快速恢复,以及为什么 TCP 能"自发"地把全网的吞吐挤到一个不错的平衡点。
参考: