粘包、拆包?
客戶端或者服務(wù)端不斷的發(fā)送數(shù)據(jù)包時(shí),接收的數(shù)據(jù)會出現(xiàn)兩個(gè)數(shù)據(jù)包粘在一起的情況,這就是TCP協(xié)議中經(jīng)常會遇到的粘包以及拆包的問題。
我們都知道TCP屬于傳輸層的協(xié)議,傳輸層除了有TCP協(xié)議外還有UDP協(xié)議。那么UDP是否會發(fā)生粘包或拆包的現(xiàn)象呢?答案是不會。UDP是基于報(bào)文發(fā)送的,從UDP的幀結(jié)構(gòu)可以看出,在UDP首部采用了16bit來指示UDP數(shù)據(jù)報(bào)文的長度,因此在應(yīng)用層能很好的將不同的數(shù)據(jù)報(bào)文區(qū)分開,從而避免粘包和拆包的問題。
而TCP是基于字節(jié)流的,雖然應(yīng)用層和TCP傳輸層之間的數(shù)據(jù)交互是大小不等的數(shù)據(jù)塊,但是TCP把這些數(shù)據(jù)塊僅僅看成一連串無結(jié)構(gòu)的字節(jié)流,沒有邊界;另外從TCP的幀結(jié)構(gòu)也可以看出,在TCP的首部沒有表示數(shù)據(jù)長度的字段,這種情況由于接收端不知道這兩個(gè)數(shù)據(jù)包的界限,所以對于接收端來說很難處理。
粘包、拆包發(fā)生原因
發(fā)生TCP粘包或拆包有很多原因,現(xiàn)列出常見的幾點(diǎn),可能不全面,歡迎補(bǔ)充,
1、要發(fā)送的數(shù)據(jù)大于TCP發(fā)送緩沖區(qū)剩余空間大小,將會發(fā)生拆包。
2、待發(fā)送數(shù)據(jù)大于MSS(最大報(bào)文長度),TCP在傳輸前將進(jìn)行拆包。
3、要發(fā)送的數(shù)據(jù)小于TCP發(fā)送緩沖區(qū)的大小,TCP將多次寫入緩沖區(qū)的數(shù)據(jù)一次發(fā)送出去,將會發(fā)生粘包。
4、接收數(shù)據(jù)端的應(yīng)用層沒有及時(shí)讀取接收緩沖區(qū)中的數(shù)據(jù),將發(fā)生粘包。
粘包、拆包解決辦法
通過以上分析,我們清楚了粘包或拆包發(fā)生的原因,那么如何解決這個(gè)問題呢?解決問題的關(guān)鍵在于如何給每個(gè)數(shù)據(jù)包添加邊界信息,常用的方法有如下幾個(gè):
1、發(fā)送端給每個(gè)數(shù)據(jù)包添加包首部,首部中應(yīng)該至少包含數(shù)據(jù)包的長度,這樣接收端在接收到數(shù)據(jù)后,通過讀取包首部的長度字段,便知道每一個(gè)數(shù)據(jù)包的實(shí)際長度了。
2、發(fā)送端將每個(gè)數(shù)據(jù)包封裝為固定長度(不夠的可以通過補(bǔ)0填充),這樣接收端每次從接收緩沖區(qū)中讀取固定長度的數(shù)據(jù)就自然而然的把每個(gè)數(shù)據(jù)包拆分開來。
3、可以在數(shù)據(jù)包之間設(shè)置邊界,如添加特殊符號,這樣,接收端通過這個(gè)邊界就可以將不同的數(shù)據(jù)包拆分開。
代碼分析:使用CocoaAsyncSocket
我們用第一種方案:(完整數(shù)據(jù)格式為數(shù)據(jù)長度+數(shù)據(jù)類型+數(shù)據(jù))
數(shù)據(jù)類型和數(shù)據(jù)長度分別占4byte,4byte
所以我們在發(fā)送數(shù)據(jù)時(shí)把數(shù)據(jù)包裝成Length+ type+data即可:
##2 數(shù)據(jù)類型枚舉
typedefNS_ENUM(NSUInteger, TMCommandType) {
TMCommandTypeImg = 1,
TMCommandTypeText = 2,
TMCommandTypeVideo = 3,
};
//發(fā)送時(shí)包裝數(shù)據(jù)
- (void)sendData:(NSData*)data type:(TMCommandType)type{
NSMutableData *mData = [NSMutableData data];
// 計(jì)算數(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){
//讀取前四個(gè)字節(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表示永不超時(shí)
[self.mSocket readDataWithTimeout:-1 tag:10086];
}