亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb

首頁 > 學院 > 開發設計 > 正文

即時通訊下數據粘包、斷包處理實例(基于CocoaAsyncSocket)

2019-11-09 15:17:02
字體:
來源:轉載
供稿:網友
前言

本文旨以實例的方式,使用CocoaAsyncSocket這個框架進行數據封包和拆包。來解決頻繁的數據發送下,導致的數據粘包、以及較大數據(例如圖片、錄音等等)的發送,導致的數據斷包。

本文實例Github地址:即時通訊的數據粘包、斷包處理實例。

注:文章內容屬于應用的范疇,內容相對簡單易懂。給大家對數據包的處理提供了一個思路, 希望能拋磚引玉。它是樓主CocoaAsyncSocket系列Read篇解析的一個前置插曲,至于詳細的實現原理,作者會在后續的文章中寫出。

正文
一、什么是粘包?

經常我們發現,如果用客戶端同一時間發送幾條數據,而服務端只能收到一大條數據,類似下圖:

如圖,由于傳輸的過程為數據流,經過TCP傳輸后,三條數據被合并成了一條,這就是數據粘包了。

那么為什么會造成粘包呢?

原來這是因為TCP使用了優化方法(Nagle算法)。它將多次間隔較小且數據量小的數據,合并成一個大的數據塊,然后進行封包。這么做優點也很明顯,就是為了減少廣域網的小分組數目,從而減小網絡擁塞的出現。

具體的內容感興趣的可以看看這兩篇文章:TCP之Nagle算法&&延遲ACKTCP NAGLE算法和實現

而UDP就不會有這種情況,它不會使用塊的合并優化算法。這里說到了就順便提一下,由于它支持的是一對多的模式,所以接收端的skbuff(套接字緩沖區)采用了鏈式結構來記錄每一個到達的UDP包,在每個UDP包中就有了消息頭(消息來源地址,端口等信息)。

當然除了優化算法,TCP和UDP都會因為下面兩種情況造成粘包:

發送端需要等緩沖區滿才發送出去,造成粘包接收方不及時接收緩沖區的包,造成多個包接收。
二、什么是斷包?

斷包應該還是比較好理解的,比如我們發送一條很大的數據包,類似圖片和錄音等等,很顯然一次發送或者讀取數據的緩沖區大小是有限的,所以我們會分段去發送或者讀取數據。類似下圖:

無論是粘包還是斷包,如果我們要正確解析數據,那么必須要使用一種合理的機制去解包。這個機制的思路其實很簡單:

我們在封包的時候給每個數據包加一個長度或者一個開始結束標記。然后我們拆包的時候就能區分每個數據包了,再按照長度或者分解符去分拆成各個數據包。

Talk is cheap. Show me the code

三、實例:基于CocoaAsyncSocket的封包,拆包處理。

開始動手之前,我們需要去理解下面這幾個方法

//讀取數據,有數據就會觸發代理- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag;//直到讀到這個長度的數據,才會觸發代理- (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag;//直到讀到data這個邊界,才會觸發代理- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag;

還記得我們之前講:iOS即時通訊,從入門到“放棄”?中提到過,這個框架每次讀取數據,必須手動的去調用上述這些read方法,而我們之前的實現思路是,第一次連接成功的代理觸發后調用:

- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag;

之后每次收到消息之后,都在去調用一次這個方法,超時為-1,即不超時。這樣我們每次收到消息,都會即時觸發我們讀取消息的代理:

- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag

然而這么做顯然沒有考慮數據的拆包,如果我們一條一條的發送文字信息,自然沒什么問題。如果我們一次發送數條,或者發送大圖片。那么問題就出來了,我們解析出來的數據顯然是不對的。

這時候我們就需要另外兩個read方法了,一個是讀取到指定長度,另一個是讀取到指定邊界。我們通過自己定義的數據邊界,去調用這兩個方法,而觸發的讀取代理,得到的數據才是正確的一個包的數據。

所以我們的核心思路有了:
封包的時候給每個包的數據加一個標記,來標明數據的長度和類型(類型顯然是需要的,我們需要知道它是文本、圖片、還是錄音等等,來用正確的方式處理這個數據)。拆包的時候,先獲取到我們給每個包的標記,然后根據標記的數據長度,去獲取數據。最后再根據標記的類型去處理數據。(文字輸出、圖片展示、錄音播放等等)。

接著我們可以開始動手了:這里我們首先需要一個服務端,一個客戶端。為了簡單,我們都用OC來實現。

其中我們客戶端用手機,服務端我們用Xcode模擬器。(由于Xcode只能同一時間運行一個模擬器...)

這里我們用客戶端封包發送數據,然后服務端拆包解析數據。

我們先來看看客戶端的代碼:

static NSString * Khost = @"10.10.100.48";static const uint16_t Kport = 6969;//建立連接- (BOOL)connect{ return [gcdSocket connectToHost:Khost onPort:Kport error:nil];}

初始化略過了,大家可以看看github中的代碼,這里需要說的是,為了連接上本機的服務端,我們這里的host為服務端的ip地址:

端口為6969(只需和服務端accpet端口一致即可)。

注意:如果大家要運行github上的demo,只需修改這個host地址即可,把它改成你電腦(服務端)的IP地址。

接著我們來看看write方法,我們在該方法中進行封包:

//發送消息- (void)sendMsg{ NSData *data = [@"你好" dataUsingEncoding:NSUTF8StringEncoding]; NSData *data1 = [@"豬頭" dataUsingEncoding:NSUTF8StringEncoding]; NSData *data2 = [@"先生" dataUsingEncoding:NSUTF8StringEncoding]; NSData *data3 = [@"今天天氣好" dataUsingEncoding:NSUTF8StringEncoding]; NSData *data4 = [@"吃飯了嗎" dataUsingEncoding:NSUTF8StringEncoding]; [self sendData:data :@"txt"]; [self sendData:data1 :@"txt"]; [self sendData:data2 :@"txt"]; [self sendData:data3 :@"txt"]; [self sendData:data4 :@"txt"]; NSString *filePath = [[NSBundle mainBundle]pathForResource:@"test1" ofType:@"jpg"]; NSData *data5 = [NSData dataWithContentsOfFile:filePath]; [self sendData:data5 :@"img"];}- (void)sendData:(NSData *)data :(NSString *)type{ NSUInteger size = data.length; NSMutableDictionary *headDic = [NSMutableDictionary dictionary]; [headDic setObject:type forKey:@"type"]; [headDic setObject:[NSString stringWithFormat:@"%ld",size] forKey:@"size"]; NSString *jsonStr = [self dictionaryToJson:headDic]; NSData *lengthData = [jsonStr dataUsingEncoding:NSUTF8StringEncoding]; NSMutableData *mData = [NSMutableData dataWithData:lengthData]; //分界 [mData appendData:[GCDAsyncSocket CRLFData]]; [mData appendData:data]; //第二個參數,請求超時時間 [gcdSocket writeData:mData withTimeout:-1 tag:110];}- (NSString *)dictionaryToJson:(NSDictionary *)dic{ NSError *error = nil; NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dic options:NSJSONWritingPRettyPrinted error:&error]; return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];}

總共上述兩個方法,也很簡單,我們發送了6條數據,前5條為文本形式,最后一條是一個20多M的圖片。當我們點擊發送的時候會觸發這個方法,這6條數據會被同時發出。

這里我們來看看我們是如何封包的:

我們定義了一個headDic,這個是我們數據包的頭部,里面裝了這個數據包的大小和類型信息(當然,你可以裝更多的其他標識信息。)然后我們把它轉成了json,最后轉成data。然后我們把這個head拼在最前面,接著拼了一個:[GCDAsyncSocket CRLFData]這個是什么呢?其實它就是一個/r/n。我們用它來做頭部的邊界。(又或者我們可以規定一個固定的頭部長度,來作為邊界,這里僅僅是提供給大家一個思路)。最后我們把真正的數據包給拼接上。

注:如果你想的更遠的話,甚至可以在結尾,再拼一個包結束的標識符,后面我們會講到為什么可以這么做。這里暫時先這樣。

就這樣,我們完成了數據的封包和發送。

客戶端有了,接著我們來看看服務端是如何來拆包的:

首先我們需要監聽本機6969端口。(完整代碼可以見github)

static const uint16_t Kport = 6969;//等待連接- (BOOL)accept{ NSError *error = nil; BOOL isSuccess = [gcdSocket acceptOnPort:Kport error:&error]; if (isSuccess) { NSLog(@"監聽成功6969端口成功,等待連接"); return YES; }else{ NSLog(@"監聽失敗,原因:%@",error); return NO; }}

當客戶端連接上來后,調用成功接收到客戶端連接的代理方法:

- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket{ NSLog(@"接受到socket連接"); [_sockets addObject:newSocket]; [newSocket readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:110];}

這里需要注意的是,成功接收到連接后,調用代理我們必須把新生成的這個newSocket保存起來,如果它被銷毀了,那么連接就斷開了,這里我們把它放到了一個數組中去了。這里需要注意的是,成功連接后,我們就調用了:

[newSocket readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:110];

還記得我們封包的時候,數據包頭部之后拼了這么一個分解符data。這樣,當有數據包傳輸過來我們就能獲取到這個數據包的頭部(后面的信息先不讀取)。

接著我們來看看服務端的read代理方法是如何拆包的:

- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{ //先讀取到當前數據包頭部信息 if (!currentPacketHead) { currentPacketHead = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil]; NSUInteger packetLength = [currentPacketHead[@"size"] integerValue]; //讀到數據包的大小 [sock readDataToLength:packetLength withTimeout:-1 tag:110]; return; } if (!currentPacketHead) { NSLog(@"error:當前數據包的頭為空"); //斷開連接 [self disConnect]; return; } //正式的包處理 NSUInteger packetLength = [currentPacketHead[@"size"] integerValue]; //說明數據有問題 if (packetLength <= 0 || data.length != packetLength) { NSLog(@"error:當前數據包數據大小不正確"); [self disConnect]; return; } NSString *type = currentPacketHead[@"type"]; if ([type isEqualToString:@"img"]) { NSLog(@"圖片設置成功"); self.recvImg.image = [UIImage imageWithData:data]; }else{ NSString *msg = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding]; NSLog(@"收到消息:%@",msg); } currentPacketHead = nil; [sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:110];}

這個方法也很簡單,我們判斷,如果currentPacketHead(當前數據包的頭部)為空,則說明這次讀取,是一個頭部信息,我們去獲取到該數據包的頭部信息。并且調用下一次讀取,讀取長度為從頭部信息中取出來的數據包長度:

[sock readDataToLength:packetLength withTimeout:-1 tag:110];

這樣當GCDAsyncSocket中數據緩沖區長度達到我們需要讀取的length就能觸發代理方法的第二次回調。(具體原理實現會在樓主的GCDAsyncSocket解析的后續系列Read篇中去講,敬請期待)。這時候因為currentPacketHead不為空,所以我們就知道是去獲取一個數據包,我們從頭部信息中拿到數據包的類型,如果是文本或者圖片,則分別輸出或展示到屏幕上。讀取完成后我們再次調用:

[sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:110];

這樣就開始了下一個數據包的頭部信息讀取。就這樣,整個數據拆包的處理就完成了。

接著我們來講講我們之前所說的為什么可以在數據包之后加一個結束標識符。我們數據很可能在傳輸的過程中,丟失了一部分,或者頭部信息不可讀,導致我們無法正常讀取這個數據包。可能我們會有一個應用場景,當出現錯誤包的時候,我們就直接拋棄掉它,直接開始下一個數據包的讀取(當然現實中,我們往往是需要重新發送,這里僅僅是舉一個應用場景)。這樣這個結束標識符就起作用了,我們可以直接把數據讀取到這個錯誤包的結束標識處,不做任何處理,這樣相當于丟棄掉這個錯誤包了。

最后我們來看看運行效果:

我們客戶端手機連接上服務器后,點擊發送,發出我們上述客戶端寫的6條數據,在我們服務端,按照順序接受到數據如圖:

寫在結尾:

本來不打算寫應用篇的,但是很多朋友在問數據包相關的內容,而且正好之后的Read篇會涉及到這些,所以就當為了后面的內容做一個鋪墊吧。

關于IM的路還有很長,路漫漫其修遠兮,吾將上下而求索。


發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb
欧美成人精品在线观看| 精品国产欧美一区二区三区成人| 久久久久久久久久久免费| 亚洲综合视频1区| 55夜色66夜色国产精品视频| 亚洲欧美日韩图片| 91国产美女视频| 色综合男人天堂| 91在线观看免费| 亚洲国产精品va在线观看黑人| 中文在线资源观看视频网站免费不卡| 欧美性生交大片免费| 精品久久久久久国产| 亚洲第一中文字幕在线观看| 91视频免费网站| 亚洲人成77777在线观看网| 日韩免费电影在线观看| 国产综合香蕉五月婷在线| 中文字幕日韩视频| 丝袜情趣国产精品| 亚洲欧洲第一视频| 久久久999国产| 国产精品久久久久久久午夜| 日韩在线视频观看| 91亚洲国产成人精品性色| 亚洲精品97久久| 国产精品永久免费视频| 国产成人精品免高潮在线观看| 51精品国产黑色丝袜高跟鞋| 国产精品99久久久久久久久久久久| 激情成人在线视频| 97视频在线观看视频免费视频| 亚洲国产中文字幕久久网| 91成品人片a无限观看| 国产一区二区三区丝袜| 久久久免费观看视频| 日韩国产一区三区| 亚洲精品国产拍免费91在线| 国产精品一区av| 亚洲欧美在线一区| 亚洲成人在线视频播放| 色婷婷av一区二区三区久久| 国产成人精品在线视频| 色妞色视频一区二区三区四区| 一本一本久久a久久精品牛牛影视| 91老司机在线| 欧美成年人视频网站欧美| 亚洲天堂网在线观看| 欧美成人三级视频网站| 亚洲精品成a人在线观看| 欧美老少配视频| 欧美大片在线免费观看| 久久精品福利视频| 亚洲男人的天堂网站| 韩国精品美女www爽爽爽视频| 一个人看的www欧美| 国产精品久久久久久久av大片| 色偷偷偷综合中文字幕;dd| 欧美性猛交xxxx久久久| 另类图片亚洲另类| 欧美精品九九久久| 欧美自拍大量在线观看| 国产日韩欧美在线播放| 国产成人短视频| 一区二区三区黄色| 欧美激情在线观看| 亚洲免费av片| 欧美日韩午夜视频在线观看| 日韩a**中文字幕| 国产日韩精品在线播放| 亚洲一区二区三区视频播放| 亚洲国产中文字幕久久网| 久久久久久成人精品| 91精品综合视频| 亚洲第一区在线观看| 国产视频福利一区| 北条麻妃一区二区三区中文字幕| 国产精品网站大全| 自拍偷拍亚洲区| 91成品人片a无限观看| 日韩精品久久久久久福利| 亚洲aaaaaa| 日韩激情视频在线| 日韩成人av网| 亚洲第一国产精品| 毛片精品免费在线观看| 91精品国产91久久久久久久久| 青青a在线精品免费观看| 国产伦精品一区二区三区精品视频| 中文字幕亚洲二区| 国产欧美 在线欧美| 国产成人在线亚洲欧美| 91av在线免费观看| 国产精品美女午夜av| 91精品视频播放| 欧美电影免费观看高清| 91在线国产电影| 国产日韩欧美电影在线观看| 国产精品久久久久久久久久ktv| 久久精品99久久久香蕉| 亚洲男人7777| 国产精品美女免费看| 亚洲在线www| 欧美激情亚洲精品| 欧美日韩国产丝袜另类| 九九热这里只有在线精品视| 欧美另类在线播放| 日韩欧美福利视频| 日韩在线视频一区| 亚洲黄色有码视频| 成人h片在线播放免费网站| 久久91精品国产91久久跳| 亚洲国产精品久久精品怡红院| 欧美黑人极品猛少妇色xxxxx| 欧美巨大黑人极品精男| 日韩欧美在线第一页| 国产精品高潮呻吟久久av野狼| 成人午夜小视频| 亚洲男人天堂手机在线| 久久精品国产视频| 美女啪啪无遮挡免费久久网站| 亚洲一二三在线| 国产精品久久久久久久一区探花| 亚洲男人的天堂在线| 国产视频精品va久久久久久| 亚洲理论电影网| 国产精品一区二区三区毛片淫片| 日韩av在线一区二区| 欧美成人午夜影院| 亚洲二区中文字幕| 欧美猛少妇色xxxxx| 亚洲色图13p| 久久综合国产精品台湾中文娱乐网| 亚洲三级免费看| 国产suv精品一区二区三区88区| 国产亚洲精品高潮| 91久久久久久久久久| 成人国产精品av| 萌白酱国产一区二区| 精品亚洲国产成av人片传媒| 亚洲最大av网| 最新国产精品亚洲| 国产精品吹潮在线观看| 一级做a爰片久久毛片美女图片| 91av成人在线| 欧美激情一级精品国产| 亚洲自拍另类欧美丝袜| 黑人欧美xxxx| 91精品国产自产在线老师啪| 国产精品网站大全| 国产精品白嫩美女在线观看| 亚洲欧美中文日韩在线v日本| 中文字幕精品www乱入免费视频| 欧美激情在线播放| 国产不卡av在线免费观看| 日韩国产精品亚洲а∨天堂免| 日韩中文字幕免费视频| 欧美国产欧美亚洲国产日韩mv天天看完整| 精品人伦一区二区三区蜜桃网站| 91精品在线观看视频| 亚洲国产精品成人va在线观看| 欧美日韩福利视频| 成人激情av在线| 国产一区私人高清影院|