會議投屏直播: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];
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章