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 传输层的三道防线:

  1. Pacing:防止突发发送,从源头减少丢包
  2. FEC:提前准备冗余,快速恢复小规模丢包
  3. NACK:兜底方案,恢复 FEC 无法处理的丢包

下面这张图展示了它们的关系:

WebRTC 传输层核心模块协同工作图 编码器输出 Pacing 与发送队列 平滑发送,优先级调度 网络传输 接收端解码 NACK 重传请求 FEC 前向纠错 冗余包生成与恢复 图例 数据流 NACK 反馈 FEC 冗余 网络传输

从图上可以看到,Pacing 是数据出口的第一道关卡。它把编码器产出的数据平滑化,然后送入网络。FEC 在发送前就生成冗余包,一起发送。接收端如果发现丢包,先尝试用 FEC 恢复,恢复不了再发 NACK 请求重传。

这个流程说起来简单,但实际调优非常复杂。每个参数都要根据网络状况动态调整。WebRTC 的拥塞控制模块会实时计算带宽、RTT、丢包率,然后动态调整 Pacing 速率、FEC 冗余率和 NACK 策略。

我的建议:刚开始做 WebRTC 开发时,不要急着调这些参数。先用默认配置跑起来,观察网络状况,再针对性地调整。我曾经见过有人一上来就把 FEC 冗余开到 50%,结果带宽全被冗余吃掉了,视频反而更卡。

好了,传输层的这三个核心模块就聊到这里。记住一句话:Pacing 是预防,FEC 是保险,NACK 是补救。三者缺一不可,配合好了才能打造出健壮的实时通信系统。

公众号:蓝海资料掘金营,微信deep3321