会议投屏直播:UDP通讯方案的探索(四. FEC分片数据生成及向前纠错处理)

文章目录

    • XOR-FEC分片分组
    • XOR-FEC分片还原

与TCP数据传输机制不同,采用UDP向目标地址发送报文时。从理论上说,数据的接收顺序有可能是乱序的,更令人头痛的是UDP其发出后不管的特性使到数据是否已经到达无法感知,而丢包问题又经常发生,所以采用UDP进行数据传输时,基本上一直围绕着丢包处理这件事展开。

那么当丢包事情发生的时个,一般的处理办法如下:

  • NACK机制
    当接收端发现数据包丢失时,通过向发送端发送丢包重发请求或者是数据包接收状态,对数据数据丢包与接收情况向发送端发起反馈(具体的实现逻辑有很多种,有超时重发,丢包请求重发等,具体方案可按项目要求进行实现),驱使发送端对某个接收端的丢失数据进行定点发送。

  • FEC机制
    丢包重发机制似乎已经可以解决很多问题了,其实在很多场景上,FEC向前恢复机制的处理方式更为重要和普遍。

    • 一方面丢包重发机制需要重新等待丢失数据包的重发,很多实现上是通过新开另一个通道,甚至用TCP通道来确保丢包数据重发成功。
    • 另一方面,丢包重传阶段,接收端要进行短暂等待。在很多使用场景上,是可以接受少部分丢包却不能接受时间效率上的等待,比如音视频直播等。

得益于UDP在数据发送上最大限度对网络带宽进行行了榨取,虽然存在丢包问题,但与TCP顺序串行发送及对于某些场景下效率低下的调节调整机制相比,UDP简单而粗暴但效率却非常的高。

FEC机制通过冗余发送的方式,通对分片进行分组,生成分组数据的冗余包,与数据包一起发送到接收端。接收端在收到数据包后,对分片进行分组恢复,对每个分片中存在丢失的数据包进行反向生成丢失的分片。但理论上单靠FEC不能实现100%数据的恢复。而FEC恢复算法多种多样,有最简单的XOR,还有RS矩阵算法等。

这里提到的是向前纠错方案中最为简单易懂的XOR异或算法。

XOR-FEC分片分组

以XOR异或算法为例,在TPLine中的FEC数据分片的实现流程是这样的:

  1. 对数据包长度进行分析,计算出最优分组长度。
  • 在分组长度上,有对数值范围进行限制的阀值。一方面实现分组长度上不要过大,提高数据的恢复的几率。另一方面要对单个数据包分片分组尽可能均匀,避免最后一个分组元素过短。
  1. 对单个数据包进行分片并分组,生成FEC分片数据头部。
  • 与很多现行的实现方式不同的是,为是方案的实施更加简单,对于TPLine分组中,没有强制要求所有分组中元素的个数都相同。这个集中表现在最后一个分组的数量上。有可能前面分组元素个数都为6, 但最后一个分组的元素个数为4。
  • 在进行接收端数据分组重现时,其实可以通过包头中,分片总数与分组长度进行所有分组个数的还原。
  1. 分组完成后,通过对分组中的每个分片进行“异或”操作,最功生成冗余分片
  • 冗余分片的长度为MTU的值
  • 对包进行分片的过程中,最后一个分片存在大小少于MTU值的情况,在生成冗余分片的时候要进行补码操作,但数据本身不做补码发送。所以最后一个分组可能在最后一个分片长度少于其它分片长度的情况下,也正常进行“异或”操作,生成长度为MTU值的冗余分片

/**
 * @param fragmentArray 进行分片后的 单个包的分片集合
 * @param length MTU值
 **/
+ (NSMutableArray*__nonnull)toSplitFECPackage:(NSMutableArray*__nonnull)fragmentArray length:(int)length{
    NSMutableArray *newPackages = [[NSMutableArray alloc] init];
    if (fragmentArray != nil && fragmentArray.count > 0){
        int count   = fragmentArray.count;
        //“计算组长”
        int gLength = [self countGroupLength:count];            
        //fec附加包的总个数
        int fecCount      = count / gLength + (count % gLength > 0? 1 : 0);       
        int fragmentIndex = 0;
        //实际组长, 组长不代表所有组都是这个数量,特别是最后一组有可能少于,可以计算得出
        int groupLength   = gLength + 1;                          
        
        Byte *fecByte = (Byte*)malloc(length);
        memset(fecByte, 0, sizeof(Byte) * length);
        
        for (int i = 0; i < count; i++) {
            // 0. 计算groupID
            int groupID = ((i + 1) / gLength) - 1 + ((i + 1) % gLength > 0? 1 : 0);
            
            // 1. 对分片中的 fec分组值进行设置
            UDPFragment *fragment  = fragmentArray[i];
            fragment.groupID 	   =  groupID;   			//Fec Identity
            fragment.fragmentIndex = fragmentIndex;
            fragment.fragmentType  = 1;                  	//分片数据类型  1: 数据, 2: fec
            fragment.groupLength   = groupLength;         	//分片组 长度
            fragment.fragmentCount += fecCount;         	//分片总数
            [newPackages addObject:fragment];
            
            // 2. 进行“异或”操作
            NSData *data = fragment.data;
            Byte *targetByte = (Byte*)[data bytes];
            for (int j = 0; j < length; j++) {
                Byte tb = 0;
                if (j < data.length){
                    tb  = targetByte[j];
                }
                fecByte[j] = fecByte[j] ^ tb;
            }
            fragmentIndex++;
            
            // 3. 一个fec分组“异或”操作完成后,对数据进行封包处理
            if ((i + 1) % gLength == 0 || i + 1 == count){
                UDPFragment *fecFragment  = [[UDPFragment alloc] init];
                fecFragment.packageID     = fragment.packageID;
                fecFragment.groupID       = groupID;
                fecFragment.fragmentIndex = fragmentIndex;
                fecFragment.groupLength   = groupLength;       					//分片组 长度
                fecFragment.fragmentCount = fragment.fragmentCount;          	//分片总数
                fecFragment.fragmentSize  = length;               				//分片大小
                fecFragment.packageSize   = fragment.packageSize;   			//数据包总大小
                fecFragment.fragmentType  = 2;                 					//分片数据类型  1: 数据, 2: fec
                fecFragment.data          = [[NSData alloc] initWithBytes:fecByte length:length];
                [newPackages addObject:fecFragment];
                fragmentIndex ++;
                memset(fecByte, 0, sizeof(Byte) * length);             			//重置“异或”值的空间
            }	
        }
    }
    return newPackages;
    
}

XOR-FEC分片还原

#pragma mark- 开始运行向前纠错
- (void)startFECOperation:(UDPPackage*_Nonnull)package{
    for (NSNumber *number in package.lostArray){
        int index   = number.intValue;
        // 1.计算出所在groupID
        int groupID = ((index + 1) / package.groudLength) - 1 + ((index + 1) % package.groudLength > 0? 1 : 0);
        // 1.1 计算应该分组中,组员个数
        int fragmentCount  = package.fragmentCount;
        //该分组中有多少个片(包括fec分片)  ,组长不代表所有组都是这个数量,特别是最后一组有可能少于,可以计算得出
        int groudItemCount = 0;   
        if (fragmentCount > package.groudLength){
            //计算出最后一个groupID
            int lastID = fragmentCount / package.groudLength - 1 + (fragmentCount % package.groudLength > 0? 1 : 0); 		
            if (groupID == lastID){
                groudItemCount = fragmentCount % package.groudLength; 		//最后一组
            }else{
                groudItemCount = package.groudLength; 						//非最后一组
            }
        }else{
            groudItemCount = fragmentCount;
        }
        
        // 2.收集分片组的所有分片
        int groupReCount = 0;   											//分组中收集到的分片个数
        NSMutableArray *array = [[NSMutableArray alloc] init];
        UDPFragment *fecFragment = nil;
        for (int i = 0; i < package.fragmentBuffer.count; i++) {
            UDPFragment *item = [package.fragmentBuffer objectAtIndex:i];
            if (item.groupID == groupID){
                if (item.fragmentType == 1){  								//1: 数据, 2: fec
                    [array addObject:item];
                }else if(item.fragmentType == 2){  							//1: 数据, 2: fec
                    fecFragment = item;
                }
                groupReCount++;
            }
        }
        
        // 3.FEC处理
        if (groupReCount == groudItemCount - 1){ 							//一个组中,只是少了一个包,则可以还原
            if (fecFragment == nil){
                //丢的是FEC分片,则数据为完整,可以进行包组合工作
                UDPFragment *fecEmptyPackage  = [[UDPFragment alloc] init];
                fecEmptyPackage.fragmentType  = 2;                 			//1: 数据, 2: fec
                fecEmptyPackage.packageID     = groupID;
                fecEmptyPackage.packageID     = package.packageID;
                fecEmptyPackage.fragmentIndex = index;
                [package.fragmentBuffer addObject:fecEmptyPackage];  		//手动添加一个,在下一步合并数据包时再回收
            }else{															//还原丢失的分片
                UDPFragment *losePackage = [XORFec reduceLosePackage:array fec:fecFragment index:index];
                if (losePackage != nil){
                    [package.fragmentBuffer addObject:losePackage]; 		//还完后的分片入库
                }else{
                    NSLog(@"*** [FEC] 还原数据包 [失败]: PID: %d, index = %d ***", package.packageID, index);
                }
            }
        }else{
            NSLog(logString, package.packageID, index, groudItemCount - groupReCount - 1);
        }
    }
    
    // === 4. 重组检测 ===
    if(package.fragmentCount == package.fragmentBuffer.count){
        [self onRegroupAction:package];              										//包已经收完, 对包进行合并
        [self.packageMap removeObjectForKey:[NSNumber numberWithInt:package.packageID]];   	//放到合并队列后,回收缓存
        self.lastUDPPackage = nil;
    }
}
  • 在实际的测试中发现,在大多数情况下,数据报的接收顺序是与发送顺序一样是顺序接收的。所以在前期的逻辑中,我们以分片序列号是否是连续接收为依据来判断是否要进行数据还原操作。
  • 在执行向前纠错之前,要首先取出分片所在分组中的所有分片。因为生成的XOR-FEC分片是基于一个分组的所有分片异或产生的。
  • 当我们发现丢失的是分片组中的XOR-FEC分片,那么可以不处理,XOR-FEC分片本来就不是真实数据的一部分。
  • XOR-FEC算法是最为简单的向前纠错算法,但缺点也是很明显,当分组中,丢失一个以上分片时,丢失的分片将无法还原,只能配合NACK机制请求服务端定向重发丢失的分片。

你可能感兴趣的:(直播,ios)