iOS 处理socket粘包问题
1.什么是粘包?
粘包通常出现在TCP的协议里面,对于UDP来说是不会出现粘包状况的,之所以出现这种状况的原因,涉及到一种名为Nagle的算法。
Nagle算法通过减少必须发送的封包的数量,提高网络应用程序系统的效率,解决负载问题。通俗的讲,就是在发包的时候会建立一个缓存区,发送的数据都会先进入这个缓存区,当上一条数据的接收被确认或者到达最大等待时间之后,才会将缓存区的数据一块发送过去,如此反复。将小包进行整合,避免小包多次发送造成的传输速度慢等问题。
2.什么时候才需要处理粘包?
理论上来讲,只要是基于TCP的socket链接,都需要处理粘包的情况。
可能有些人在测试的时候并没有出现粘包的情况,认为并不需要对粘包进行处理,这种想法是错误的。
首先,在测试的时候之所以没有出现粘包情况,极有可能是因为网络路由问题,导致TCP的MTU会有所变化,Internet上的标准MTU值为576,以太网MTU为1500。所以测试粘包一般以外网环境进行测试。
3.粘包解决方案
由于Nagle算法已经成为了默认的执行方式,所以对于粘包,在Server端和Client端都需要解决粘包的问题,由于问题一致,所以解决方案也基本是通用的,只是相关语法需要变换一下。
由于iOS的底层对于socket的封装并不是那么完善,使用的话还得采用C语言,因此使用了一个名为CocoaAsyncSocket的第三方,将C的socket封装成了适合OC面向对象的形式,github地址如下:https://github.com/robbiehanson/CocoaAsyncSocket
导入成功后,正式开始处理粘包问题。
首先,跟服务器确定数据头的问题,对于粘包,一般有两种解决方案,第一种就是服务器返回的字段中有可识别的头和尾,我们可以根据可识别的头和尾来拆包。第二种是服务器返回的数据只包含头,头里面有数据的长度,我们可以根据这个头包含的数据长度来进行拆包。本文采用的便是第二种方案。
服务器返回的数据为data形式的数据,打印出来的数据如下:
<fefd00f9 7b22736e 223a342c 22766572 73696f6e 223a2231 2e30222c 226e6574 466c6167 223a312c 22636d64 54797065 223a312c 22706475 223a7b22 70647554 79706522 3a343039 382c2264 65764461 7461223a 5b7b2264 65764964 223a2231 32333435 36373822 2c226465 764e616d 65223a22 e59b9ee8 b7afe68e a7e588b6 222c2270 4964223a 22343039 3833222c 22706172 616d223a 5b7b2274 79706522 3a383232 372c2276 616c7565 223a317d 2c7b2274 79706522 3a383232 382c2276 616c7565 223a307d 2c7b2274 79706522 3a383232 392c2276 616c7565 223a317d 2c7b2274 79706522 3a383233 302c2276 616c7565 223a317d 5d7d5d7d 7d>
其中fefd00f9为服务器返回的头,后面为具体的数据。其中fefd占两个字节,00f9占两个字节,fefd是固定的值不做任何处理,00f9就是数据的长度
接下来,在链接成功之后,在回调方法里面创建一个缓存区
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port { self.readBuf = [[NSMutableData alloc] init];
NSLog(@"链接成功后的其他操作");
}
然后在收到服务器发送的数据的时候,对这个数据进行处理
//服务器发送的数据 - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag { //将数据存入缓存区 [self.readBuf appendData:data]; //数据中前面有4个字节的头信息,其中前两位是固定的头长度(用处不大),后两位才是数据的长度。 //如果大于4个字节证明有消息,因为服务器只要发送数据,必定包含头 while (self.readBuf.length > 4) { //将消息转化成byte,计算总长度 = 数据的内容长度 + 前面4个字节的头长度 Byte *bytes = (Byte *)[self.readBuf bytes]; NSUInteger allLength = (bytes[2]<<8) + bytes[3] +4; //缓存区的长度大于总长度,证明有完整的数据包在缓存区,然后进行处理 if (self.readBuf.length >= allLength) { NSMutableData *msgData = [[self.readBuf subdataWithRange:NSMakeRange(0, allLength)] mutableCopy]; //提取出前面4个字节的头内容,之所以提取出来,是因为在处理数据问题的时候,比如data转json的时候,头内容里面包含非法字符,会导致转化出来的json内容为空,所以要先去掉再处理数据问题 [msgData replaceBytesInRange:NSMakeRange(0, 4) withBytes:NULL length:0]; NSLog(@"开始处理数据问题"); //处理完数据后将处理过的数据移出缓存区 _readBuf = [NSMutableData dataWithData:[_readBuf subdataWithRange:NSMakeRange(allLength, _readBuf.length - allLength)]]; }else{ //缓存区内数据包不是完整的,再次从服务器获取数据,中断while循环 [self.clientSocket readDataWithTimeout:-1 tag:0]; break; } } //读取到服务端数据值后,能再次读取 [self.clientSocket readDataWithTimeout:-1 tag:0]; }
这样处理之后,可以将粘包问题解决