【计算机网络 | 传输层5:TCP 如何实现可靠传输?滑动窗口、累计确认与重传机制】

在五层网络模型中,传输层负责进程到进程的通信。前面已经从通用角度介绍了可靠数据传输:发送方给数据编号,接收方返回确认,数据丢失后由定时器触发重传;为了摆脱停止等待的低效率,还需要让多个数据同时"在路上"。

TCP 正是这些思想在互联网中的具体实现。不过,TCP 不是简单照搬某一种 ARQ 协议,而是围绕字节序号、滑动窗口、累计确认、超时重传、快速重传和选择确认建立了一套组合机制。

本文只沿一个方向分析:主机 A 向主机 B 发送数据,B 向 A 返回确认。由于 TCP 是全双工协议,反方向也有一套完全对称的发送窗口和接收窗口。

一、可靠传输要解决什么问题?

TCP 的下层是 IP。IP 尽力交付数据报,却不保证数据报一定到达、只到达一次或按发送顺序到达。因此,一段应用数据交给 TCP 后,途中可能出现:

  • 某个 TCP 报文段丢失;
  • 报文段发生比特差错;
  • 后发送的报文段先到达;
  • 确认报文段丢失,导致发送方误以为数据未到达;
  • 网络时延发生变化,固定的等待时间不再合适。

TCP 最终要向接收应用交付一条与发送应用写入内容一致的、无重复且有序的字节流。实现这一目标,需要解决三个基本问题:

  1. 哪些数据已经到达?------依靠字节序号和确认号。
  2. 一次可以连续发送多少数据?------依靠滑动窗口。
  3. 怎样发现并弥补丢失?------依靠超时重传、快速重传以及可选的 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. 发送缓存保存什么?

发送缓存主要保存两类数据:

  1. 应用进程已经写入、TCP 尚未发送的数据;
  2. TCP 已经发送、但尚未得到确认的数据。

已发送数据不能立即删除。即使它已经离开发送方,也可能在网络中丢失,发送方仍需依靠缓存中的副本完成重传。只有被累计确认覆盖的数据,才可以安全释放。

2. 接收缓存保存什么?

接收缓存通常保存:

  1. 已经按序到达、但应用进程尚未读取的数据;
  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,之后的传输情况如下:

  1. 1001~1500 丢失;
  2. 1501~2000 正确到达;
  3. 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 实现更接近选择重传的丢失恢复。它是一套面向字节流、经过长期演进的实际协议,而不是课堂模型中某一种协议的直接复制。

九、把完整过程串起来

现在用一次存在丢包的发送过程串联全部机制:

  1. 应用把字节流写入 A 的发送缓存。
  2. A 根据 rwndcwnd 和当前确认状态确定发送窗口。
  3. A 把窗口内的连续字节装入多个 TCP 报文段,连续发送,并保留未确认数据的副本。
  4. B 校验收到的报文段。出错的报文段被丢弃;正确数据进入接收缓存。
  5. B 对连续收到的最高字节范围进行累计确认,确认号指向下一个期望字节。
  6. 若中间出现缺口,B 对后续失序到达的数据继续返回相同确认号,并可通过 SACK 报告已收到的数据块。
  7. A 收到推进确认号的 ACK 后,释放已确认数据并让窗口后沿前移;新字节因此进入可发送范围。
  8. A 若收到足够的重复 ACK,可在超时前快速重传疑似丢失的报文段;若 ACK 始终没有到达,则由 RTO 触发超时重传。
  9. B 补齐缺口后,把连续字节按序交付应用,并发送新的累计确认。

这套过程把效率与可靠性结合起来:窗口允许流水线发送,累计确认降低反馈开销,缓存和重传弥补 IP 层可能造成的丢失、失序与重复。

十、常见误区

1. "窗口就是缓存"

不是。缓存是实际存放数据的内存空间,窗口是当前允许发送或接收的序号范围。窗口通常只是相关缓存所对应字节范围的一部分。

2. "ACK=1001 表示只确认了字节 1001"

恰好相反。ACK=1001 表示 1001 之前的连续字节已经收到,接下来期待 1001。

3. "收到后面的数据,确认号就应继续增大"

累计确认不能跨过缺口。即使更高序号的数据已经缓存,只要前面的字节缺失,确认号通常仍指向最早缺失的字节。

4. "发送窗口大小就是接收窗口大小"

不一定。通告窗口从接收方传到发送方存在时延,而且实际发送窗口还受拥塞窗口等状态限制。

5. "快速重传和超时重传是一回事"

不是。超时重传由 RTO 到期触发;快速重传通常由 3 个重复 ACK 触发,目的就是在定时器到期前发现并恢复丢失。

十一、总结

TCP 的可靠传输可以归纳为一条清晰的主线:

  • 给字节编号,用确认号表达下一个期望字节;
  • 用发送窗口允许多个报文段连续传输;
  • 用接收缓存处理按序和失序到达的数据;
  • 用累计确认推动窗口向前滑动;
  • 用动态 RTO 和超时重传提供最终保障;
  • 用重复 ACK 触发快速重传;
  • 用 SACK 补充失序数据块的信息,减少不必要的重传。

不过,这一切都有一个前提:通信双方必须先进入一致的连接状态,知道这条连接从哪些初始序号开始,并协商 MSS、窗口扩大和 SACK 等能力。TCP 是怎样完成这些准备工作的,正要从建立连接时的三次握手说起。

如果这篇文章对你有帮助,欢迎点赞、评论、关注、收藏。你们的支持是我前进的动力!

相关推荐
啊阿狸不会拉杆1 小时前
《计算机网络-自顶向下方法》5.5 SDN控制平面 读书笔记
计算机网络·平面
zfoo-framework1 小时前
kotlin技术栈各种 学习文档
学习
蓝速科技1 小时前
固定涉外场景台式翻译机选型与落地指南
网络·人工智能·自然语言处理·语音识别·技术分享
Java小学生丶1 小时前
[开源自荐] ZTShell,一个使用 Tauri 2 + Rust 全程由 AI 开发的跨平台桌面 SSH 工具
linux·网络·centos7·云服务器·开发技巧·资源分享·个人私货
是隼人1 小时前
buuctf-pwn wdb_2018_3rd_soEasy(ret2shellcode)题解(学习过程持续更新)
c语言·学习·安全·pwn入门·ctf入门
星 海3 小时前
【网络】VirtualBox网络模式
网络·虚拟机·virtualbox·vm
测试猿实操笔记9 小时前
DNS 通俗讲解:域名 ↔ IP 翻译
网络·网络协议·tcp/ip
mykj155110 小时前
英语单词APP解锁学习新方式,智能高效背单词
学习·英语单词app
zhengqweasd10 小时前
流量没跑满、带宽空闲,业务依然卡顿
运维·服务器·网络