实时通信为何不能用 TCP?
很多人第一反应是:既然丢包,为什么不用 TCP?答案很简单------延迟不可接受。
TCP 的重传机制是"可靠优先",而实时音视频是"时效优先"。一个已经过了播放时间窗口的数据包,即使成功重传,也失去了意义。因此,实时系统必须在两个目标之间取得平衡:
- 尽量恢复丢失数据(保证质量)
- 严格控制时延(保证实时性)
NACK 与 RTX 正是在这个约束下诞生的。
NACK:轻量级的丢包反馈机制
NACK(Negative Acknowledgement)的作用是:告诉发送端"我缺了哪些包"。
在 RTP 传输中,每个数据包都有递增的序列号。当接收端发现序列号不连续时,就可以判断存在丢包。例如:
接收序列:1001, 1002, 1004
缺失序列:1003
此时接收端不会立刻请求重传,而是进入一个短暂的"观察窗口"(通常 20~50ms),以避免将网络乱序误判为丢包。这是实际工程中非常关键的一步。
一旦确认丢失,接收端会通过 RTCP 发送 NACK 报文。该报文采用紧凑编码方式,用一个起始序号(PID)和一个位掩码(BLP)描述多个丢失包,从而减少控制信令的开销。
更重要的是,NACK 并不是"立即逐包发送",而是批量聚合,通常每 10~30ms 发送一次。这种策略可以显著降低 RTCP 带宽占用,同时避免反馈风暴。
RTX:独立通道的重传机制
如果说 NACK 是"请求",那么 RTX(Retransmission)才是"执行"。
RTX 的设计并不是简单地"再发一遍原包",而是引入了一套独立机制:
- 使用独立 SSRC(区分原始流与重传流)
- 使用独立 Payload Type
- 在 Payload 中携带原始序列号(OSN)
这样设计的好处是:接收端可以明确识别这是重传数据,并进行正确恢复,而不会干扰正常的解码流程。
一个典型的 RTX 包结构如下:
[ RTX Header ]
[ OSN(原始序列号) ]
[ 原始 RTP Payload ]
接收端在收到 RTX 包后,会提取 OSN,将其还原为原始 RTP 包,再交给抖动缓冲区(Jitter Buffer)进行重排序与播放。
完整链路:从丢包到恢复
将 NACK 与 RTX 串联起来,可以得到一个完整的数据恢复闭环:
- 接收端检测到序列号缺失
- 等待短暂窗口确认丢包
- 发送 RTCP NACK 请求
- 发送端在缓存中查找对应 RTP 包
- 封装为 RTX 包重新发送
- 接收端恢复原始包并插入播放队列
这套机制的关键不在"流程",而在每一步的时序控制。如果任何一个环节过慢,重传数据就可能赶不上播放时间,从而失效。
核心:发送缓存设计
NACK/RTX 能否生效,取决于发送端是否还能"找回历史数据"。因此,RTP 发送缓存(Packet History)是整个机制的核心组件。
缓存设计需要权衡三个关键因素:
1. 缓存时长
通常设置为:
缓存时间 ≈ RTT + 抖动裕量(一般 200ms ~ 1000ms)
过短:包已经被清理,无法重传
过长:内存占用过高,影响系统稳定性
2. 数据结构
常见实现:
std::map<uint16_t, RtpPacket>
或者环形缓冲区(更高效)
3. 清理策略
- 按时间淘汰(推荐)
- 按容量淘汰(兜底)
什么时候"放弃重传"?
并不是所有丢包都值得重传。工程中必须明确"放弃条件":
1. 超过播放时延
如果数据已经错过播放时间窗口(例如 >100ms):
即使重传成功,也不会被使用
2. 重试次数限制
通常限制为 2~3 次:
避免无效重传浪费带宽
3. 关键帧丢失
如果丢的是视频关键帧(I帧):
直接发送 PLI(请求新关键帧)比 NACK 更有效
与其他机制的协同
NACK/RTX 并不是孤立存在,而是与多个机制协同工作:
1. 与 FEC(前向纠错)
- FEC:无延迟,但增加带宽
- NACK:低带宽,但有往返延迟
实际系统通常采用组合策略:
轻微丢包 → FEC
严重丢包 → NACK + RTX
2. 与拥塞控制
重传流量会增加带宽压力,因此必须纳入带宽估计算法(如 GCC)。否则容易导致网络进一步拥塞,形成"重传风暴"。
3. 与抖动缓冲
RTX 恢复的数据必须正确插入时间序列,否则会导致解码错误甚至崩溃。因此 Jitter Buffer 必须支持:
- 乱序插入
- 时间戳对齐
- 丢帧容忍
音频与视频的差异策略
一个常被忽略的事实是:音频和视频对重传的依赖完全不同。
视频
- 强依赖 NACK/RTX
- 丢包会导致花屏或预测链断裂
音频
- 通常不启用重传
- 使用 PLC(Packet Loss Concealment)进行补偿
原因在于:音频对延迟极其敏感,而单个丢包影响相对较小。
常见问题与踩坑经验
- 发了 NACK,但发送端缓存已被清理
- RTX SSRC 映射错误,接收端无法识别
- 重传包未正确恢复 OSN,导致解码异常
- NACK 发送过于频繁,引发 RTCP 风暴
- 未做乱序容忍,误判丢包
这些问题的本质都指向一点:时序与状态管理比协议本身更重要。
总结:本质是"延迟与可靠性的博弈"
NACK 与 RTX 并不是简单的"补包机制",而是一套围绕实时性设计的动态系统。它的核心思想可以概括为一句话:
在有限时间窗口内,用最小代价恢复最有价值的数据