粘包皆愉、拆包?
客戶端或者服務(wù)端不斷的發(fā)送數(shù)據(jù)包時艇抠,接收的數(shù)據(jù)會出現(xiàn)兩個數(shù)據(jù)包粘在一起的情況幕庐,這就是TCP協(xié)議中經(jīng)常會遇到的粘包以及拆包的問題。
我們都知道TCP屬于傳輸層的協(xié)議家淤,傳輸層除了有TCP協(xié)議外還有UDP協(xié)議异剥。那么UDP是否會發(fā)生粘包或拆包的現(xiàn)象呢?答案是不會絮重。UDP是基于報文發(fā)送的冤寿,從UDP的幀結(jié)構(gòu)可以看出,在UDP首部采用了16bit來指示UDP數(shù)據(jù)報文的長度青伤,因此在應(yīng)用層能很好的將不同的數(shù)據(jù)報文區(qū)分開督怜,從而避免粘包和拆包的問題。
而TCP是基于字節(jié)流的狠角,雖然應(yīng)用層和TCP傳輸層之間的數(shù)據(jù)交互是大小不等的數(shù)據(jù)塊号杠,但是TCP把這些數(shù)據(jù)塊僅僅看成一連串無結(jié)構(gòu)的字節(jié)流,沒有邊界丰歌;另外從TCP的幀結(jié)構(gòu)也可以看出姨蟋,在TCP的首部沒有表示數(shù)據(jù)長度的字段辣吃,這種情況由于接收端不知道這兩個數(shù)據(jù)包的界限,所以對于接收端來說很難處理芬探。
粘包神得、拆包發(fā)生原因
發(fā)生TCP粘包或拆包有很多原因,現(xiàn)列出常見的幾點(diǎn)偷仿,可能不全面哩簿,歡迎補(bǔ)充,
1酝静、要發(fā)送的數(shù)據(jù)大于TCP發(fā)送緩沖區(qū)剩余空間大小节榜,將會發(fā)生拆包。
2别智、待發(fā)送數(shù)據(jù)大于MSS(最大報文長度)宗苍,TCP在傳輸前將進(jìn)行拆包。
3薄榛、要發(fā)送的數(shù)據(jù)小于TCP發(fā)送緩沖區(qū)的大小讳窟,TCP將多次寫入緩沖區(qū)的數(shù)據(jù)一次發(fā)送出去,將會發(fā)生粘包敞恋。
4丽啡、接收數(shù)據(jù)端的應(yīng)用層沒有及時讀取接收緩沖區(qū)中的數(shù)據(jù),將發(fā)生粘包硬猫。
粘包补箍、拆包解決辦法
通過以上分析,我們清楚了粘包或拆包發(fā)生的原因啸蜜,那么如何解決這個問題呢坑雅?解決問題的關(guān)鍵在于如何給每個數(shù)據(jù)包添加邊界信息,常用的方法有如下幾個:
1衬横、發(fā)送端給每個數(shù)據(jù)包添加包首部裹粤,首部中應(yīng)該至少包含數(shù)據(jù)包的長度,這樣接收端在接收到數(shù)據(jù)后冕香,通過讀取包首部的長度字段蛹尝,便知道每一個數(shù)據(jù)包的實(shí)際長度了。
2悉尾、發(fā)送端將每個數(shù)據(jù)包封裝為固定長度(不夠的可以通過補(bǔ)0填充)突那,這樣接收端每次從接收緩沖區(qū)中讀取固定長度的數(shù)據(jù)就自然而然的把每個數(shù)據(jù)包拆分開來。
3构眯、可以在數(shù)據(jù)包之間設(shè)置邊界愕难,如添加特殊符號,這樣,接收端通過這個邊界就可以將不同的數(shù)據(jù)包拆分開猫缭。
代碼分析:使用CocoaAsyncSocket
我們用第一種方案:(完整數(shù)據(jù)格式為數(shù)據(jù)長度+數(shù)據(jù)類型+數(shù)據(jù))
數(shù)據(jù)類型和數(shù)據(jù)長度分別占4byte,4byte
所以我們在發(fā)送數(shù)據(jù)時把數(shù)據(jù)包裝成Length+ type+data即可:
##2 數(shù)據(jù)類型枚舉
typedefNS_ENUM(NSUInteger, TMCommandType) {
TMCommandTypeImg = 1,
TMCommandTypeText = 2,
TMCommandTypeVideo = 3,
};
//發(fā)送時包裝數(shù)據(jù)
- (void)sendData:(NSData*)data type:(TMCommandType)type{
NSMutableData *mData = [NSMutableData data];
// 計算數(shù)據(jù)總長度 data
unsignedintdataLength =4+4+(int)data.length;
NSData*lengthData = [NSDatadataWithBytes:&dataLengthlength:4];
[mData appendData:lengthData];
// 數(shù)據(jù)類型 data
// 2.拼接指令類型(4~7:指令)
NSData *typeData = [NSData dataWithBytes:&type length:4];
[mData appendData:typeData];
// 最后拼接數(shù)據(jù)
[mData appendData:data];
NSLog(@"發(fā)送數(shù)據(jù)的總字節(jié)大小:%ld",mData.length);
// 發(fā)數(shù)據(jù)
[self.mSocket writeData:mData withTimeout:-1 tag:999];
}
//接收服務(wù)器返回來的數(shù)據(jù) 拆包
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{
NSLog(@"總共接收到tag = %ld : %ld 長度的數(shù)據(jù)",tag,data.length);
if (data.length == 0) {
return;
}
// 1.第一次接收數(shù)據(jù)
if(self.mData.length == 0){
//讀取前四個字節(jié) 數(shù)據(jù)包大小length
NSData *totalSizeData = [data subdataWithRange:NSMakeRange(0, 4)];
unsigned int totalSize = 0;
[totalSizeData getBytes:&totalSize length:4];
self.mTotalSize = totalSize;
// 獲取指令類型Type
NSData *commandIdData = [data subdataWithRange:NSMakeRange(4, 4)];
unsigned int commandId = 0;
[commandIdData getBytes:&commandId length:4];
self.mCurrentCommandId = commandId;
}
//拼接數(shù)據(jù)
[self.mData appendData:data];
if (self.mData.length == self.mTotalSize) {
//數(shù)據(jù)內(nèi)容
NSData *data_data = [self.mData subdataWithRange:NSMakeRange(8, self.mData.length - 8)];
NSLog(@"數(shù)據(jù)已經(jīng)接收完成");
if (self.mCurrentCommandId == TMCommandTypeImg) {
NSLog(@"接收到圖片");
[self saveImage: data_data];
}else if (self.mCurrentCommandId == TMCommandTypeVideo){
NSLog(@"接收到視頻");
}else if (self.mCurrentCommandId == TMCommandTypeText){
NSLog(@"接收到文本");
}
// 清除數(shù)據(jù)
self.mData = [NSMutableData data];
};
//-1表示永不超時
[self.mSocket readDataWithTimeout:-1 tag:10086];
}