粘包、拆包?
客户端或者服务端不断的发送数据包时,接收的数据会出现两个数据包粘在一起的情况,这就是TCP协议中经常会遇到的粘包以及拆包的问题。
我们都知道TCP属于传输层的协议,传输层除了有TCP协议外还有UDP协议。那么UDP是否会发生粘包或拆包的现象呢?答案是不会。UDP是基于报文发送的,从UDP的帧结构可以看出,在UDP首部采用了16bit来指示UDP数据报文的长度,因此在应用层能很好的将不同的数据报文区分开,从而避免粘包和拆包的问题。
而TCP是基于字节流的,虽然应用层和TCP传输层之间的数据交互是大小不等的数据块,但是TCP把这些数据块仅仅看成一连串无结构的字节流,没有边界;另外从TCP的帧结构也可以看出,在TCP的首部没有表示数据长度的字段,这种情况由于接收端不知道这两个数据包的界限,所以对于接收端来说很难处理。
粘包、拆包发生原因
发生TCP粘包或拆包有很多原因,现列出常见的几点,可能不全面,欢迎补充,
1、要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。
2、待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。
3、要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。
4、接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。
粘包、拆包解决办法
通过以上分析,我们清楚了粘包或拆包发生的原因,那么如何解决这个问题呢?解决问题的关键在于如何给每个数据包添加边界信息,常用的方法有如下几个:
1、发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。
2、发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
3、可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。
代码分析:使用CocoaAsyncSocket
我们用第一种方案:(完整数据格式为数据长度+数据类型+数据)
数据类型和数据长度分别占4byte,4byte
所以我们在发送数据时把数据包装成Length+ type+data即可:
##2 数据类型枚举
typedefNS_ENUM(NSUInteger, TMCommandType) {
TMCommandTypeImg = 1,
TMCommandTypeText = 2,
TMCommandTypeVideo = 3,
};
//发送时包装数据
- (void)sendData:(NSData*)data type:(TMCommandType)type{
NSMutableData *mData = [NSMutableData data];
// 计算数据总长度 data
unsignedintdataLength =4+4+(int)data.length;
NSData*lengthData = [NSDatadataWithBytes:&dataLengthlength:4];
[mData appendData:lengthData];
// 数据类型 data
// 2.拼接指令类型(4~7:指令)
NSData *typeData = [NSData dataWithBytes:&type length:4];
[mData appendData:typeData];
// 最后拼接数据
[mData appendData:data];
NSLog(@"发送数据的总字节大小:%ld",mData.length);
// 发数据
[self.mSocket writeData:mData withTimeout:-1 tag:999];
}
//接收服务器返回来的数据 拆包
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{
NSLog(@"总共接收到tag = %ld : %ld 长度的数据",tag,data.length);
if (data.length == 0) {
return;
}
// 1.第一次接收数据
if(self.mData.length == 0){
//读取前四个字节 数据包大小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;
}
//拼接数据
[self.mData appendData:data];
if (self.mData.length == self.mTotalSize) {
//数据内容
NSData *data_data = [self.mData subdataWithRange:NSMakeRange(8, self.mData.length - 8)];
NSLog(@"数据已经接收完成");
if (self.mCurrentCommandId == TMCommandTypeImg) {
NSLog(@"接收到图片");
[self saveImage: data_data];
}else if (self.mCurrentCommandId == TMCommandTypeVideo){
NSLog(@"接收到视频");
}else if (self.mCurrentCommandId == TMCommandTypeText){
NSLog(@"接收到文本");
}
// 清除数据
self.mData = [NSMutableData data];
};
//-1表示永不超时
[self.mSocket readDataWithTimeout:-1 tag:10086];
}