选择重传协议:从滑动窗口到TCP SACK的可靠传输核心

📅 2026/8/1 6:08:28 👁️ 阅读次数 📝 编程学习
选择重传协议:从滑动窗口到TCP SACK的可靠传输核心

1. 从“停等”到“流水线”:为什么我们需要选择重传协议?

如果你写过网络编程,或者调试过TCP连接,大概率遇到过“丢包”和“重传”这两个词。在数据链路层和传输层,可靠传输是基石。最早的“停等协议”(Stop-and-Wait)简单直接:发一帧,等一个确认(ACK),收到后再发下一帧。这就像两个人用对讲机,你说一句“完毕”,必须等对方回一句“收到”,才能说下一句。在网络质量尚可的局域网里,这种方式勉强够用。

但一旦把场景放到广域网,或者带宽稍高、延迟稍大的环境里,停等协议的效率问题就暴露无遗。它的信道利用率低得可怜,计算公式是U = Td / (Td + RTT + Ta),其中Td是发送数据时间,RTT是往返时延,Ta是发送确认时间。在高速、高延迟的链路上,Td可能很短,但RTT很长,导致大部分时间信道都在空等,带宽被白白浪费。这就像用万吨巨轮一次只运一个集装箱,船跑得再快,大部分时间也在等装货卸货。

于是,“滑动窗口协议”登场了。它允许发送方在收到确认前,连续发送多个数据帧,将信道“管道化”,极大地提高了利用率。滑动窗口协议主要有两种:回退N帧(Go-Back-N, GBN)和选择重传(Selective Repeat, SR)。GBN协议相对简单,发送方维护一个发送窗口,按序发送;接收方只按序接收,一旦发现某帧出错或丢失,就丢弃该帧及之后所有帧,发送方需要从出错帧开始全部重传。这就像流水线上,一个零件装错了,整条线都得停下来,从这个零件开始全部返工。

而选择重传协议(SR)则更加“精明”和“宽容”。它的核心思想是:接收方可以缓存乱序到达但正确的帧,只要求发送方重传真正丢失或出错的那一帧。这解决了GBN协议中“一个错误,株连九族”的弊端,在错误率较高的信道中(比如早期的无线网络、卫星链路)优势明显。今天,虽然TCP等高层协议有更复杂的拥塞控制,但SR协议的思想——选择性确认与重传——依然是其重要组成部分。理解SR,不仅是理解计算机网络课本上的一个算法,更是理解现代可靠传输协议设计哲学的一把钥匙。

2. SR协议的核心机制:发送与接收窗口如何协同工作?

选择重传协议的精髓,在于发送方和接收方窗口的独立与协同。它们不再是GBN中那种强耦合的同步关系,而是各自维护状态,通过确认机制进行松耦合的交互。

2.1 发送方:不只是个无情的发送机器

发送方维护着三个关键的数据结构:

  1. 发送窗口(Send Window, SWND): 一个固定大小的、允许已发送但未被确认的帧的序号范围。假设窗口大小为N,序号从0开始,那么在任何时刻,发送方只能发送序号落在[send_base, send_base + N - 1]这个区间内的帧。send_base指向最早已发送但未确认的帧。
  2. 定时器(Timer): 在GBN中,通常只有一个定时器用于最早的未确认帧。而在SR中,每个已发送但未确认的帧都需要一个独立的定时器。这是SR能实现“选择性”重传的关键。当某个帧的定时器超时,发送方只重传这一帧,然后重启该帧的定时器。
  3. 确认状态缓存: 记录哪些帧已被确认(ACKed)。通常用一个布尔数组或位图来实现。

发送方的动作可以分解为以下几个驱动事件:

  • 上层调用(发送数据): 检查下一个要发送的序号next_seq_num是否在发送窗口内。如果在,则封装数据帧并发送,启动该帧的独立定时器,然后next_seq_num加一。如果不在,则缓存数据或通知上层窗口已满。
  • 收到ACK: 这是最有趣的部分。SR协议允许接收方发送累计确认,但更典型的是使用选择性确认(SACK)。假设收到对序号n的ACK。
    • 如果n在发送窗口内(即send_base <= n < send_base + N),发送方会标记该帧为已确认。
    • 如果n恰好等于send_base(即确认了窗口最左端的帧),那么发送方会将send_base向右移动到当前窗口内最小未确认的序号。这相当于窗口向前“滑动”了。
    • 无论n是否等于send_base,只要该帧被确认,就停止该帧的定时器
  • 定时器超时: 当序号为n的帧定时器超时,发送方仅重传这一帧,并重启该帧的定时器。这是与GBN最本质的区别。

这里有一个关键细节:窗口大小N和序号空间大小必须满足一定关系,否则会造成歧义。我们稍后在“序号空间与窗口大小的约束”一节会详细讨论。

2.2 接收方:一个有序的缓存管理员

接收方的逻辑比发送方更复杂一些,因为它要处理乱序到达的帧。

  1. 接收窗口(Receive Window, RWND): 同样是一个固定大小为N的窗口,期望接收的帧序号范围是[rcv_base, rcv_base + N - 1]。任何序号落在这个窗口内的帧都会被接收方处理,窗口外的帧会被直接丢弃(并可能引发一个ACK,用于帮助发送方同步)。
  2. 缓存区(Buffer): 一个能容纳至少N个帧的缓存区,用于存放乱序到达但正确的帧。
  3. 交付队列: 用于向上层按序交付数据。

接收方的动作分解:

  • 收到序号为n的帧
    • 情况A:帧在接收窗口内且正确(rcv_base <= n < rcv_base + N
      1. 发送一个针对该帧的ACK(ACK n)。
      2. 如果该帧是新的(之前没缓存过),则将其缓存。
      3. 如果该帧恰好是期望的帧(即n == rcv_base),接收方会检查缓存,rcv_base开始,连续地向上层交付所有已缓存的、按序的帧。每交付一帧,rcv_base就加一,接收窗口随之向右滑动。这个过程会一直持续,直到遇到第一个未缓存的序号为止。
    • 情况B:帧在接收窗口内但出错: 直接丢弃。在SR中,接收方通常不会发送否定确认(NAK),而是等待发送方该帧的定时器超时。当然,有些SR变种会使用NAK来加速重传。
    • 情况C:帧在接收窗口左侧(n < rcv_base: 这说明接收方已经交付了该帧,并且窗口已经滑过。此时接收方必须再发送一个ACK n。为什么?因为发送方可能丢失了之前对这个帧的ACK,这个重复的ACK能帮助发送方知道该帧已被正确接收,从而避免不必要的重传。
    • 情况D:帧在接收窗口右侧(n >= rcv_base + N: 帧已超出接收方当前能处理的范畴,直接丢弃。这通常意味着发送方和接收方窗口出现了不同步。

接收方的设计体现了SR的智能:它利用缓存容忍乱序,通过按序交付保证上层语义,并通过ACK机制(包括对旧帧的重复ACK)来积极协助发送方维护连接状态。

3. 关键问题深度剖析:序号、窗口与定时器

理解了基本流程,我们来看几个让SR协议真正稳定工作的关键设计点,这些也是面试和实际理解中的高频考点。

3.1 序号空间与窗口大小的约束:为什么N必须小于等于序号空间的一半?

这是一个经典的、必须搞清楚的约束条件。我们用一个反例来说明。假设序号空间很小,只有0, 1, 2, 3(即2比特,模4运算),而发送/接收窗口大小N = 3

考虑如下场景:

  1. 初始状态:发送方和接收方窗口都是[0, 1, 2]。发送方发送了帧0, 1, 2,接收方都收到了,并发送了ACK 0, ACK 1, ACK 2。但ACK 0和ACK 1在网络中丢失了,只有ACK 2到达。
  2. 发送方收到ACK 2,窗口滑动。现在发送窗口变为[3, 0, 1](因为模4运算,3之后是0)。send_base现在是3?不对,这里有个关键:send_base必须移动到最小的未确认帧。由于ACK 0和ACK 1丢失,发送方认为帧0和帧1未确认,所以send_base仍然是0,窗口实际上无法滑动!但为了继续通信,协议必须允许发送新数据。假设经过一段时间,发送方超时重传了帧0(旧的帧0)。
  3. 此时,接收方的窗口已经因为收到了0,1,2而滑动到了[3, 0, 1]。它现在期待的是帧3, 0, 1。
  4. 当重传的旧帧0到达时,它的序号0正好落在接收方当前窗口[3, 0, 1]内!接收方无法区分这个帧0是新的(属于下一个轮回)还是旧的重传。它会错误地将其作为新帧接收,导致数据错误。

问题的根源在于,当窗口大小N等于或大于序号空间大小时,窗口向前滑动后,新旧两个轮回的序号范围会产生重叠,接收方无法区分。因此,必须保证:发送窗口大小 + 接收窗口大小 <= 序号空间大小。在SR中,通常双方窗口大小相等,即N + N <= 2^k(k是序号比特数),所以N <= 2^(k-1)。也就是说,窗口最大不能超过序号范围的一半。

注意: 这是理论上的要求。在实际协议如TCP中,序号空间非常大(32位),窗口大小受其他因素(如接收缓冲区)限制,通常不会触及这个理论上限,但这个原理是设计的基础。

3.2 独立定时器 vs 单一定时器:管理开销与效率的权衡

GBN使用单一定时器管理最早未确认的帧,简单但粗放。SR为每个未确认帧维护独立定时器,精细但复杂。

  • 实现开销: 独立定时器意味着更多的数据结构(如链表或优先级队列来管理超时事件)和更频繁的定时器操作(启动、停止、检查)。在帧数量很多时,这是一个不可忽视的开销。
  • 重传精度与效率: 这是独立定时器带来的最大好处。假设窗口内帧1丢失,帧2, 3, 4…都正确到达。在GBN下,帧1超时会导致2,3,4…全部被重传,浪费带宽。在SR下,只有帧1被重传。接收方已经缓存了2,3,4…,一旦收到重传的帧1,就可以立即向上交付一批数据,时延更低。
  • 实战心得: 在实现SR协议仿真或理解其性能时,定时器管理是核心。一种常见的优化是使用一个“主定时器”配合时间戳。为每个发送的帧记录其发送时间戳。主定时器周期性检查所有未确认帧的时间戳,将那些(当前时间 - 发送时间 > RTO)的帧加入重传队列。这样避免了大量操作系统定时器资源的使用。

3.3 确认机制:ACK、NAK与SACK

  • 肯定确认(ACK): SR协议主要依赖ACK。接收方每收到一个在窗口内的新帧,就立即发送对该帧的ACK。这个ACK有两个作用:一是确认该帧收到,二是作为接收方窗口状态的隐式通告(告诉发送方“我期待rcv_base的帧”)。
  • 否定确认(NAK): 标准SR协议不一定需要NAK。没有NAK,丢包完全依靠发送方定时器超时来检测,这至少需要一个RTO的时间。加入NAK后,接收方一旦检测到序号间隙(比如收到了帧0和帧2,但没收到帧1),可以立即发送一个NAK 1,通知发送方“帧1可能丢了,快重传”。这可以显著减少丢包恢复时间,特别是在错误率高的链路上。许多实际实现(包括TCP的SACK选项)都包含了类似NAK的机制。
  • 选择性确认(SACK): 这是对基本ACK机制的强大增强。一个SACK报文可以同时确认多个不连续的数据块。例如,接收方收到了帧0, 1, 3, 4,它可以发送一个ACK,其中包含“SACK块”指明已收到[0-1][3-4]。发送方据此能精确知道只有帧2丢失了,无需等待超时就可以重传帧2,同时知道帧3和帧4无需重传。TCP的SACK选项正是SR思想在传输层的直接体现

4. 与回退N帧(GBN)协议的对比与选型思考

理解了SR,再回头看GBN,就能更深刻地体会其设计取舍。我们可以从几个维度对比:

特性维度回退N帧 (GBN)选择重传 (SR)
接收方缓存不缓存乱序帧,直接丢弃。缓存乱序但正确的帧。
确认机制累计确认。ACK(n)表示n及之前所有帧已正确接收。独立确认或选择性确认(SACK)。每个帧或数据块可被单独确认。
重传策略超时后,重传所有已发送但未确认的帧(从最早未确认帧开始)。超时后,仅重传超时的那个帧。
定时器一个,用于最早的未确认帧。每个已发送未确认的帧都有一个独立定时器(或等效机制)。
接收窗口大小固定为1。通常大于1,与发送窗口大小相等。
优点实现极其简单,接收方逻辑简单,定时器管理容易。在低错误率信道中效率尚可。带宽利用率高,尤其在高错误率、高带宽延迟积(BDP)的信道中。只重传错误帧,避免不必要的重传。
缺点一个错误,拖累全局。错误率高时,大量正确帧被无辜重传,效率急剧下降。不适合卫星、无线等链路。实现复杂。需要维护多个定时器、接收方需要缓存管理、序号空间要求更严格(窗口<=序号空间/2)。
适用场景错误率极低的可靠有线链路(如局域网),或对实现复杂度有严格限制的嵌入式环境。错误率较高的链路(无线网络、早期卫星通信)、带宽延迟积大的长肥管道。是现代可靠传输协议(如TCP)的基础。

选型思考: 这本质上是一个“简单性”与“效率”的权衡。在计算机早期,处理能力和内存非常宝贵,GBN的简单性极具吸引力。但随着硬件发展,复杂度不再是首要瓶颈,而网络带宽和延迟成为关键,SR的效率优势就凸显出来。TCP协议的设计就融合了这两种思想:默认使用累计确认(类似GBN),但通过SACK选项实现了选择重传的能力;它使用单个重传定时器,但通过快速重传(收到3个重复ACK即触发重传)机制部分实现了对单个丢包的选择性响应。可以说,TCP是一个混合体,在实践中根据网络状况动态调整策略。

5. 实战推演:一个完整的SR协议工作流程与故障模拟

让我们通过一个具体的例子,把SR协议的所有机制串起来。假设窗口大小N=4,序号空间8(0-7,模8运算),满足N <= 8/2

初始状态

  • 发送方:send_base = 0,next_seq_num = 0, 窗口[0,1,2,3]
  • 接收方:rcv_base = 0, 窗口[0,1,2,3],缓存空。

步骤1:正常发送与接收

  1. 发送方发送帧0,1,2,3,并为每个启动独立定时器。
  2. 接收方按序收到帧0。发送ACK 0,缓存帧0。发现rcv_base=0已收到,交付帧0给上层,rcv_base变为1,窗口滑动至[1,2,3,4]
  3. 接收方收到帧2(乱序)。发送ACK 2,缓存帧2。rcv_base仍是1(因为帧1没到),无法交付。
  4. 接收方收到帧1。发送ACK 1,缓存帧1。此时缓存中有帧1,2。检查rcv_base=1,已收到,于是连续交付帧1和帧2,rcv_base变为3,窗口滑动至[3,4,5,6]
  5. 接收方收到帧3。发送ACK 3,缓存帧3。交付帧3,rcv_base变为4,窗口滑动至[4,5,6,7]
  6. 发送方陆续收到ACK 0,ACK 1,ACK 2,ACK 3。窗口滑动,send_base变为4,现在可以发送帧4,5,6,7。

步骤2:模拟帧丢失与选择性重传

  1. 发送方发送帧4,5,6,7。
  2. 假设帧5在网络中丢失。帧4,6,7正确到达接收方。
  3. 接收方收到帧4:发送ACK 4,交付,rcv_base=5,窗口[5,6,7,0](模8)。
  4. 接收方收到帧6:发送ACK 6,缓存帧6(因为期望的是帧5)。
  5. 接收方收到帧7:发送ACK 7,缓存帧7。
  6. 发送方收到ACK 4ACK 6ACK 7。它知道帧4,6,7已收到,停止它们的定时器。但帧5的ACK始终没来。
  7. 帧5的定时器超时。发送方仅重传帧5,并重启帧5的定时器。
  8. 接收方收到重传的帧5。发送ACK 5。此时,它发现rcv_base=5已收到,且缓存中有帧6,7。于是它连续交付帧5,6,7,rcv_base变为0(模8),窗口滑动至[0,1,2,3]
  9. 发送方收到ACK 5,停止帧5的定时器,窗口可以继续滑动。

步骤3:模拟ACK丢失与重复ACK

  1. 接续步骤2后,发送方发送新的一批帧0,1,2,3(注意序号轮回)。
  2. 假设帧0的ACK丢失了。
  3. 发送方帧0的定时器超时,重传帧0。
  4. 此时接收方的窗口是[0,1,2,3],它收到了这个重传的帧0。它无法判断这是新的帧0还是旧的重传。但根据SR规则,它必须检查这是否是重复帧。实现上,接收方需要维护一个“已接收并确认”的最高序号信息。它发现帧0(序号0)小于当前的rcv_base(此时rcv_base可能已经大于0,因为收到了更新的帧),或者它发现自己已经缓存过帧0。于是,接收方再次发送一个ACK 0
  5. 发送方收到这个重复的ACK 0。它知道接收方已经收到了帧0(可能是之前ACK丢了),于是它可以安全地忽略这个重传帧带来的影响,并确认帧0已收到。这避免了发送方错误地认为接收方没收到帧0而陷入死循环。

这个推演展示了SR协议如何处理乱序、丢包、ACK丢失等各种异常情况,其核心在于缓存、独立确认和重复ACK的巧妙运用。

6. 从理论到实践:SR思想在现代网络协议中的体现

虽然教科书上的SR是一个数据链路层协议,但其思想已经深深嵌入现代网络协议栈,尤其是在传输层。

TCP协议中的SR元素

  1. 选择性确认(SACK): 如前所述,这是最直接的体现。通过在TCP选项字段中携带SACK块,接收方可以告知发送方多个不连续的数据段已收到,使发送方能进行选择性重传。
  2. 快速重传与快速恢复: 当发送方连续收到3个对同一序号的重复ACK(Dup-ACK)时,它推断该序号的数据段可能丢失,于是不等超时,立即重传该数据段。这本质上是利用重复ACK作为一种隐式的、负面的选择信号,实现了类似SR的快速重传。随后的“快速恢复”算法调整拥塞窗口,也体现了对单个丢包的选择性处理,而非GBN式的全面回退。
  3. 乱序缓存与按序交付: TCP接收端有接收缓冲区,可以缓存乱序到达的报文段,等待缺失的报文段到达后,再按序交付给应用层。这完全继承了SR接收方的核心逻辑。

QUIC协议中的强化: 谷歌提出的QUIC(基于UDP)协议,将SR思想更进一步。它在传输层原生支持了更细粒度的、基于数据流的可靠传输,每个数据包都有独立的包号,重传机制天然就是选择性的,避免了TCP中因序列号重用可能带来的歧义问题(在高速长连接下),重传效率更高。

实战心得与注意事项

  • 窗口大小的动态调整: 在实际协议中(如TCP),窗口大小不是固定的,而是动态变化的,受限于接收方通告窗口(rwnd)和拥塞窗口(cwnd)。理解固定窗口的SR是基础,但更要明白在实际中,窗口是流动的,协议需要同时处理可靠传输和流量控制、拥塞控制。
  • 定时器管理的优化: 为每个数据包维护一个硬件定时器是不现实的。实际中,TCP使用一个重传定时器(RTO),但其超时时间是通过动态测量RTT来计算的。当需要重传时,它可能采用类似“批量重传”或基于SACK信息精确重传的策略。在你自己实现可靠UDP时,可以采用一个时间轮或优先队列来管理多个虚拟定时器。
  • 应用场景选择: 如果你在设计一个内部系统的通信模块,网络环境可控(低错误率、低延迟),那么实现一个简单的GBN变种可能更省心。但如果你面对的是公网、移动网络等不稳定环境,那么引入选择性重传(哪怕是简化版)对提升性能至关重要。很多时候,不必完全实现教科书式的SR,可以取其精髓:例如,实现乱序缓存和按序交付,但重传策略可以简化,或者使用一个主定时器配合SACK信息来实现选择性重传。

理解选择重传协议,不仅仅是记住它的规则,更是理解一种“在不可靠的媒介上实现可靠通信”的设计哲学:通过增加接收端的复杂性(缓存)和智能(选择性确认),来换取信道利用率的本质提升,这是一种典型的以空间(缓存)和计算复杂度换取时间和效率的工程权衡。下次当你用Wireshark抓包看到TCP报文里的SACK选项时,你就会会心一笑,知道这正是数据链路层那个古老而精妙的选择重传思想,在互联网的血管中继续跳动。