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; // 期望收到的下一个序列号
接收方状态机的事件处理:
- 收到一个包:检查序列号是否在接收窗口内。
- 如果在窗口内:存入缓存,标记 received=true。
- 如果是期望的包:尝试向上层递交连续的数据块。
- 如果是乱序包:先存着,发送 ACK,等前面的包。
- 发送 ACK:SR 要求每个包都要单独确认,不能累积确认。
避坑指南:我曾经在实现 SR 接收方时,犯过一个低级错误——收到乱序包后,只缓存但不发 ACK。结果发送方超时重传,接收方又收到了重复包,导致缓存混乱。记住:收到任何窗口内的包,都必须立即回复 ACK,不管它是不是乱序的。
4.3 缓存管理与重排序:把拼图拼完整
重排序是 SR 接收方的核心工作。说白了,就是把收到的乱序包按序列号排好,等前面的空缺补上后,再一起提交。
我画了一张图来说明这个过程:
你想想看,如果没有缓存和重排序,收到 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 时踩过的坑:
- 序列号回绕问题:SR 的序列号空间有限,窗口大小不能超过序列号空间的一半。否则接收方无法区分是新包还是旧包重传。我见过有人设了 8 位序列号,窗口大小设了 200,结果系统跑着跑着就乱了。
- 多计时器管理:SR 需要为每个在途包维护一个计时器。如果窗口是 256,就要管理 256 个计时器。在软件里可以用一个优先队列来实现,但要注意性能。
- 接收窗口溢出:如果接收方处理速度跟不上,缓存可能被填满。这时候需要流量控制机制,比如让接收方在 ACK 中携带窗口大小信息。
- ACK 丢失:发送方超时重传后,接收方可能收到重复包。要做好去重处理,同时回复 ACK 防止发送方继续重传。
SR 协议虽然复杂,但它是很多现代传输协议的基础。TCP 的 SACK(Selective Acknowledgment)选项就是借鉴了 SR 的思想。理解了 SR,你再看 TCP 的拥塞控制和丢包恢复,会豁然开朗。