【原创】FEC/QOS中的NACK方案

 

一、FEC-QOS配合NACK的优势

ACK是大家比较熟悉的传输层保障措施,在类TCP的UDP传输方案中(UDT、KCP等)中通过优化ACK发送时机、更短的超时重传时间等措施来获得更高的数据吞吐率,但ACK并不适合对实时要求极高的直播互动领域。

NACK与ACK不同,它是在没有收到数据包时向远端请求重传,因而更加适合实时通讯。通过与FEC的结合,只对FEC无法恢复的数据包请求NACK重传,能够尽量的降低重传发起概率,降低重传带来的副作用。

二、NACK在系统中的位置

NACK可以放置在原来的FEC-QOS传输层之外,作为上层应用层,这种实现方式NACK将FEC-QOS看做普通的UDP传输,二者并无紧密结合,其优势是可以与成熟的NACK方案无缝衔接。我们知道任何NACK方案都必将引入延时抖动,因为接收端在发起重传请求后,需要等待发送端重新发出的数据,在“重传等待时间”内不对外输出数据。而QOS阶段里为解决UDP乱序包的问题也引入了一个“丢包等待时间”,当遇到包序号不连续时,将等待这一时间,若仍未收到所需的包则认定丢包,不再等待。如果将这两个时间合二为一,可以尽量的降低系统时延和抖动,毕竟我们需要的是一个高实时性的NACK传输方案。我们将NACK的发起和等待放置在QOS之中,入下图所示:

xx1

图1 在QOS中实现NACK发起

当QOS检测到序号不连续时,可能是发生丢包或者是乱序,此时QOS将通过FEC解码模块分析当前疑似丢包是否将导致FEC无法恢复。此时将产生三种分析结果:

A、当前丢包即使丢了也不影响FEC恢复,比如当前丢失的包为一个或者多个冗余包,且该冗余包所在的group内的媒体包均已接收,或者借助已接收的冗余包足够恢复。

B、当前丢包不能确定是否影响FEC恢复,需要接收更多的包才能确定。比如丢包发生在group的中段且丢的数量小于冗余包总数。

C、当前丢包将导致FEC确定无法恢复,比如同一个group内丢失的包数大于冗余包总数。

对于情况A,QOS将直接不予等待,将后续接收的包直接交与FEC。对于情况B,QOS将进入“丢包等待时间”,以期收到乱序的包。对于情况C,QOS将发起NACK重传并进入等待,这个等待时间即是“丢包等待时间”又是“重传等待时间”,在等待期内不管是该乱序包到达或者重传包到达,都能满足FEC的恢复条件。

在介绍了NACK的发起条件后,我们来关注“重传等待时间”的取值问题。若设置固定的重传等待时间将很难满足各类网络情况。时间过小将导致重传包尚未到达,QOS已结束等待并输出后续包,后续即便再收到重传包也将直接丢弃。重传包也可能因网络原因丢包,若“重传等待时间”过大,将导致更大的延时和抖动。为了提高自适应能力,系统通过实时获取当前网络的UDP通讯RTT时间来作为“重传等待时间”的参考,计算出合理的值。

三、信令通道与媒体通道分离

我们使用独立的一个UDP信令通道用来传输NACK请求,而不是复用媒体通道。这样做的主要考虑是:

A、媒体通道上使用的FEC\QOS将必然引入部分延时和抖动(具体参见FEC\QOS原理说明),NACK请求对于时间特别敏感,希望是越早越快通知对方越好。在信令通道上我们将只进行裸UDP收发,不会加入FEC和QOS。

B、避免出现NACK请求包丢失后也发起NACK重传请求的情况。

C、更好的兼容性,对于不支持NACK的节点,只需要忽略信令通道的内容即可与NACK节点互通。

D、程序实现上更加简洁,无需在媒体通道上新增包类型来区分哪个包是NACK请求包。媒体包上增加字节也都会直接转化为带宽的增长。

四、实现流程

方案的实现流程如下图所示:

xx2

图2 NACK的整体流程

对于传输层模块,它是全双工的,为演示方便我们只列出单向的情况,另外一个方向也是完全一致的。在FEC编码之后,所有的发出的UDP(RTP)包均会被存入一个环形缓存区中,当收到远端NACK请求时,将在下一个媒体包传输时触发重传动作,后者将在环形缓存区中检索需要重传的包并在媒体通道上发出。我们没有新增内部线程去执行重传动作,而是借助原本的媒体包发送行为来触发,这样可以简化设计提高稳定性。

检索过程中我们进行了数据包的合法性校验和时间戳有效性校验,当发现当前时间距离数据包初次发送时间的间隔已经较大时,将放弃重传,因为此时远端极有可能已经退出等待,没有必要再浪费带宽。检索使用的是RTP头中的序号字段,需要考虑序号达到最大值时的跳变动作。