CocoaAsyncSocket + Protobuf 处理粘包和拆包问题
在上一篇文章《iOS之ProtocolBuffer搭建和示例demo》分享环境的搭建, 我们和服务器进行IM通讯用了github有名的框架CocoaAsynSocket, 然后和服务器之间的数据媒介是ProtoBuf。然后后面在开发的过程中也碰到了拆包和粘包问题,这方面网上资料很少,曲折了一下才解决,这里分享一下问题的解决过程!
首先描述下碰到的问题:
1、服务器发送内容很长的数据过来的时候,GCDAsyncSocket监听收到的一个包解析不了,一直要接收好几个包拼接才是这条数据的完整包,即所谓的拆包/断包;
2、服务器快速发送多条数据过来,传到客户端这边的时候几条数据合成了一个包,即所谓的粘包。所以想解析这些粘在一起的数据,必须知道每条数据的长度,才能正确切割解析。
先上关键代码,解决读取每条数据的头部字节,根据头部字节读取这条数据的内容长度。这样才能完美的解决粘包问题。由于根据数据的长度不一样,导致头部字节占用的长度也会不一样,比如说我这里反复测试的结果头部占用字节一般为1和2,短内容数据头部占用字节长度为1,长内容数据头部占用字节长度为2。这里的代码参考了谷歌提供的Protobuf的objectivec版的源码。
/** 关键代码:获取data数据的内容长度和头部长度: index --> 头部占用长度 (头部占用长度1-4个字节) */ - (int32_t)getContentLength:(NSData *)data withHeadLength:(int32_t *)index{ int8_t tmp = [self readRawByte:data headIndex:index]; if (tmp >= 0) return tmp; int32_t result = tmp & 0x7f; if ((tmp = [self readRawByte:data headIndex:index]) >= 0) { result |= tmp << 7; } else { result |= (tmp & 0x7f) << 7; if ((tmp = [self readRawByte:data headIndex:index]) >= 0) { result |= tmp << 14; } else { result |= (tmp & 0x7f) << 14; if ((tmp = [self readRawByte:data headIndex:index]) >= 0) { result |= tmp << 21; } else { result |= (tmp & 0x7f) << 21; result |= (tmp = [self readRawByte:data headIndex:index]) << 28; if (tmp < 0) { for (int i = 0; i < 5; i++) { if ([self readRawByte:data headIndex:index] >= 0) { return result; } } result = -1; } } } } return result; } /** 读取字节 */ - (int8_t)readRawByte:(NSData *)data headIndex:(int32_t *)index{ if (*index >= data.length) return -1; *index = *index + 1; return ((int8_t *)data.bytes)[*index - 1]; }
解决了读取每条数据的头部占用字节,和内容的长度,粘包问题就好解决了。
再上比较完整的代码:从客户端监听服务器发送过来的数据到处理拆包和粘包问题,然后解析成自定义的protobuf模型类。
--------------------- print log test start ---------------------
在GCDAsyncSocket监听服务器代理方法里面打印日志,查看下正常包、粘包、拆包的日志信息:
1、正常包的打印日志(服务器发送一条消息达到客户端只有一个包: 1 + 49 = 50 ):
实际接收总包长度:50, 当前接收包长度:50, 读取头部占用长度: 1, 读取内容长度:49, 当前包字节: <3108c901 122c0a0a 74363330 32303137 30361203 43636318 01220c64 656e6773 6f6e676e 616e782a 00300038 9af6ccd5 b72b>
2、粘包情况下的日志 (发送的4条消息合到了一个包里:(1 + 49) * 4 = 200 ):
实际接收总包长度:200, 当前接收包长度:200, 读取头部占用长度: 1, 读取内容长度:49, 当前包字节: <3108c901 122c0a0a 74363330 32303137 30361203 46666618 01220c64 656e6773 6f6e676e 616e782a 00300038 f2cfccd5 b72b3108 c901122c 0a0a7436 33303230 31373036 12034767 67180122 0c64656e 67736f6e 676e616e 782a0030 003898d0 ccd5b72b 3108c901 122c0a0a 74363330 32303137 30361203 5a7a7a18 01220c64 656e6773 6f6e676e 616e782a 00300038 add0ccd5 b72b3108 c901122c 0a0a7436 33303230 31373036 12035878 78180122 0c64656e 67736f6e 676e616e 782a0030 0038c1d0 ccd5b72b>
3、拆包情况下的日志 (发送一条内容较长的消息,最后到达客户端时被拆成了三个接收包: 4200 + 1400 + 292 = 2+5890 ):