
在五层网络模型中,传输层负责进程到进程的通信。前面已经从通用角度介绍了可靠数据传输:发送方给数据编号,接收方返回确认,数据丢失后由定时器触发重传;为了摆脱停止等待的低效率,还需要让多个数据同时"在路上"。
TCP 正是这些思想在互联网中的具体实现。不过,TCP 不是简单照搬某一种 ARQ 协议,而是围绕字节序号、滑动窗口、累计确认、超时重传、快速重传和选择确认建立了一套组合机制。
本文只沿一个方向分析:主机 A 向主机 B 发送数据,B 向 A 返回确认。由于 TCP 是全双工协议,反方向也有一套完全对称的发送窗口和接收窗口。
一、可靠传输要解决什么问题?
TCP 的下层是 IP。IP 尽力交付数据报,却不保证数据报一定到达、只到达一次或按发送顺序到达。因此,一段应用数据交给 TCP 后,途中可能出现:
- 某个 TCP 报文段丢失;
- 报文段发生比特差错;
- 后发送的报文段先到达;
- 确认报文段丢失,导致发送方误以为数据未到达;
- 网络时延发生变化,固定的等待时间不再合适。
TCP 最终要向接收应用交付一条与发送应用写入内容一致的、无重复且有序的字节流。实现这一目标,需要解决三个基本问题:
- 哪些数据已经到达?------依靠字节序号和确认号。
- 一次可以连续发送多少数据?------依靠滑动窗口。
- 怎样发现并弥补丢失?------依靠超时重传、快速重传以及可选的 SACK。
二、TCP 的滑动窗口为什么以字节为单位?
TCP 面向字节流。应用进程交下来的数据会被视为连续字节,每个字节都拥有序号;TCP 再把一段连续字节装入报文段发送。因此:
- 序号和窗口以字节为单位;
- 报文段是实际传输载体;
- 发送窗口大小不等于一个报文段的长度。
例如,一个发送窗口允许发送 4000 字节,而当前 MSS 为 1460 字节,这批数据通常需要由多个 TCP 报文段承载。窗口表示"当前允许在未确认状态下保留多少字节",MSS 则限制单个报文段通常能携带多少 TCP 数据。
1. 发送窗口中的四段区域
假设发送方当前维护一段连续的字节序号,可以把整个字节流划分为四个区域:
text
已确认 | 已发送但未确认 | 允许发送但尚未发送 | 当前不允许发送
^ ^
窗口后沿 窗口前沿
其中,发送窗口覆盖中间两部分:
text
发送窗口 = 已发送但未确认 + 允许发送但尚未发送
- 已确认:接收方已经累计确认,可从发送缓存中释放。
- 已发送但未确认:数据已经进入网络,但发送方必须暂时保留副本,以便必要时重传。
- 允许发送但尚未发送:当前可以继续装入报文段发送的数据。
- 当前不允许发送:位于窗口之外,要等窗口向前滑动或扩大后才能发送。
窗口后沿通常随着新确认到达而前移。窗口前沿取决于后沿位置和当前可用窗口大小。一般不希望已经通告的窗口前沿向后收缩,因为这可能让发送方刚刚获得的发送许可失效;实际处理还必须遵循 TCP 对窗口更新的规则。
2. 一个具体的窗口例子
假设发送窗口覆盖字节 31~50,其中:
- 31~40 已经发送,但尚未确认;
- 41~50 尚未发送,但允许发送。
发送方收到 ACK=41,表示 40 及之前的所有连续字节都已收到,现在期待字节 41。于是窗口后沿可以从 31 移到 41,31~40 对应的数据也不再需要为重传而保留。
如果确认报文段还通告接收窗口为 15 字节,并暂不考虑拥塞窗口,那么新的发送许可可从 41 开始覆盖 15 个字节,即 41~55。发送窗口就这样向前"滑动"了。
3. 发送窗口由谁决定?
接收方会在 TCP 首部的窗口字段中通告自己的可用接收空间,记为 rwnd。发送方还要考虑网络承载能力,由拥塞控制维护拥塞窗口 cwnd。因此,发送方实际可用的发送窗口不能简单等同于接收方通告的窗口,通常受下面的上限约束:
text
实际发送窗口上限 = min(rwnd, cwnd)
rwnd 防止压垮接收方,cwnd 防止向网络注入过多数据。本篇聚焦可靠传输,只需先记住这两个约束;拥塞窗口如何变化将在拥塞控制部分展开。
三、接收窗口、缓存与按序交付
滑动窗口是序号空间中的逻辑范围,不是一块真正存放数据的内存。真正保存字节的是发送缓存和接收缓存。
1. 发送缓存保存什么?
发送缓存主要保存两类数据:
- 应用进程已经写入、TCP 尚未发送的数据;
- TCP 已经发送、但尚未得到确认的数据。
已发送数据不能立即删除。即使它已经离开发送方,也可能在网络中丢失,发送方仍需依靠缓存中的副本完成重传。只有被累计确认覆盖的数据,才可以安全释放。
2. 接收缓存保存什么?
接收缓存通常保存:
- 已经按序到达、但应用进程尚未读取的数据;
- 已正确到达但暂时失序的数据。
接收方只向应用进程交付连续、有序的字节流。假设它已经收到 1~1000,又先收到了 1501~2000,那么后一段数据可以暂存在缓存中,但 1001~1500 的缺口补齐之前,不能把 1501 开始的字节越过缺口按正常顺序交给应用。
TCP 标准并未要求所有实现必须以完全相同的方式处理失序数据。实际实现通常会缓存接收窗口内的失序字节,以减少不必要的重复传输。如果接收缓存逐渐被占满,接收方通告的 rwnd 会缩小;应用读取数据、释放空间后,rwnd 又可以增大。
四、累计确认:一个 ACK 能确认多少数据?
TCP 首部中的确认号表示:接收方下一步期望收到的字节序号。
若 B 返回 ACK=N,含义不是"只收到了第 N 个字节",而是:
text
序号小于 N 的连续字节均已正确收到,现在期待序号 N。
这就是累计确认。一个 ACK 可以一次确认一整段连续字节,因此确认报文不必和每个数据报文段严格一一对应。
1. 出现缺口时,确认号不会越过去
假设 B 已按序收到 1~1000,之后的传输情况如下:
- 1001~1500 丢失;
- 1501~2000 正确到达;
- 2001~2500 正确到达。
虽然 B 已经看到了更靠后的数据,但连续字节流仍停在 1000,所以它返回的确认号仍是 ACK=1001。后续每收到一个越过缺口的报文段,都可能再次发送同样的 ACK,这些就是重复 ACK。
当 1001~1500 最终到达后,如果 1501~2500 已缓存在接收方,连续范围会一次扩展到 2500,确认号便可直接前进为 ACK=2501。
2. 延迟确认与捎带确认
接收方不一定每收到一个报文段就立即单独发送 ACK。它可以短暂延迟确认,等待后续连续数据,以一个 ACK 累计确认更多字节;在反方向也有数据要发送时,还可以把确认信息放进数据报文段中,这叫捎带确认。
延迟不能无限持续,否则发送方可能误判数据丢失并触发重传。经典 TCP 规范要求延迟确认通常不应超过 500 ms,具体实现往往采用更短的延迟和更细化的策略。遇到失序数据或需要及时反馈缺口时,接收方通常会立即返回 ACK。
五、超时重传:ACK 一直不来怎么办?
发送方必须为尚未确认的数据维护重传定时。若在超时重传时间 RTO 内没有收到能够推进确认号的 ACK,就重传相应的未确认数据。
RTO 不能设置得过短,否则只是网络暂时变慢也会引发多余重传;也不能设置得过长,否则真正丢包后要等待很久。由于互联网中的路径、排队和负载不断变化,TCP 根据测得的往返时间动态估算 RTO,而不是使用一个永久固定值。
一种常见的表达方式是:
text
EstimatedRTT = (1 - α) × EstimatedRTT + α × SampleRTT
DevRTT = (1 - β) × DevRTT + β × |SampleRTT - EstimatedRTT|
RTO = EstimatedRTT + 4 × DevRTT
其中:
SampleRTT:某次发送到收到相应确认所测得的往返时间,单位通常为毫秒;EstimatedRTT:平滑后的 RTT 估计值;DevRTT:RTT 波动程度的平滑估计;α和β:平滑系数,经典推荐值分别为 1/8 和 1/4;RTO:超时重传时间,与 RTT 使用相同时间单位。
公式的核心不是死记参数,而是理解:网络时延越不稳定,RTO 就要留出越大的安全余量。
重传后的 RTT 为什么不好测?
某个报文段超时后被重新发送,随后 ACK 到达。此时发送方无法仅凭普通累计 ACK 判断:这个 ACK 是对原报文段的迟到确认,还是对重传报文段的确认?如果直接拿它计算 SampleRTT,结果可能失真。
Karn 算法的基本做法是:不使用发生过重传的报文段来采集 RTT 样本。同时,连续超时时通常采用指数退避,例如每次把 RTO 加倍,以适应网络时延突然显著增大的情况,也避免频繁重传进一步加重网络负担。
六、快速重传:不必每次都等到超时
RTO 是可靠性的最后保障,但等待超时可能比较慢。重复 ACK 能让发送方更早发现疑似丢包。
继续使用前面的例子:1001~1500 丢失,而后面的三个报文段先后到达。接收方每次都返回 ACK=1001,告诉发送方缺口仍从 1001 开始。发送方收到 3 个重复 ACK 后,通常会认为从 1001 开始的报文段很可能已经丢失,于是在 RTO 到期前就重传它,这就是快速重传。
需要注意两点:
- 重复 ACK 不是新的累计确认,它没有推动窗口后沿;
- 快速重传同时也是拥塞控制的重要信号,但窗口如何降速属于后续拥塞控制的内容。
因此,把 TCP 概括成"只有超时才会重传"并不准确。超时重传和快速重传是两条不同的丢失恢复路径。
七、SACK:告诉发送方哪些失序块已经收到
仅靠累计确认,发送方知道连续数据在何处中断,却不知道缺口后面的哪些数据已经到达。选择确认 SACK 是对这一信息不足的补充。
假设接收方已经连续收到 1~1000,同时还缓存了两个失序字节块:
text
1501~3000
3501~4500
普通确认号仍然是 ACK=1001,因为连续字节流在 1001 处存在缺口。若双方在建立连接时协商允许使用 SACK,接收方还可以通过 TCP 选项报告这些已收到的不连续字节块。SACK 块使用左闭右开的边界表示:
text
[1501, 3001)
[3501, 4501)
也就是说,右边界本身不属于该数据块。发送方据此可以更有针对性地重传缺失范围,避免再次发送接收方已经缓存的数据。
SACK 不会取代累计确认号。确认号仍表示连续收到的字节前缀,SACK 只是额外说明前方哪些离散区间也已到达。SACK 提供信息,具体怎样安排重传则由发送方实现决定。
八、TCP 是 GBN 还是 SR?
把 TCP 强行归类为纯粹的回退 N 步协议(GBN)或选择重传协议(SR),都会遗漏它的重要特征。
| 对比项 | GBN | SR | TCP 的典型做法 |
|---|---|---|---|
| 确认方式 | 累计确认 | 分别确认 | 以累计确认为基础,可增加 SACK 信息 |
| 失序数据 | 通常丢弃 | 接收并缓存 | 实现通常缓存接收窗口内的失序数据 |
| 丢失恢复 | 从缺失分组起回退重传 | 只重传缺失分组 | 超时、快速重传与 SACK 等机制组合 |
| 序号单位 | 分组 | 分组 | 字节 |
| 定时机制 | 常围绕最早未确认分组 | 各分组独立逻辑定时 | 通常维护重传定时器并管理最早未确认数据 |
TCP 借鉴了流水线和滑动窗口的思想,具有 GBN 式累计确认的特征,也能借助缓存、快速重传和 SACK 实现更接近选择重传的丢失恢复。它是一套面向字节流、经过长期演进的实际协议,而不是课堂模型中某一种协议的直接复制。
九、把完整过程串起来
现在用一次存在丢包的发送过程串联全部机制:
- 应用把字节流写入 A 的发送缓存。
- A 根据
rwnd、cwnd和当前确认状态确定发送窗口。 - A 把窗口内的连续字节装入多个 TCP 报文段,连续发送,并保留未确认数据的副本。
- B 校验收到的报文段。出错的报文段被丢弃;正确数据进入接收缓存。
- B 对连续收到的最高字节范围进行累计确认,确认号指向下一个期望字节。
- 若中间出现缺口,B 对后续失序到达的数据继续返回相同确认号,并可通过 SACK 报告已收到的数据块。
- A 收到推进确认号的 ACK 后,释放已确认数据并让窗口后沿前移;新字节因此进入可发送范围。
- A 若收到足够的重复 ACK,可在超时前快速重传疑似丢失的报文段;若 ACK 始终没有到达,则由 RTO 触发超时重传。
- B 补齐缺口后,把连续字节按序交付应用,并发送新的累计确认。
这套过程把效率与可靠性结合起来:窗口允许流水线发送,累计确认降低反馈开销,缓存和重传弥补 IP 层可能造成的丢失、失序与重复。
十、常见误区
1. "窗口就是缓存"
不是。缓存是实际存放数据的内存空间,窗口是当前允许发送或接收的序号范围。窗口通常只是相关缓存所对应字节范围的一部分。
2. "ACK=1001 表示只确认了字节 1001"
恰好相反。ACK=1001 表示 1001 之前的连续字节已经收到,接下来期待 1001。
3. "收到后面的数据,确认号就应继续增大"
累计确认不能跨过缺口。即使更高序号的数据已经缓存,只要前面的字节缺失,确认号通常仍指向最早缺失的字节。
4. "发送窗口大小就是接收窗口大小"
不一定。通告窗口从接收方传到发送方存在时延,而且实际发送窗口还受拥塞窗口等状态限制。
5. "快速重传和超时重传是一回事"
不是。超时重传由 RTO 到期触发;快速重传通常由 3 个重复 ACK 触发,目的就是在定时器到期前发现并恢复丢失。
十一、总结
TCP 的可靠传输可以归纳为一条清晰的主线:
- 给字节编号,用确认号表达下一个期望字节;
- 用发送窗口允许多个报文段连续传输;
- 用接收缓存处理按序和失序到达的数据;
- 用累计确认推动窗口向前滑动;
- 用动态 RTO 和超时重传提供最终保障;
- 用重复 ACK 触发快速重传;
- 用 SACK 补充失序数据块的信息,减少不必要的重传。
不过,这一切都有一个前提:通信双方必须先进入一致的连接状态,知道这条连接从哪些初始序号开始,并协商 MSS、窗口扩大和 SACK 等能力。TCP 是怎样完成这些准备工作的,正要从建立连接时的三次握手说起。
如果这篇文章对你有帮助,欢迎点赞、评论、关注、收藏。你们的支持是我前进的动力!