iOS Socket封包、粘包、拆包處理

一、封包

在iOS很多應用開發中,大部分用的網絡通信都是http/https協議,除非有特殊的需求會用到Socket網絡協議進行網絡數據傳輸,這時候在iOS客戶端就需要很好的第三方CocoaAsyncSocket 來進行長連接連接和傳輸數據,讀者可以自行查閱資料搜索這個庫的用法。

一般在使用Socket的時候,后臺會對Socket傳輸數據有一個自定義的協議,協議可能有些差別不過基本上是大同小異。 如圖


也就是說我們通過Socket發送給服務器的數據,最終要轉換成二進制流數據,并且按照協議約定的格式。

下面我簡單解釋下這個協議,因為一開始我自己也不是很理解。這個協議是指我們在發送的數據包頭部開辟一個4個字節長度的空間,用來存儲服務號轉換成的二進制數據。(將1轉換成二進制數據存儲進去占4個字節長度),然后再將數據包長度轉換成二進制數據并存儲到后面開辟的4個字節中(這里需要注意下如果數據要進行加密傳輸,這里的長度應是加密后的長度),最后將數據數據包轉換成二進制數據添加到后面,組成一個完整的數據包也就是封包。這里一定要按協議規定的順序不然服務器解析不了。
具體使用見代碼

    NSMutableDictionary *dictTemp = [NSMutableDictionary dictionary];
    dictTemp[@"username"]         = @"LD";
    
    //先創建模型 --> 轉Json -->轉字符串
    TestModel *model = [TestModel new];
    model.type       = 1;
    model.userName   = @"LD";
    model.age        = @"18";
    model.message    = @"Hellow";
    model.Content    = dictTemp;
    
    //先將模型轉換成Json格式的數據這里根據自己項目情況來看是否需要轉成Json格式  使用到了MJExtension,
    NSString * strJson  = [[NSString alloc] initWithData :model.mj_JSONData encoding :NSUTF8StringEncoding];
    Cs_Connect *connect = [Cs_Connect new];
    connect.serverID    = 1;
    connect.message     = strJson;
    connect.length      = (int)connect.message.length;

    //將數據傳換成二進制數據,轉換之后的數據和協議順序是一致的(為什么不需要調整順序我也不知道,有興趣的的同學自己去研究下這個方法)
    NSMutableData *dataModel =  [socket RequestSpliceAttribute:connect];
    
    // 通過Socket發出去
    [socket sendMessage:dataModel];
轉為二進制數據
//  將模型數據轉換成二進制數據
-(NSMutableData *)RequestSpliceAttribute:(id)obj{

    _data = nil;//記得清空不然數據包會越來越大
    if (obj == nil) {
        self.object = self.data;
        
        NSLog(@"傳入需轉二進制的數據為空");
        return nil;
     }
    unsigned int numIvars; //成員變量個數
    objc_property_t *propertys = class_copyPropertyList(NSClassFromString([NSString stringWithUTF8String:object_getClassName(obj)]), &numIvars);
    NSString *type = nil;
    NSString *name = nil;
    
    for (int i = 0; i < numIvars; i++) {
        objc_property_t thisProperty = propertys[i];
        
        name = [NSString stringWithUTF8String:property_getName(thisProperty)];
//                NSLog(@"%d.name:%@",i,name);
        type = [[[NSString stringWithUTF8String:property_getAttributes(thisProperty)] componentsSeparatedByString:@","] objectAtIndex:0]; //獲取成員變量的數據類型
//                NSLog(@"%d.type:%@",i,type);
        id propertyValue = [obj valueForKey:[(NSString *)name substringFromIndex:0]];
//                NSLog(@"%d.propertyValue:%@",i,propertyValue);
        
        if ([type isEqualToString:TYPE_UINT8]) {
            uint8_t i = [propertyValue charValue];// 8位
            [self.data appendData:[DLSocketDataUtils byteFromUInt8:i]];
        }else if([type isEqualToString:TYPE_UINT16]){
            uint16_t i = [propertyValue shortValue];// 16位
            [self.data appendData:[DLSocketDataUtils bytesFromUInt16:i]];
        }else if([type isEqualToString:TYPE_UINT32]){
            uint32_t i = [propertyValue intValue];// 32位
            [self.data appendData:[DLSocketDataUtils bytesFromUInt32:i]];
        }else if([type isEqualToString:TYPE_UINT64]){
            uint64_t i = [propertyValue longLongValue];// 64位
            [self.data appendData:[DLSocketDataUtils bytesFromUInt64:i]];
        }else if([type isEqualToString:TYPE_STRING]){
            NSData *data = [(NSString*)propertyValue \
                            dataUsingEncoding:NSUTF8StringEncoding];// 通過utf-8轉為data
            [self.data appendData:data];
            
        }else {
            NSLog(@"RequestSpliceAttribute:未知類型");
            NSAssert(YES, @"RequestSpliceAttribute:未知類型");
        }
    }
    
    // hy: 記得釋放C語言的結構體指針
    free(propertys);
    self.object = _data;
    return _data;
}

轉為二進制代碼鏈接:http://pan.baidu.com/s/1hsi7tNQ密碼: byiy
關于轉碼更詳細的說明請看下面的鏈接
參考資料:iOS開發之Socket通信實戰--Request請求數據包編碼模塊

二、粘包、拆包處理

我們一般使用的是基于TCP的流式Socket,因此本文也主要講解這一種方式,TCP是一種流協議(stream protocol)。這就意味著數據是以字節流的形式傳遞給接收者的,沒有固有的"報文"或"報文邊界"的概念。從這方面來說,讀取TCP數據就像從串行端口讀取數據一樣--無法預先得知在一次指定的讀調用中會返回多少字節(也就是說能知道總共要讀多少,但是不知道具體某一次讀多少)

讓我們來看一個例子:我們假設在主機A和主機B的應用程序之間有一條TCP連接,主機A有兩條報文D1,D2要發送到B主機,并兩次調用send來發送,每條報文調用一次。


那么,我們自然而然的希望兩條報文是作為兩個獨立的實體,在各自的分組中發送,如圖1:


這樣的話,我們無需做任何特別的處理,便能夠很容易的區分每一個獨立的數據,并根據需求分別做相應的處理。但現實往往是有所偏差的,實際的數據傳輸過程很可能不會遵循這個模型。而是會采用以下四種方式之一進行傳輸。如圖2:


  • D1和D2數據作為兩個獨立的分組,分別到達主機B;
  • D1和D2合為一個整體組,一起到達主機B;
  • D1的部分數據先到達主機B,剩下的D1數據和D2和在一組到達主機B;
  • D1和D2的部分數據先到達主機B, D2后到達主機B;
    實際上,可能的情況還不止4種,這里我們就不做深入了解,以上就是造成粘包的原因。
解決思路:拆包

在上面說到我們給每個數據包添加頭部,頭部中包含數據包的長度,這樣接收到數據后,通過讀取頭部的長度字段,便知道每一個數據包的實際長度了,再根據長度去讀取指定長度的數據便能獲取到正確的數據了。
再來回顧一下 協議:

完整的數據包 = 服務號 + 數據包長度 + 數據
數據包頭 = Id(4B) + length(4B) 共占用8字節
數據包 = length(假設占100個字節)
所以這條消息的長度就是108字節可以看到,要想知道一條完整數據的邊界,關鍵就是數據包頭中的length字段
實現代碼

-(void) didReadData:(NSData *)data {
    
    //將接收到的數據保存到緩存數據中
    [self.cacheData appendData:data];;

    // 取出4-8位保存的數據長度,計算數據包長度
    NSData *dataLength = [_cacheData subdataWithRange:NSMakeRange(4, 4)];
    int dataLenInt = CFSwapInt32BigToHost(*(int*)([dataLength bytes]));
    NSInteger lengthInteger = 0;
    lengthInteger = (NSInteger)dataLenInt;
    NSInteger complateDataLength = lengthInteger + 8;//算出一個包完整的長度(內容長度+頭長度)
    NSLog(@"data = %ld  ----   length = %d  ",data.length,dataLenInt);
    
    //因為服務號和長度字節占8位,所以大于8才是一個正確的數據包
    while (_cacheData.length > 8) {
        
        if (_cacheData.length < complateDataLength) { //如果緩存中的數據長度小于包頭長度 則繼續拼接

            [[SingletonSocket sharedInstance].socket readDataWithTimeout:-1 tag:0];//socket讀取數據
            break;
            
        }else {
            
            //截取完整數據包
           NSData *dataOne = [_cacheData subdataWithRange:NSMakeRange(0, complateDataLength)];
            [self handleTcpResponseData:dataOne];//處理包數據
            [_cacheData replaceBytesInRange:NSMakeRange(0, complateDataLength) withBytes:nil length:0];
            
            if (_cacheData.length > 8) {
                
                [self didReadData:nil];
                
            }
        }
    }
}

由于公司項目是游戲開發,所以對于數據傳輸高效、穩定性有一定的要求需要數據的實時更新,所以這次用到了Socket通信。因為之前完全沒有這方面的經驗,前期遇到很多坑。所以在這里把自己遇到的一些問題和解決方式總結出來,希望能給后面用到的人一些幫助。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 從三月份找實習到現在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發崗...
    時芥藍閱讀 42,366評論 11 349
  • 個人認為,Goodboy1881先生的TCP /IP 協議詳解學習博客系列博客是一部非常精彩的學習筆記,這雖然只是...
    貳零壹柒_fc10閱讀 5,092評論 0 8
  • 1.這篇文章不是本人原創的,只是個人為了對這部分知識做一個整理和系統的輸出而編輯成的,在此鄭重地向本文所引用文章的...
    SOMCENT閱讀 13,133評論 6 174
  • 簡介 用簡單的話來定義tcpdump,就是:dump the traffic on a network,根據使用者...
    保川閱讀 5,987評論 1 13
  • Good UI是一家研究用戶體驗的設計機構。我們知道成功的頁面設計不僅有很高的轉化率更便于用戶使用,既能滿足商業目...
    張權勝閱讀 379評論 0 4