4. 选择性重传(SR)协议:让每一份努力都不白费

讲完 GBN,咱们来看看它的升级版——选择性重传(Selective Repeat,SR)。

说实话,我第一次在项目中实现 GBN 时,心里就犯嘀咕:就因为一个包丢了,后面全得重传?这也太浪费带宽了。尤其是高延迟链路,比如卫星通信,一个 RTT 几百毫秒,重传一堆已经正确收到的包,简直是灾难。

SR 协议就是来解决这个问题的。它的核心思想很简单:谁丢了就重传谁,不连坐。

4.1 SR 发送方状态机:精细化管理每个包

SR 的发送方不再像 GBN 那样只有一个「基序号」的概念。它需要为每个在途的包维护独立的状态。我习惯用一个结构体数组来管理发送窗口:

typedef struct {
    uint32_t seq_num;      // 序列号
    bool     sent;         // 是否已发送
    bool     acked;        // 是否已确认
    uint32_t timer;        // 超时计时器(毫秒)
    uint8_t  retx_count;   // 重传次数
} sr_packet_t;

#define WINDOW_SIZE 16
sr_packet_t send_window[WINDOW_SIZE];

发送方的状态机有这几个关键事件:

  • 从上层接收数据:如果窗口没满,分配序列号,标记 sent=true,启动计时器。
  • 收到 ACK:找到对应序列号的包,标记 acked=true,停止计时器。
  • 超时:只重传那个超时的包,其他包纹丝不动。
  • 窗口滑动:只有当窗口最底部的连续包都被确认了,窗口才能向前滑动。

关键区别:GBN 的窗口滑动条件是「收到 base 的 ACK」;SR 的窗口滑动条件是「base 及之前的所有包都被确认」。这导致 SR 的窗口滑动逻辑更复杂,但效率更高。

4.2 SR 接收方状态机:缓存与重排序的艺术

接收方是 SR 协议最精彩的部分。它必须能接收乱序包,并缓存起来,等前面的包到齐后再一起交给上层。

我在项目中遇到过一个问题:接收方缓存满了怎么办?嗯,这其实是个经典的设计取舍。

接收方的核心数据结构:

typedef struct {
    uint32_t seq_num;
    bool     received;     // 是否已收到
    uint8_t* data;         // 数据指针
    uint32_t data_len;
} recv_buffer_t;

recv_buffer_t recv_window[WINDOW_SIZE];
uint32_t expected_seq = 0;  // 期望收到的下一个序列号

接收方状态机的事件处理:

  1. 收到一个包:检查序列号是否在接收窗口内。
  2. 如果在窗口内:存入缓存,标记 received=true。
  3. 如果是期望的包:尝试向上层递交连续的数据块。
  4. 如果是乱序包:先存着,发送 ACK,等前面的包。
  5. 发送 ACK:SR 要求每个包都要单独确认,不能累积确认。

避坑指南:我曾经在实现 SR 接收方时,犯过一个低级错误——收到乱序包后,只缓存但不发 ACK。结果发送方超时重传,接收方又收到了重复包,导致缓存混乱。记住:收到任何窗口内的包,都必须立即回复 ACK,不管它是不是乱序的。

4.3 缓存管理与重排序:把拼图拼完整

重排序是 SR 接收方的核心工作。说白了,就是把收到的乱序包按序列号排好,等前面的空缺补上后,再一起提交。

我画了一张图来说明这个过程:

SR 接收方缓存与重排序过程 接收窗口(初始): seq=0 seq=1 seq=2 seq=3 seq=4 seq=5 seq=6 seq=7 收到 seq=3,seq=0 未到: 丢失 等待 等待 已收 等待 等待 等待 等待 收到 seq=0(重传),递交 seq=0~3: 已收 已收 已收 已收 seq=4 seq=5 seq=6 seq=7 说明:接收方缓存乱序包(seq=3),等待 seq=0 到达后,将连续块(seq=0~3)一次性递交上层。 窗口向前滑动到 seq=4 开始。

你想想看,如果没有缓存和重排序,收到 seq=3 时 seq=0 还没到,接收方只能丢弃 seq=3。那发送方不仅要重传 seq=0,还得重传 seq=3,效率大打折扣。

缓存管理的几个要点:

  • 窗口大小限制:接收窗口大小不能超过序列号空间的一半,否则无法区分新旧包。这是 SR 协议的一个经典约束。
  • 定时清理:已经递交的包要从缓存中移除,腾出空间给新包。
  • 重复包检测:如果收到已经确认过的包,直接丢弃,但还是要回复 ACK。

注意:SR 的接收窗口和发送窗口大小必须相等。这是协议正确性的前提。如果接收窗口比发送窗口小,接收方可能因为缓存不足而丢弃有效包,导致死锁。

4.4 SR 与 GBN 的对比:没有银弹

很多初学者问我:SR 看起来全面优于 GBN,那为什么还要学 GBN?

嗯,这个问题问得好。我直接上对比表:

对比维度 GBN(回退 N 步) SR(选择性重传)
重传粒度 重传从丢失包开始的所有包 只重传丢失的包
接收方缓存 不需要缓存(只收顺序包) 需要缓存乱序包
ACK 类型 累积确认 独立确认(每个包单独 ACK)
窗口滑动条件 收到 base 的 ACK 即可滑动 base 及之前所有包都被确认才能滑动
带宽利用率 低(丢包率高时尤其明显) 高(只重传必要包)
实现复杂度 简单 复杂(需要缓存管理、重排序、多计时器)
适用场景 丢包率低、实现资源受限 丢包率高、高延迟、带宽宝贵

我个人习惯这样选型:

  • 如果是在嵌入式设备上做简单的可靠传输,RAM 只有几 KB,我会选 GBN。省心,省资源。
  • 如果是在卫星链路或移动网络上做文件传输,丢包率可能到 10% 以上,我会毫不犹豫上 SR。GBN 在这种场景下会把自己重传死。
  • 如果是在数据中心内部,延迟极低、丢包极少,GBN 完全够用,没必要引入 SR 的复杂度。

我的经验:曾经在一个物联网项目中,设备通过 NB-IoT 上报数据,丢包率经常到 5%~8%。一开始用的 GBN,结果设备功耗飙升——大部分电量都浪费在重传上了。换成 SR 后,重传量减少了 60% 以上,电池续航从 3 个月延长到 8 个月。选对协议,效果立竿见影。

4.5 SR 的坑与注意事项

最后聊几个我在实现 SR 时踩过的坑:

  1. 序列号回绕问题:SR 的序列号空间有限,窗口大小不能超过序列号空间的一半。否则接收方无法区分是新包还是旧包重传。我见过有人设了 8 位序列号,窗口大小设了 200,结果系统跑着跑着就乱了。
  2. 多计时器管理:SR 需要为每个在途包维护一个计时器。如果窗口是 256,就要管理 256 个计时器。在软件里可以用一个优先队列来实现,但要注意性能。
  3. 接收窗口溢出:如果接收方处理速度跟不上,缓存可能被填满。这时候需要流量控制机制,比如让接收方在 ACK 中携带窗口大小信息。
  4. ACK 丢失:发送方超时重传后,接收方可能收到重复包。要做好去重处理,同时回复 ACK 防止发送方继续重传。

SR 协议虽然复杂,但它是很多现代传输协议的基础。TCP 的 SACK(Selective Acknowledgment)选项就是借鉴了 SR 的思想。理解了 SR,你再看 TCP 的拥塞控制和丢包恢复,会豁然开朗。