会议投屏直播:UDP通讯方案的探索(一. 数据传输格式的定义)

文章目录

    • 一. 前言
    • 二. 格式的定义
      • TCP数据包格式
      • UDP数据包格式
        • 最大传输单员(MTU)
        • 应用层分片的重要性
        • UDP数据包结构
        • UDP数据分片封包
        • UDP数据解包

一. 前言

一开始用TCP,很大程度时因为简单,可以快速实现一个初级的版本。因为受限于各种要求,TPLine从一开始就不准备通过中间转发推流服务对用户端实施推流。所以在使用中,单台IPad作为主播端,同时开启推流程序,同时为多个接入端推送数据流。

TCP通讯方案实现之后存在以下几个问题:

  • 接入并发能力有限。
  • 接入数量上升的后,画面质量不断下降。

于是在TCP通讯方案实现后,决定用UDP+组播的方式尝试解决上面的问题。

在做TPLine的时候,没铺天盖地用第三方库,而是所有细节都自己写。是因为看到很多人用了很多第三方库已经用到连基础知道都不想去了解了,以为做技术也就这样了,这是何等的悲哀。其实不用太在意一开始代码写得不好,算法写得完不完美,过程很重要,成长需要不断积累。

二. 格式的定义

格式很大程度上受传输方式的影响。
早期用TCP可靠传输方式进行流媒体数据传输时,对于最上层的应用层调用来说,只要关心数据类型及其对应的传输数据即可(音频数据与视频数据共用同一个通道,各自处理发送与接收及播放),发送端在发送数据时,在数据包中分配一个字节用于记录数据包的类型,类型包括:

  1. 音频数据
    数据封装格式为 aac音频数据
  2. 视频数据
    数据封装格式为 h.264视频数据
  3. 逻辑控制数据
    逻辑控制包括用户身份验证等其他辅助逻辑

TCP数据包格式

因为没有采用其它第三方的实现方式,所有实现都是自己实现的。所以在运用TCP进行数据传输时,在发送及接收端的逻辑处理上,肯定要考虑的问题就是“数据粘包”问题。所有定义的数据格式代码如下:

#pragma mark - 数据封包逻辑
+ (NSData*)toDataPackage:(NSData*)bodyData {
    if(bodyData != nil){
        NSMutableData *postData = [[NSMutableData alloc] init];
        
        char charnum[4];
        uint64_t bodyLen = bodyData.length;// 有四位是协议号
        charnum[0] = (unsigned char) ((bodyLen & 0xff000000) >> 24);
        charnum[1] = (unsigned char) ((bodyLen & 0x00ff0000) >> 16);
        charnum[2] = (unsigned char) ((bodyLen & 0x0000ff00) >> 8);
        charnum[3] = (unsigned char) ((bodyLen & 0x000000ff));
        
        char code[1];
        code[1] = (unsigned char) ((DATATRANS & 0x00ff));
        
        [postData appendData:[@"SRET" dataUsingEncoding:NSUTF8StringEncoding]];// 4位 协义头
        [postData appendBytes:charnum length:4];// 4位 包长度 (code + playload)
        [postData appendBytes:code length:1];// 1位code
        [postData appendData:bodyData];
        return postData;
    }
    return nil;
}
  • “SRET”用于数据分隔及判断是否为非法数据包的依据。当接收的数据不是SRET开头时会不断丢弃无效的数据,直到找到合法数据包头为止。
  • 其后的4个字节为“数据长度”。在接收端进行数据包分拆合并时,这个“数据长度”就显得由为种要。在处理数据粘连时,可以根据“数据长度”来有效对连续发送来过的数据进行分拆。
  • “类型参数”,用一个字节表示,用于区分数据类型的。
  • “类型参数”之后就是对应要传输的二进制数据。因为TCP在传输过程内部可以对发送的数据进行分片发送操作,所以简单来说这样的包结构简单而又可以满足需求。

数据拆包逻辑包码如下:

#pragma mark - 数据拆包逻辑
+ (void)onReceiveData:(NSMutableData*)data callBack:(CallBackBlock)block{
    static int hSize = 9;//头的长度
    if (data == nil) {
        return;
    }
    
    NSInteger index = 0;
    NSUInteger packageSize = [data length];
    
    while (true) {
        @autoreleasepool {
            if (index >= packageSize || index + hSize > packageSize) {
                break;//位置大于接收数据包长度, 少于一个头信息长度,不处理
            }
            
            NSData *SubHeaderData = [data subdataWithRange:NSMakeRange(index + 0, 4)];
            NSString *subHeader = [[NSString alloc] initWithData:SubHeaderData encoding:NSUTF8StringEncoding];
            if (subHeader == nil || [@"SRET" isEqualToString:subHeader] == NO) {
                NSLog(@" === 非法数据包 : %@ === ", subHeader);
                index++;
                continue;
            }
            
            NSData *SizeData = [data subdataWithRange:NSMakeRange(index + 4, 4)];
            char *headernum = (char*)[SizeData bytes];
            UInt64 headerNum0 = (headernum[0] << 24 );
            UInt32 headerNum1 = (headernum[1] << 16);
            UInt16 headerNum2 = (headernum[2] << 8 );
            UInt8  headerNum3 = headernum[3];
            UInt64 bodySize = headerNum0 + headerNum1 + headerNum2 + headerNum3;
            if (bodySize == 0 || (packageSize - index - hSize) < bodySize) {
                NSLog(@"数据包未接收完整, 完整数据包大小为: %llu,还差 %llu", bodySize, bodySize - packageSize);
                break;
            }
            
            NSData *codeData = [data subdataWithRange:NSMakeRange(index + 8, 1)];
            char *codeChar = (char*)[codeData bytes];
            UInt8 uiCode = codeChar[1];
            unsigned short code = uiCode;
            
            NSData *bodyData = [data subdataWithRange:NSMakeRange(index + hSize, bodySize)];
            index += (bodySize + hSize);
            NSError *error;
            NSLog(@"======== 已经分离的数据包 ======== \n 包体: %lld",bodySize);
            if (!error) {
                if (block != nil) {
                	//在非主线程中运行
                    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 
                        block(code, bodyData);
                    });
                }
            }
        }
    }
    
    if (index > 0) {
    	//删除已经处理的包,有没有处理与有没有设置delegate有关
        [data replaceBytesInRange:NSMakeRange(0, index) withBytes:NULL length:0];
        NSLog(@"删除已经处理数据包");
    }
}

拆包过程经过代码有删减,可选择性参考。也欢迎提出更好的实现逻辑共同进步。
相对于UDP而言,TCP在传输格式的定义上主要为数据拆包而服务,经过对数据进行合法性判断后,取得包的长度。在后续进行数据接收时,可以清晰知道包的结束位置,进而进行数据的拆分。
而采用UDP进行数据传输时,其实也少不了数据分包拆包的操作。只是在此基础上,我们对于
“二进制数据”部分的内容要进一步进行细化处理。

UDP数据包格式

无论是TCP还是UDP,始终是以报文的方式进行数据传输的。
就应用层使用上说,TCP不用调用者做过多的数据分片处理,而TCP的内部从分片到传输到反馈到流量控制都有一个完整的流程进行应对。
而对于UDP来说,尽管它也有自己的分片逻辑,但无法像TCP内部处理那样,实现全方位的容错处理(当然相比UDP来说发送效率效低)。比如sendto函数允许发送的最大数据长度为 65507 ,而TCP使用上没有这个方面的限制。致于原因,看完下面的简单说明,大概就懂了。

最大传输单员(MTU)

  • MTU: 最大传输单员,即单次报文传输的最大字节数。

  • UDP与TCP一样处于“传输层”。而TCP和UDP都是基于下层“网络层”中的“IP协议”进行数据发送。而“网络层”而依赖更下层“数据链路层”进行数据发送的,如常用的以太网协议。

  • 从“数据链路层”分析,不同的链路层协议,基于各种设计上的考虑,其支持的MTU值是不一样的。以“以太网”为例,在局域网内的最大值为1500 ,鉴于Internet上的标准MTU值为576字节,经测试在Internet上大多数为1500, 少部分为 576, 所以基于Internet上的应用进行UDP手动分包时,最好以MTU = 576 - 20 - 8 以下进行分包。

  • 受限于“以太网”的MTU值。IP层1500字节要包括IP的包头与数据。所以对于“传输层”传过来的数据包过大的时候要进行分片处理。当IP的包头在内少于46个字节时,IP层会对其进行补码处理。

  • 其中IP协议的协议头占用20个字节,基中包括用于记录分片长度的16位,16位数据包标识,13位分片序列号,用于分片重组,还有其他属性等。

  • 在UDP层,也占用了8个字节。其中源端口号和目标端口号各占用了两个字节,包长度占用了两个字节,还有一个用于校验的CRC值。

所以,基于局域网的应用,可用字节数 = 1500 - 20(IP包头)- 8(UDP包头) Byte

应用层分片的重要性

  • 在进行sendto调用的时候,大家可能已经发现,当发传入数据的长度大于 65535 - 1 -20 -8 = 65507的时候,UDP数据发送失败,返回-1的值。这个很大程度上是因为在UDP包头中,用于记录UDP包大小的字节长度为两个Byte,也就是65535为最大值。所以理论上,UDP同样不需要基于MTU进行分片,只在少于65535就可以了。
sendto(self.socketFD, send_Message, data.length, 0, (struct sockaddr*)&m_serveraddr, sizeof(m_serveraddr));
  • 在实际的应用中,基于IP层进行数据分片的灵活性效低。当分片在发送中,一个分片发生丢失时,IP层无法对数据包进行恢复,不完整的数据包不能向上回调,应用层结合数据恢复机制进行数据重发,就只能对整个包进行重发,而不能精准对丢失的分片进行重发,消费了资源和时间。所以,不难发现,在TCP的内存实现中同样要进行内部分片发送的重要性。

UDP数据包结构


/**
 *  UDP 数据分片
 **/
@interface UDPFragment : NSObject

@property(nonatomic, assign)UInt16  packageID;          //包唯一标识
@property(nonatomic, assign)UInt32  packageSize;        //包大小

@property(nonatomic, assign)UInt8   fragmentCount;      //分片总数
@property(nonatomic, assign)UInt8   fragmentIndex;      //分片序号
@property(nonatomic, assign)UInt16  fragmentSize;       //分片大小

@property(nonatomic, assign)UInt8   groupID;            //分组唯一标识
@property(nonatomic, assign)UInt8   groupLength;        //fec组长 
@property(nonatomic, assign)UInt8   fragmentType;       //分片数据类型  1: 数据, 2: fec

@property(nonatomic, strong)NSData  *data;              //分片数据

@end

  • packageID: 包的唯一id。
  • packageSize:包的大小,在后面的文章中,对接收的分片进行包还原时,对包大小进行配对判断包是否还原成功。
  • fragmentCount:分片总数,记录对包进行分片后的数量,同样作用于分片合成还原。
  • fragmentIndex:分片序号,用于记录分片在包中的位置,用天包还原及分片重发等。
  • fragmentSize:分片数据大小,记录分片上的数据大小,用于粘包处理和包还原等。
  • groupID:分组唯一标识,这个在后期用到FEC数据丢包还原时用到。当分组中
    部分数据丢失时,可以通过算法处理,还原丢失的数据。这个在后面的文章中会提到。
  • fragmentType:分片数据类型,在FEC数据处理时,要生成FEC冗余数据包用于还原丢失的数据。

UDP数据分片封包


#define PACKAGE_MTU 1456

#pragma mark- 转换成网络包发送格式
- (NSMutableData*)toData{
    NSMutableData *postData = [[NSMutableData alloc] init];
    
    char identity[2];
    identity[0] = (unsigned char) ((self.packageID & 0x0000ff00) >> 8);
    identity[1] = (unsigned char) ((self.packageID & 0x000000ff));
    
    char totalCount[1];
    totalCount[0] = (unsigned char) ((self.fragmentCount & 0x000000ff));
    
    char index[1];
    index[0] = (unsigned char) ((self.fragmentIndex & 0x000000ff));
    
    char pageSize[2];
    pageSize[0] = (unsigned char) ((self.fragmentSize & 0x0000ff00) >> 8);
    pageSize[1] = (unsigned char) ((self.fragmentSize & 0x000000ff));
    
    char totalSize[4];
    totalSize[0] = (unsigned char) ((self.packageSize & 0xff000000) >> 24);
    totalSize[1] = (unsigned char) ((self.packageSize & 0x00ff0000) >> 16);
    totalSize[2] = (unsigned char) ((self.packageSize & 0x0000ff00) >> 8);
    totalSize[3] = (unsigned char) ((self.packageSize & 0x000000ff));
    
    // 分片组相关属性
    char groupID[1];
    groupID[0] = (unsigned char) ((self.groupID & 0x000000ff));
    
    char groupLength[1];
    groupLength[0] = (unsigned char) ((self.groupLength & 0x000000ff));
    
    char type[1];
    type[0] = (unsigned char) ((self.fragmentType & 0x000000ff));
    
    [postData appendData:[@"SRET" dataUsingEncoding:NSUTF8StringEncoding]];// 4位 协义头
    [postData appendBytes:identity length:2];           //packageID
    [postData appendBytes:totalCount length:1];         //packageSize
    [postData appendBytes:index length:1];              //fragmentIndex
    [postData appendBytes:pageSize length:2];           //fragmentSize
    [postData appendBytes:totalSize length:4];          //packageSize
    [postData appendBytes:groupID length:1];            //groupID
    [postData appendBytes:groupLength length:1];        //groupLength
    [postData appendBytes:type length:1];               //type
    [postData appendData:self.data];
    
    return postData;
}

  • 现在的数据发送是以分片为单元进行发送的。在数据封包上,和上面TCP的原理一样。

UDP数据解包

#pragma mark - 数据拆包逻辑
+ (void)onReceiveData:(NSMutableData*)data callBack:(Block)block{
    static int hSize = 14;//头的长度
    if (data == nil) {
        return;
    }
    
    NSInteger index = 0;
    NSUInteger packageSize = [data length];
    
    while (true) {
        @autoreleasepool {
            if (index >= packageSize || index + hSize > packageSize) {
                break;//位置大于接收数据包长度, 少于一个头信息长度,不处理
            }
            
            // 1. header ("FUCK")
            NSData *SubHeaderData = [data subdataWithRange:NSMakeRange(index + 0, 4)];
            NSString *subHeader = [[NSString alloc] initWithData:SubHeaderData encoding:NSUTF8StringEncoding];
            if (subHeader == nil || [@"SRET" isEqualToString:subHeader] == NO) {
                NSLog(@" === 非法数据包 : %@ === ", subHeader);
                index++;
                continue;
            }
            
            // 2. identity
            NSData *identityData = [data subdataWithRange:NSMakeRange(index + 4, 2)];
            char *idenChar = (char*)[identityData bytes];
            UInt16 idenH = (idenChar[0] << 8 );
            UInt8 ideL = idenChar[1];
            UInt16 identity = ideL + idenH;
            
            // 3. count
            NSData *countData = [data subdataWithRange:NSMakeRange(index + 6, 1)];
            char *countChar = (char*)[countData bytes];
            UInt8 pageCount = countChar[0];
            
            // 4. index
            NSData *indexData = [data subdataWithRange:NSMakeRange(index + 7, 1)];
            char *indexChar = (char*)[indexData bytes];
            UInt8 pageIndex = indexChar[0];
            
            // 5. page size
            NSData *pageSizeData = [data subdataWithRange:NSMakeRange(index + 8, 2)];
            char *pageSizeChar = (char*)[pageSizeData bytes];
            UInt16 pageSizeH = (pageSizeChar[0] << 8 );
            UInt8 pageSizeL = pageSizeChar[1];
            UInt16 pageSize = pageSizeH + pageSizeL;
            
            // 6. size
            NSData *sizeData = [data subdataWithRange:NSMakeRange(index + 10, 4)];
            char *sizeChar = (char*)[sizeData bytes];
            UInt32 sizeH3 = (sizeChar[0] << 24 );
            UInt32 sizeH2 = (sizeChar[1] << 16 );
            UInt16 sizeH1 = (sizeChar[2] << 8 );
            UInt8 sizeL = sizeChar[3];
            UInt16 bodySize = sizeH1 + sizeH2 + sizeH3 + sizeL;
            
            if (pageSize == 0 || (packageSize - index - hSize) < pageSize) {
                break;
            }
            // 7. 分片数据
            NSData *bodyData = [data subdataWithRange:NSMakeRange(index + hSize, pageSize)];
            index += (pageSize + hSize);
            // 8. 回调
            if (block != nil) {
                UDPackage *package = [[UDPackage alloc] init];
                package.identity = identity;
                package.totalCount = pageCount;
                package.index = pageIndex;
                package.pageSize = pageSize;
                package.totalSize = bodySize;
                package.data = bodyData;
                
                dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //在非主线程中运行
                    block(package);
                });
            }
        }
    }

    if (index > 0) {
        //删除已经处理的包,有没有处理与有没有设置delegate有关
        [data replaceBytesInRange:NSMakeRange(0, index) withBytes:NULL length:0];
    }
}

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