12、传输层核心设计:Pacing与发送队列、NACK与重传机制、FEC前向纠错
WebRTC 的传输层,说白了就是整个系统的血管。视频数据能不能流畅到达对端,全看这一层怎么调度。我做了这么多年实时通信,见过太多项目在编码器上死磕,结果传输层一塌糊涂——卡顿、花屏、延迟飙升,最后全白费。
今天咱们就聊聊传输层的三个核心模块:Pacing 与发送队列、NACK 与重传机制、FEC 前向纠错。这三兄弟配合好了,你的 WebRTC 应用才能扛得住真实网络环境。
12.1 Pacing 与发送队列:别一股脑儿往外扔
你想想看,如果编码器产出一帧数据,你立刻把它塞进 UDP socket 发出去,会发生什么?
嗯,大概率是网络瞬间拥塞,丢包率飙升。尤其是视频关键帧,动不动几十 KB,一个突发就把路由器打懵了。
Pacing 的核心思想:把数据平滑地发送出去,而不是突发式发送。就像你喝水,一口一口慢慢喝,而不是整杯灌下去——后者大概率呛着。
关键指标:Pacing 的目标是让发送速率接近目标码率,而不是超过它。WebRTC 的 Pacing 模块会维护一个发送队列,按照计算好的间隔逐个发送数据包。
我在项目中遇到过一个问题:某次测试发现视频总是周期性卡顿,查了半天,发现是 Pacing 模块的时钟精度不够,导致发送间隔不均匀。后来换成了高精度定时器,问题就解决了。
发送队列的优先级策略
WebRTC 的发送队列不是简单的 FIFO。它把数据包分成几个优先级:
- 音频包:最高优先级,必须立即发送
- 视频关键帧:次高优先级,因为丢了会影响后续帧解码
- 视频非关键帧:普通优先级
- 重传包:优先级高于普通视频包,但低于音频
- FEC 包:优先级最低,带宽不够时可以丢弃
这个优先级设计很合理。音频不能等,这是底线。关键帧丢了,后面一堆帧都解不出来,所以也得优先保障。
避坑指南:我曾经把重传包的优先级设得过高,结果音频包被堵在后面,导致通话质量严重下降。记住,音频永远是第一位的。
12.2 NACK 与重传机制:丢了就补
UDP 不保证可靠传输,丢包是家常便饭。WebRTC 用 NACK(Negative ACKnowledgement)来请求重传丢失的包。
流程很简单:接收方发现某个序列号没收到,就发一个 NACK 给发送方。发送方收到后,从发送队列里找到对应的包,重新发一次。
但这里有个问题:什么时候发 NACK?
发早了,可能包还在路上,造成不必要的重传。发晚了,延迟就上去了。WebRTC 的做法是:等一小段时间(通常是 10-20ms),如果还没收到,就认为丢了。
// WebRTC NACK 逻辑简化版
if (now - packet.received_time > kNackThresholdMs) {
// 包还没到,发 NACK
SendNack(packet.sequence_number);
}
我个人习惯把 NACK 阈值设得稍微宽松一点,尤其是在 Wi-Fi 环境下。Wi-Fi 的延迟抖动比较大,太激进的 NACK 会导致大量不必要的重传,反而加剧拥塞。
重传的代价
重传不是免费的。每次重传都会占用带宽,如果丢包率很高,重传会吃掉大量带宽,导致新数据发不出去。
| 丢包率 | 重传带宽占比 | 影响 |
|---|---|---|
| 1% | 约 1% | 几乎无影响 |
| 5% | 约 5-8% | 轻微影响 |
| 10% | 约 15-20% | 明显卡顿 |
| 20% | 约 30-40% | 基本不可用 |
所以当丢包率超过 10% 时,单纯靠重传已经不够了。这时候就需要 FEC 上场。
注意:NACK 重传会增加延迟。每个重传周期至少增加一个 RTT 的延迟。对于实时通话来说,RTT 超过 200ms 就已经很难受了。
12.3 FEC 前向纠错:提前准备,防患于未然
FEC 的思路和重传完全不同。重传是「丢了再补」,FEC 是「提前多传一些冗余数据,丢了也能恢复」。
WebRTC 主要使用 ULP FEC(Uneven Level Protection)和 FlexFEC。ULP FEC 是早期方案,FlexFEC 是后来改进的,更灵活。
FEC 的核心参数是 冗余率。比如你设 20% 的冗余,就是每 10 个数据包,额外生成 2 个 FEC 包。这样即使丢 2 个包,也能恢复。
// FEC 冗余率计算示例
// 目标:在 10% 丢包率下,恢复概率达到 99%
// 需要冗余率约 15-20%
fec_rate = 0.15; // 15% 冗余
但冗余不是越多越好。冗余多了,带宽消耗就大,可能反而导致拥塞和丢包。这是个权衡问题。
FEC 与 NACK 的配合
实际系统中,FEC 和 NACK 是配合使用的。我的经验是:
- 低丢包率(< 5%):只用 NACK,FEC 不开,节省带宽
- 中等丢包率(5-15%):开启低冗余 FEC(5-10%),配合 NACK
- 高丢包率(> 15%):提高 FEC 冗余(15-25%),NACK 作为补充
我曾经在一个卫星通信项目里,丢包率高达 30%。那时候 FEC 冗余开到 40%,配合 NACK,才勉强把视频传过去。虽然画质很差,但至少能用了。
核心原则:FEC 用于恢复小规模丢包,NACK 用于恢复大规模丢包。两者结合,才能在各种网络条件下保持通话质量。
12.4 三者的协同工作
Pacing、NACK、FEC 不是各自为战的。它们共同构成了 WebRTC 传输层的三道防线:
- Pacing:防止突发发送,从源头减少丢包
- FEC:提前准备冗余,快速恢复小规模丢包
- NACK:兜底方案,恢复 FEC 无法处理的丢包
下面这张图展示了它们的关系:
从图上可以看到,Pacing 是数据出口的第一道关卡。它把编码器产出的数据平滑化,然后送入网络。FEC 在发送前就生成冗余包,一起发送。接收端如果发现丢包,先尝试用 FEC 恢复,恢复不了再发 NACK 请求重传。
这个流程说起来简单,但实际调优非常复杂。每个参数都要根据网络状况动态调整。WebRTC 的拥塞控制模块会实时计算带宽、RTT、丢包率,然后动态调整 Pacing 速率、FEC 冗余率和 NACK 策略。
我的建议:刚开始做 WebRTC 开发时,不要急着调这些参数。先用默认配置跑起来,观察网络状况,再针对性地调整。我曾经见过有人一上来就把 FEC 冗余开到 50%,结果带宽全被冗余吃掉了,视频反而更卡。
好了,传输层的这三个核心模块就聊到这里。记住一句话:Pacing 是预防,FEC 是保险,NACK 是补救。三者缺一不可,配合好了才能打造出健壮的实时通信系统。