4、丢包与重传策略:丢包检测、NACK 与 ACK、选择性重传、FEC 前向纠错、重传超时设置
流媒体传输中,丢包是绕不开的坎。不管你用 TCP 还是 UDP,网络总会跟你开点玩笑。我做了这么多年流媒体,见过太多因为丢包处理不当导致的卡顿、花屏、延迟飙升。说白了,丢包不可怕,可怕的是不知道怎么应对。
这一章,我们就来聊聊丢包检测、重传机制、前向纠错这些硬核话题。我会结合我踩过的坑,给你讲清楚每个策略的适用场景和实现要点。
核心观点:没有完美的丢包处理方案,只有最适合当前场景的折中。延迟敏感选 FEC,可靠性优先选重传,两者结合才是王道。
4.1 丢包检测:怎么知道包丢了?
丢包检测是重传的前提。你连包丢了都不知道,谈何重传?常见的检测手段有两种:超时检测和序列号间隙检测。
超时检测:发送方给每个包设一个定时器。如果超时还没收到确认,就认为包丢了。这个方法简单,但有个问题——超时时间设长了延迟大,设短了容易误判。我见过一个项目,RTT 波动很大,固定超时导致大量误重传,把网络搞得更堵。
序列号间隙检测:接收方维护收到的序列号。如果发现跳号(比如收到 1、2、4,缺了 3),就知道 3 丢了。这种方式更及时,但需要接收方主动反馈。
我的习惯:实际项目中,我通常把两种方法结合使用。序列号间隙做快速检测,超时做兜底。这样既快又稳。
4.2 NACK 与 ACK:两种确认模式
确认机制是重传的指挥棒。主流有两种模式:ACK 和 NACK。
| 特性 | ACK(肯定确认) | NACK(否定确认) |
|---|---|---|
| 反馈内容 | 收到哪些包 | 丢了哪些包 |
| 反馈频率 | 每个包或批量确认 | 仅丢包时触发 |
| 带宽开销 | 高(持续反馈) | 低(按需反馈) |
| 适用场景 | 高可靠、低丢包率 | 实时流媒体、高丢包 |
| 典型协议 | TCP | RTP/RTCP、QUIC |
我个人更偏爱 NACK 模式。为什么?因为流媒体场景下,大部分包是正常到达的。用 ACK 等于每时每刻都在发确认,浪费带宽。NACK 只在丢包时反馈,省下来的带宽可以留给音视频数据。
不过要注意,NACK 有个坑:如果 NACK 本身丢了怎么办?我曾经在一个项目中遇到过,接收方发了 NACK,但网络拥塞导致 NACK 也丢了,发送方完全不知道需要重传。解决方案是给 NACK 加个超时重传机制,或者用 ACK 做兜底。
4.3 选择性重传:只传丢失的包
选择性重传(Selective Retransmission, SR)是 NACK 模式的自然延伸。接收方告诉发送方:「我丢了包 3、7、11,请只重传这些。」发送方照做,不传那些已经收到的包。
这听起来很合理,对吧?但实现时有个细节要注意:重传优先级。不是所有丢包都同等重要。
- 视频关键帧(I帧):丢了必须重传,否则后续 P/B 帧全废。
- 音频包:丢了可以部分容忍,重传优先级低于关键帧。
- 视频非关键帧:如果延迟已经超标,干脆放弃重传,等下一个帧。
我记得有一次做直播优化,客户端反馈画面频繁卡顿。排查发现,接收方把所有丢包都排队重传,结果关键帧的重传被非关键帧堵住了。后来加了优先级队列,问题就解决了。
避坑指南:我曾经在一个项目中把选择性重传的窗口设得太大,导致重传风暴——发送方同时重传几十个包,网络瞬间拥塞。建议重传窗口控制在 5-10 个包以内,配合拥塞控制一起做。
4.4 FEC 前向纠错:不重传也能恢复
FEC(Forward Error Correction)的思路和重传完全不同。发送方在发送原始数据的同时,额外发送一些冗余数据。接收方即使丢了一部分包,也能通过冗余数据恢复出原始内容。
常见的 FEC 算法有两种:
- XOR FEC:对 n 个原始包做异或运算,生成 1 个冗余包。丢 1 个包时可以用冗余包恢复。简单高效,但只能恢复 1 个丢包。
- Reed-Solomon (RS) 码:可以配置 k 个原始包 + m 个冗余包。只要丢包数 ≤ m,就能完全恢复。灵活性高,但计算开销大。
我个人的经验是:FEC 适合丢包率稳定且延迟敏感的场景。比如视频会议,延迟要求 200ms 以内,重传根本来不及,FEC 就是救命稻草。
但 FEC 也有代价。冗余数据会占用额外带宽。假设你设了 20% 的冗余,那实际传输的数据量就是原始数据的 1.2 倍。如果网络本身不丢包,这 20% 就白费了。
关键权衡:FEC 冗余比例 = 预期丢包率 × 安全系数。比如预期丢包 5%,冗余设 8%-10% 比较合理。设太高浪费带宽,设太低恢复不了。
4.5 重传超时设置:RTO 的计算艺术
重传超时(Retransmission Timeout, RTO)是重传策略的「最后一道防线」。RTO 设得太短,会导致不必要的重传;设得太长,延迟会飙升。
RTO 的计算核心是 RTT(往返时间)的估计。TCP 的经典公式是:
SRTT = (1 - α) × SRTT + α × RTT_sample
RTTVAR = (1 - β) × RTTVAR + β × |SRTT - RTT_sample|
RTO = SRTT + 4 × RTTVAR
其中 α 通常取 0.125,β 取 0.25。这个公式考虑了 RTT 的均值和方差,能自适应网络波动。
但在流媒体场景下,我建议做两点改进:
- 最小 RTO 限制:设一个下限,比如 50ms。防止 RTT 突然变小时 RTO 过短导致误重传。
- 最大 RTO 限制:设一个上限,比如 3s。防止长时间等待,影响用户体验。
我的经验:在 WebRTC 项目中,我把 RTO 初始值设为 500ms,然后根据实际 RTT 动态调整。同时加了「退避」机制——连续重传失败时,RTO 指数增长,避免网络拥塞时雪上加霜。
4.6 混合策略:ARQ + FEC 的组合拳
实际工程中,很少有人只用纯重传或纯 FEC。更常见的做法是两者结合:
- 轻度丢包(< 5%):只用 FEC,不触发重传。延迟最低。
- 中度丢包(5% - 15%):FEC 兜底 + 选择性重传补漏。FEC 恢复大部分丢包,重传只处理 FEC 恢复不了的。
- 重度丢包(> 15%):降低码率 + 增加 FEC 冗余 + 重传配合。这时候网络已经不行了,先保流畅度。
我记得有个项目,客户要求 99.9% 的包必须在 500ms 内到达。纯重传做不到,纯 FEC 带宽又不够。最后用了分层方案:视频用 FEC + 选择性重传,音频用纯 FEC(音频包小,冗余开销低)。效果很好,客户很满意。
注意:混合策略的复杂度会翻倍。你需要协调 FEC 窗口和重传窗口,避免两者冲突。我曾经见过一个实现,FEC 恢复了一个包,但重传线程不知道,又重传了一次,浪费带宽。解决方案是加一个「已恢复包」的共享状态表。
4.7 本章小结
丢包与重传策略,说白了就是在延迟、带宽、可靠性之间找平衡。没有银弹,只有最适合你场景的方案。
- 丢包检测:序列号间隙 + 超时兜底
- 确认模式:流媒体首选 NACK
- 选择性重传:加优先级,控窗口
- FEC:适合延迟敏感场景,冗余比例要算好
- RTO:动态计算,加退避机制
- 混合策略:根据丢包率动态切换