TCP 可靠数据传输:滑动窗口、超时重传与 SACK
本文是《计算机网络学习笔记》系列的第三篇。IP 层尽力而为,不保证送达,不保证顺序,不保证不重复——而 TCP 在这之上,构建出了对应用层"完全透明"的可靠字节流。本文就来讲清楚这件事是怎么做到的。
可靠传输要解决哪些问题?
在不可靠的网络上,数据包可能遇到以下情况:
| 问题 | 表现 |
|---|---|
| 丢包 | 数据包发出去,目的地没收到 |
| 乱序 | 包 3 先到,包 2 后到 |
| 重复 | 同一个包被收到多次 |
| 损坏 | 包在传输中发生了比特翻转 |
TCP 的可靠传输机制,正是为了逐一解决这四个问题而设计的。
一、从"停-等"到滑动窗口:效率从哪里来?
停-等协议(Stop-and-Wait)
最朴素的可靠传输方式:发送一个包,等到收到 ACK,再发下一个。
发送方: [包1] ——> 等待 ——————> [包2] ——> 等待 ——> ...
↑ ACK1 ↑ ACK2可靠性没问题,但效率极低。假如 RTT 是 100ms,每个包的发送时间是 1ms,那么 99% 的时间都在"等",链路利用率不足 1%。在洲际网络(RTT 动辄 150ms+)上,这几乎是灾难性的。
滑动窗口(Sliding Window)
解法很直观:在等待 ACK 的时间里,继续发包。
发送方维护一个"窗口",窗口内的包可以不等 ACK、连续发出去:

每收到一个 ACK,窗口向右滑动一格,允许再发一个新包。这就是"滑动"的来由。
滑动窗口的核心价值:让网络管道始终"满"着数据,而不是走走停停。在同样的 RTT 下,可以把链路利用率提升数十乃至数百倍。
二、两种经典的滑动窗口算法
回退 N 步(Go-Back-N,GBN)
接收方窗口大小为 1,即只接受按顺序到达的包,乱序的直接丢弃。
以发送 0、1、2、3、4、5 为例,假设窗口大小 N=4,包 2 在传输中丢失:

特点:
- 实现简单,接收方无需缓存
- 缺点明显:一个包丢了,后面所有已发的包都要重传,严重浪费带宽
选择重传(Selective Repeat,SR)
接收方窗口大小也为 N,乱序到达的包先缓存下来,只要求重传真正丢失的那一个。
以同样场景为例,包 2 丢失:

特点:
- 精准重传,带宽利用率高
- 缺点:接收方需要缓存乱序包,实现复杂
三、真实的 TCP:GBN 和 SR 的结合体
现实中的 TCP 并非完全照搬 GBN 或 SR,而是融合了两者的优点,同时引入了 SACK 作为"决定性武器"。
像 GBN 的地方:累计确认
TCP 的 ACK 采用累计确认:
ack = N 表示"序号 N 之前的所有字节我全收到了,下一个请从 N 开始发"一个 ACK 可以确认前面所有未确认的字节,不需要逐一回复。
一个常被忽略的细节:ack = 100 表示 100 之前(即 0~99)的数据已收到,100 本身还没收到。这个"左开右闭"的语义在分析序号变化时很重要。
像 SR 的地方:乱序缓存
接收方不会像 GBN 那样丢弃乱序包,而是先缓存起来:
发送方发出:1 2 3 4 5
其中 2 丢了,3、4、5 顺利到达
GBN 接收方:丢弃 3、4、5,反复回复 ACK1,等 2 重传
TCP 接收方:3、4、5 存进接收缓冲区,等 2 补上来,一气儿交付 1~5把重传工作量从"重传 2、3、4、5"减少到了"只重传 2"。
SACK:解决累计确认的信息盲区
上面的场景中,TCP 缓存了 3、4、5,但 ACK 字段只能填 2(因为累计确认的规则)。发送方看到重复的 ACK2,会困惑:"是只有 2 丢了,还是 2 之后的全丢了?"
SACK(Selective Acknowledgment,选择性确认) 解决了这个问题。
开启 SACK 后(在握手时协商),接收方可以在 ACK 报文的选项字段中额外附上已收到但不连续的区间:
ACK = 2 ← 主确认号:还在等 2(累计确认语义不变)
SACK = [3, 6) ← 额外告知:3、4、5 我已经收到了发送方看到这个,就明白只有 2 是真的丢了,精准重传 2,3、4、5 不需要动。
SACK 是在 1996 年由 RFC 2018 标准化的,是现代 TCP 性能优化不可缺少的一环。在 Linux 上,SACK 默认开启(net.ipv4.tcp_sack = 1)。四、超时重传与 RTT 的估算
触发重传的两种情形
| 触发条件 | 名称 | 说明 |
|---|---|---|
| 计时器超时 | 超时重传 | 发出包后等了太久没收到 ACK |
| 收到 3 个重复 ACK | 快速重传 | 不等超时,立即重传 |
两种机制互为补充,超时重传兜底,快速重传抢先。
RTT 是怎么算出来的?
超时重传的计时器要设置多长?太短会误触发(网络正常,只是 ACK 刚好慢了一点),太长会让真正的丢包等太久。因此,TCP 需要动态估算当前网络的往返时延(RTT)。
第一步:采集样本
每发出一个非重传包,记录发出时间 t0;收到对应 ACK 时,记录时间 t1:
SampleRTT = t1 - t0为什么排除重传包?因为重传包的 ACK 有歧义——它是对第一次发送的确认,还是对重传的确认?无法区分,所以不采样(这叫 Karn 算法)。
第二步:平滑估算(EWMA)
单次采样值波动很大,用指数加权移动平均来滤波:
EstimatedRTT = (1 - α) × EstimatedRTT + α × SampleRTTα 通常取 1/8,意思是:新采样只占 1/8 的权重,历史经验占 7/8。这样 RTT 的估算值会"稳",不会因为一次网络抖动就大起大落。
第三步:衡量抖动(DevRTT)
光有均值还不够,还要知道网络的"脾气"——波动有多大:
DevRTT = (1 - β) × DevRTT + β × |SampleRTT - EstimatedRTT|β 通常取 1/4,DevRTT 越大说明网络越不稳定。
第四步:计算超时间隔
TimeoutInterval = EstimatedRTT + 4 × DevRTT加上 4 × DevRTT 是为了留出安全边距,确保在网络抖动时不会误触发重传。网络越稳定(DevRTT 越小),超时间隔越紧凑;网络越抖动,超时间隔越宽松。
五、快速重传:3 个冗余 ACK 就够了
超时重传的缺点是延迟高——从丢包到触发,至少要等一个完整的超时间隔(通常在几百毫秒到几秒之间)。
快速重传(Fast Retransmit)是一种更灵敏的检测机制,不依赖超时。
工作原理:
发送方发出: 1 2 3 4 5
↓ 2 丢了
接收方收到顺序:1 3 4 5
接收方回复:
收到 1 → ACK 2 (正常)
收到 3 → ACK 2 (期望 2,但收到 3,说明有问题!)
收到 4 → ACK 2 (重复第 2 次)
收到 5 → ACK 2 (重复第 3 次)
发送方:收到 3 个重复的 ACK2 ← 触发快速重传,立即重发 2
接收方:收到补来的 2 → 缓存里已经有 3、4、5 → 回复 ACK 6(累计确认)为什么是 3 个? 1 个重复 ACK 可能只是乱序(包 3 先于 2 到达),2 个也还说不准,3 个重复 ACK 基本可以确认是真丢包而非乱序。3 是经验值,在误报率和响应速度之间取得的折中。
六、ACK 的发送策略:三条潜规则
ACK 不是收到一个包就立刻回一个——TCP 有一套节省带宽的 ACK 发送策略(RFC 5681):
规则一:正常情况——能蹭车就蹭车
接收方收到期望的包后,启动一个短暂的延迟定时器(50~200ms),看看自己是否也有数据要发给对方。如果有,ACK 就附在数据报文里"顺风车"一起发出,这叫捎带确认(Piggybacking)。
如果蹭不到车,就等凑满两个未发 ACK 再发;如果定时器超时,也立刻发出。
结果:减少了大量只携带 ACK、不含任何数据的纯 ACK 包,一定程度上节省了带宽。
规则二:收到乱序包——立刻大声抱怨
接收方收到乱序的包(比如期望 2,却收到了 3),立即发出重复 ACK,不等也不攒,催促发送方赶紧重传缺失的包。这正是快速重传能快速响应的前提。
规则三:缺口填上了——立刻汇报
等到重传包补上缺口(比如 2 终于来了),接收方立即发出 ACK,同步最新的确认状态,让发送方知道可以继续推进发送窗口了。
七、序号与消耗规则速查
| 报文类型 | 是否消耗序号 | 说明 |
|---|---|---|
| 数据报文 | ✅ 消耗 | 每个字节消耗 1 个序号 |
| SYN 报文 | ✅ 消耗 1 个 | 即使不带数据 |
| FIN 报文 | ✅ 消耗 1 个 | 即使不带数据 |
| 纯 ACK | ❌ 不消耗 | 只是确认,不占序号 |
这条规则保证了 SYN 和 FIN 各能被精确地 ACK 确认——三次握手和四次挥手中序号的变化(比如 ack = isn + 1)都源于此。
总结
| 机制 | 解决的问题 | 关键点 |
|---|---|---|
| 滑动窗口 | 效率低(停-等) | 窗口内的包连续发出,不等 ACK |
| 累计确认 | 逐包确认的带宽浪费 | 一个 ACK 确认此前所有字节 |
| 乱序缓存 | GBN 的无效重传 | 乱序包入缓冲区,等缺口填上再交付 |
| SACK | 累计确认的信息盲区 | 额外附上已收到的不连续区间 |
| RTT 估算 | 超时时间难以确定 | EWMA 平滑 + 抖动冗余 |
| 超时重传 | 数据包或 ACK 丢失 | 计时器到期,重发未确认的包 |
| 快速重传 | 超时重传延迟太高 | 3 个重复 ACK 立刻触发,不等超时 |
| ACK 延迟策略 | 纯 ACK 包浪费带宽 | 捎带、攒两个、乱序立即发 |
这些机制相互配合,共同让 TCP 在"尽力而为"的 IP 网络之上,构建出了应用层可以完全信赖的可靠字节流。
本文涉及的 SACK、序号等协议字段细节,参见系列第一篇 《TCP 协议格式详解》。
三次握手与四次挥手中序号的具体变化,参见系列第二篇 《TCP 三次握手与四次挥手》。
参考资料:《计算机网络:自顶向下方法》