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

一明垢、封包

在iOS很多應(yīng)用開發(fā)中蚣常,大部分用的網(wǎng)絡(luò)通信都是http/https協(xié)議,除非有特殊的需求會(huì)用到Socket網(wǎng)絡(luò)協(xié)議進(jìn)行網(wǎng)絡(luò)數(shù)據(jù)傳輸袖外,這時(shí)候在iOS客戶端就需要很好的第三方CocoaAsyncSocket 來進(jìn)行長(zhǎng)連接連接和傳輸數(shù)據(jù)史隆,讀者可以自行查閱資料搜索這個(gè)庫的用法。

一般在使用Socket的時(shí)候曼验,后臺(tái)會(huì)對(duì)Socket傳輸數(shù)據(jù)有一個(gè)自定義的協(xié)議泌射,協(xié)議可能有些差別不過基本上是大同小異。 如圖


也就是說我們通過Socket發(fā)送給服務(wù)器的數(shù)據(jù)鬓照,最終要轉(zhuǎn)換成二進(jìn)制流數(shù)據(jù)熔酷,并且按照協(xié)議約定的格式。

下面我簡(jiǎn)單解釋下這個(gè)協(xié)議豺裆,因?yàn)橐婚_始我自己也不是很理解拒秘。這個(gè)協(xié)議是指我們?cè)诎l(fā)送的數(shù)據(jù)包頭部開辟一個(gè)4個(gè)字節(jié)長(zhǎng)度的空間号显,用來存儲(chǔ)服務(wù)號(hào)轉(zhuǎn)換成的二進(jìn)制數(shù)據(jù)。(將1轉(zhuǎn)換成二進(jìn)制數(shù)據(jù)存儲(chǔ)進(jìn)去占4個(gè)字節(jié)長(zhǎng)度)躺酒,然后再將數(shù)據(jù)包長(zhǎng)度轉(zhuǎn)換成二進(jìn)制數(shù)據(jù)并存儲(chǔ)到后面開辟的4個(gè)字節(jié)中(這里需要注意下如果數(shù)據(jù)要進(jìn)行加密傳輸押蚤,這里的長(zhǎng)度應(yīng)是加密后的長(zhǎng)度),最后將數(shù)據(jù)數(shù)據(jù)包轉(zhuǎn)換成二進(jìn)制數(shù)據(jù)添加到后面羹应,組成一個(gè)完整的數(shù)據(jù)包也就是封包揽碘。這里一定要按協(xié)議規(guī)定的順序不然服務(wù)器解析不了。
具體使用見代碼

    NSMutableDictionary *dictTemp = [NSMutableDictionary dictionary];
    dictTemp[@"username"]         = @"LD";
    
    //先創(chuàng)建模型 --> 轉(zhuǎn)Json -->轉(zhuǎn)字符串
    TestModel *model = [TestModel new];
    model.type       = 1;
    model.userName   = @"LD";
    model.age        = @"18";
    model.message    = @"Hellow";
    model.Content    = dictTemp;
    
    //先將模型轉(zhuǎn)換成Json格式的數(shù)據(jù)這里根據(jù)自己項(xiàng)目情況來看是否需要轉(zhuǎn)成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;

    //將數(shù)據(jù)傳換成二進(jìn)制數(shù)據(jù),轉(zhuǎn)換之后的數(shù)據(jù)和協(xié)議順序是一致的(為什么不需要調(diào)整順序我也不知道雳刺,有興趣的的同學(xué)自己去研究下這個(gè)方法)
    NSMutableData *dataModel =  [socket RequestSpliceAttribute:connect];
    
    // 通過Socket發(fā)出去
    [socket sendMessage:dataModel];
轉(zhuǎn)為二進(jìn)制數(shù)據(jù)
//  將模型數(shù)據(jù)轉(zhuǎn)換成二進(jìn)制數(shù)據(jù)
-(NSMutableData *)RequestSpliceAttribute:(id)obj{

    _data = nil;//記得清空不然數(shù)據(jù)包會(huì)越來越大
    if (obj == nil) {
        self.object = self.data;
        
        NSLog(@"傳入需轉(zhuǎn)二進(jìn)制的數(shù)據(jù)為空");
        return nil;
     }
    unsigned int numIvars; //成員變量個(gè)數(shù)
    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]; //獲取成員變量的數(shù)據(jù)類型
//                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轉(zhuǎn)為data
            [self.data appendData:data];
            
        }else {
            NSLog(@"RequestSpliceAttribute:未知類型");
            NSAssert(YES, @"RequestSpliceAttribute:未知類型");
        }
    }
    
    // hy: 記得釋放C語言的結(jié)構(gòu)體指針
    free(propertys);
    self.object = _data;
    return _data;
}

轉(zhuǎn)為二進(jìn)制代碼鏈接:http://pan.baidu.com/s/1hsi7tNQ密碼: byiy
關(guān)于轉(zhuǎn)碼更詳細(xì)的說明請(qǐng)看下面的鏈接
參考資料:iOS開發(fā)之Socket通信實(shí)戰(zhàn)--Request請(qǐng)求數(shù)據(jù)包編碼模塊

二、粘包裸违、拆包處理

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

讓我們來看一個(gè)例子:我們假設(shè)在主機(jī)A和主機(jī)B的應(yīng)用程序之間有一條TCP連接,主機(jī)A有兩條報(bào)文D1,D2要發(fā)送到B主機(jī)朱监,并兩次調(diào)用send來發(fā)送岸啡,每條報(bào)文調(diào)用一次。


那么赫编,我們自然而然的希望兩條報(bào)文是作為兩個(gè)獨(dú)立的實(shí)體巡蘸,在各自的分組中發(fā)送,如圖1:


這樣的話擂送,我們無需做任何特別的處理悦荒,便能夠很容易的區(qū)分每一個(gè)獨(dú)立的數(shù)據(jù),并根據(jù)需求分別做相應(yīng)的處理嘹吨。但現(xiàn)實(shí)往往是有所偏差的搬味,實(shí)際的數(shù)據(jù)傳輸過程很可能不會(huì)遵循這個(gè)模型。而是會(huì)采用以下四種方式之一進(jìn)行傳輸蟀拷。如圖2:


  • D1和D2數(shù)據(jù)作為兩個(gè)獨(dú)立的分組碰纬,分別到達(dá)主機(jī)B;
  • D1和D2合為一個(gè)整體組问芬,一起到達(dá)主機(jī)B悦析;
  • D1的部分?jǐn)?shù)據(jù)先到達(dá)主機(jī)B,剩下的D1數(shù)據(jù)和D2和在一組到達(dá)主機(jī)B此衅;
  • D1和D2的部分?jǐn)?shù)據(jù)先到達(dá)主機(jī)B强戴, D2后到達(dá)主機(jī)B亭螟;
    實(shí)際上,可能的情況還不止4種骑歹,這里我們就不做深入了解预烙,以上就是造成粘包的原因。
解決思路:拆包

在上面說到我們給每個(gè)數(shù)據(jù)包添加頭部道媚,頭部中包含數(shù)據(jù)包的長(zhǎng)度默伍,這樣接收到數(shù)據(jù)后,通過讀取頭部的長(zhǎng)度字段衰琐,便知道每一個(gè)數(shù)據(jù)包的實(shí)際長(zhǎng)度了,再根據(jù)長(zhǎng)度去讀取指定長(zhǎng)度的數(shù)據(jù)便能獲取到正確的數(shù)據(jù)了炼蹦。
再來回顧一下 協(xié)議:

完整的數(shù)據(jù)包 = 服務(wù)號(hào) + 數(shù)據(jù)包長(zhǎng)度 + 數(shù)據(jù)
數(shù)據(jù)包頭 = Id(4B) + length(4B) 共占用8字節(jié)
數(shù)據(jù)包 = length(假設(shè)占100個(gè)字節(jié))
所以這條消息的長(zhǎng)度就是108字節(jié)可以看到羡宙,要想知道一條完整數(shù)據(jù)的邊界,關(guān)鍵就是數(shù)據(jù)包頭中的length字段
實(shí)現(xiàn)代碼

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

    // 取出4-8位保存的數(shù)據(jù)長(zhǎng)度掐隐,計(jì)算數(shù)據(jù)包長(zhǎng)度
    NSData *dataLength = [_cacheData subdataWithRange:NSMakeRange(4, 4)];
    int dataLenInt = CFSwapInt32BigToHost(*(int*)([dataLength bytes]));
    NSInteger lengthInteger = 0;
    lengthInteger = (NSInteger)dataLenInt;
    NSInteger complateDataLength = lengthInteger + 8;//算出一個(gè)包完整的長(zhǎng)度(內(nèi)容長(zhǎng)度+頭長(zhǎng)度)
    NSLog(@"data = %ld  ----   length = %d  ",data.length,dataLenInt);
    
    //因?yàn)榉?wù)號(hào)和長(zhǎng)度字節(jié)占8位狗热,所以大于8才是一個(gè)正確的數(shù)據(jù)包
    while (_cacheData.length > 8) {
        
        if (_cacheData.length < complateDataLength) { //如果緩存中的數(shù)據(jù)長(zhǎng)度小于包頭長(zhǎng)度 則繼續(xù)拼接

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

由于公司項(xiàng)目是游戲開發(fā),所以對(duì)于數(shù)據(jù)傳輸高效虑省、穩(wěn)定性有一定的要求需要數(shù)據(jù)的實(shí)時(shí)更新匿刮,所以這次用到了Socket通信。因?yàn)橹巴耆珱]有這方面的經(jīng)驗(yàn)探颈,前期遇到很多坑熟丸。所以在這里把自己遇到的一些問題和解決方式總結(jié)出來,希望能給后面用到的人一些幫助伪节。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末光羞,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子怀大,更是在濱河造成了極大的恐慌纱兑,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件化借,死亡現(xiàn)場(chǎng)離奇詭異潜慎,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)蓖康,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門铐炫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蒜焊,你說我怎么就攤上這事驳遵。” “怎么了山涡?”我有些...
    開封第一講書人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵堤结,是天一觀的道長(zhǎng)唆迁。 經(jīng)常有香客問我,道長(zhǎng)竞穷,這世上最難降的妖魔是什么唐责? 我笑而不...
    開封第一講書人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮瘾带,結(jié)果婚禮上鼠哥,老公的妹妹穿的比我還像新娘。我一直安慰自己看政,他們只是感情好朴恳,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著允蚣,像睡著了一般于颖。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上嚷兔,一...
    開封第一講書人閱讀 51,631評(píng)論 1 305
  • 那天森渐,我揣著相機(jī)與錄音,去河邊找鬼冒晰。 笑死同衣,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的壶运。 我是一名探鬼主播耐齐,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼蒋情!你這毒婦竟也來了蚪缀?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤恕出,失蹤者是張志新(化名)和其女友劉穎询枚,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體浙巫,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡金蜀,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了的畴。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片渊抄。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖丧裁,靈堂內(nèi)的尸體忽然破棺而出护桦,到底是詐尸還是另有隱情,我是刑警寧澤煎娇,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布二庵,位于F島的核電站贪染,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏催享。R本人自食惡果不足惜杭隙,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望因妙。 院中可真熱鬧痰憎,春花似錦、人聲如沸攀涵。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽以故。三九已至蜗细,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間据德,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工跷车, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留棘利,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓朽缴,卻偏偏與公主長(zhǎng)得像善玫,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子密强,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 從三月份找實(shí)習(xí)到現(xiàn)在茅郎,面了一些公司,掛了不少或渤,但最終還是拿到小米系冗、百度、阿里薪鹦、京東掌敬、新浪、CVTE池磁、樂視家的研發(fā)崗...
    時(shí)芥藍(lán)閱讀 42,253評(píng)論 11 349
  • 個(gè)人認(rèn)為奔害,Goodboy1881先生的TCP /IP 協(xié)議詳解學(xué)習(xí)博客系列博客是一部非常精彩的學(xué)習(xí)筆記,這雖然只是...
    貳零壹柒_fc10閱讀 5,054評(píng)論 0 8
  • 1.這篇文章不是本人原創(chuàng)的地熄,只是個(gè)人為了對(duì)這部分知識(shí)做一個(gè)整理和系統(tǒng)的輸出而編輯成的华临,在此鄭重地向本文所引用文章的...
    SOMCENT閱讀 13,068評(píng)論 6 174
  • 簡(jiǎn)介 用簡(jiǎn)單的話來定義tcpdump,就是:dump the traffic on a network端考,根據(jù)使用者...
    保川閱讀 5,956評(píng)論 1 13
  • Good UI是一家研究用戶體驗(yàn)的設(shè)計(jì)機(jī)構(gòu)雅潭。我們知道成功的頁面設(shè)計(jì)不僅有很高的轉(zhuǎn)化率更便于用戶使用揭厚,既能滿足商業(yè)目...
    張權(quán)勝閱讀 377評(píng)論 0 4