Protobuf简介
Protocol Buffer是google 的一种数据交换的格式,已经在Github开源,目前最新版本为3.4.0
说明
步骤
1 转换:将我们编写好的XXX.proto文件转成Objective C文件,也就是XXX.h和XXX.m文件,转换的工具是使用protoc这种二进制文件来生成的,这文件需要自己生成,稍后会介绍如何使用它来转换Objective-C文件
2 集成:如果在iOS项目中加入protobuf库以及步骤1生成的OC文件
转换
如果没有装autoconf automake libtool需要先装这几个,这里使用brew来安装,在shell执行 brew install autoconf automake libtool即可,如果没有brew请自行先安装brew。
下载面向Objective-C的protobuf库,地址为(https://github.com/google/protobuf/releases),要下载对应Objective-C的版本比如 protobuf-objectivec-3.4.0.zip,解压。

cd到下载的目录,依次执行:
再执行 - objectivec/DevTools/full_mac_build.sh 执行完后会看到src目录下生成了protoc二进制文件
创建proto文件,这里是服务端给的
需要注意的是要指明proto的语法规则是proto2还是proto3。
在src目录(protoc所在目录)执行
其中proto_path是我们创建的proto文件所在目录,objc_out为Objective-C文件输出路径,XXX.proto是我们创建的proto文件,可以一次转换多个proto文件,加在XXX.proto后面即可。
protoc --proto_path=protocols --objc_out=gen protocols/PBData.proto
然后在gen文件夹下就会生成Person.pbobjc.h和Person.pbobjc.m文件。
集成
将生成的Ojective-C文件(上面例子的Pbdata.pbobjc.h和Pbdata.pbobjc.m)放到项目中,如果项目使用了ARC,要将.m(例子的Person.pbobjc.m)的Complier Flags设为-fno-objc-arc。(protobuf基于性能原因没有使用ARC)
加入protobuf库,有两种方式
第一种是使用CocoaPods集成
用CocoaPods集成,有一个现成的pod可以使用–Protobuf,可 以pod search Protobuf搜索查看详情,pod内容为
pod 'Protobuf', '~> 3.1.0'
需要注意的是 platform :iOS, ‘7.1’
及以上才能导入这个库,这种方式优点是操作简单
第二种是把相关文件拖入项目中。
拖入相关文件到项目中,将objectivec文件夹下的所有的.h文件和.m文件(除了GPBProtocolBuffers.m)(GPB开头的那些文件)以及整个google文件夹add到项目中,如果项目中使用了ARC需要将以上所有.m文件的的Complier Flags设为-fno-objc-arc。这种方法的优点是灵活性强,没有7.1的束缚。缺点是操作麻烦点,如果用了ARC的话还要手动添加-fno-objc-arc(使用CocoaPods集成会自动添加),记得添加User Header Search Paths为$(PROJECT_DIR)/项目名/后接文件地址 不然头文件会报错
在这里要提两个概念序列化与反序列化
序列化
我们在使用socket与服务器通信时,是以二进制数据流的形式进行传输的,因此我们要将Pbdata.pbobjc创建的对象转为二进制数据流,这个过程就称之为序列化
如图,其中delimitedData方法就是对per对象进行序列化,通过观察源码可以发现,序列化过程中,内部会自动设置数据长度,以便告知服务器数据包的长度
将序列化好化的data调用下面方法便可向服务器发送消息了
顺便提一下socket我用的第三方框架CocoaAsyncSocket,因为这个比较简单,这里就不再赘述了
反序列化
了解完序列化,反序列也是比较好理解了,同样的,服务器给我们传输的也是二进制数据流,所以我们需要不断拼接数据包,直到拼接成服务器规定的大小,将其转为OC对象,那么这个过程就是反序列化
粘包
在了解完反序列化后,相信大家都有一个问题,那就是服务器返回数据包的长度到底多少?只要在知道数据包大小的情况下,才能反序列出一个完整的对象,假设服务器给我们返回的数据包大小为500字节,如果我们自己拼接数据大于了500字节,就会造成粘包
半包
同理,还是假设服务器给我们返回的数据包大小为500字节,如果我们自己拼接的数据小于500字节,就会造成半包
更蛋疼的问题
由于服务器使用的是第三方框架netty,使用这个框架,服务器只要简单调用几句API便可传输数据,但是正所谓有利有弊,那就是服务器不关心数据包长度!!!!
解决方案:
如果在没有嵌套数据的情况,我们自己也是可以获取数据包长度大小的,简单的思路如下
先获取头部数据,即四个字节的数据,拼接为int类型,获取这个int的值,就可以获取数据包大小
#pragma mark - 处理拆包和粘包
/** 关键代码:获取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];
}
当然,上面这个方法是在接受到服务器数据时调用
// 接收信息
- (void)socket:(GCDAsyncSocket *)sock didReadData:(nonnull NSData *)data withTag:(long)tag{
[self.socket readDataWithTimeout: -1 tag:0];
#pragma mark - 处理粘包,拆包部分
[self.receiveData appendData:data];
// 每条消息的头部占用字节长度
int32_t headL = 0;
int32_t contentL = [self getContentLength:self.receiveData withHeadLength:&headL];
NSLog(@"实际接收总长度:%zd, 当前接收包长度: %zd, 读取头部占用长度: %zd, 读取内容长度: %zd \n",self.receiveData.length,data.length,headL,contentL);
// 反序列化
[self deserialize:nil];
}
拼接完数据包后进行反序列化
- (Data *)deserialize:(NSData *)data {
//二进制数据反序列化为对象
GPBCodedInputStream *inputStream;
if (data) {
inputStream = [GPBCodedInputStream streamWithData:data];
}else {
inputStream = [GPBCodedInputStream streamWithData:self.receiveData];
}
NSError *error;
Data *per = [Data parseDelimitedFromCodedInputStream:inputStream extensionRegistry:nil error:&error];
if (error){
NSLog(@"解析数据失败!");
return nil;
}
// 解析任意数据
LoginInfo *login = per.data_p;
NSLog(@"login %@---%@",login.loginName,login.passWord);
//展示数据
NSMutableString *str = [[NSMutableString alloc] init];
[str appendString:@"二进制数据反序列化为对象----"];
[str appendFormat:@"cmd: %d, sub: %d", per.cmd, per.sub];
NSLog(@"%@",str);
}
如题
如果你们服务器不要求嵌套数据的话,那么上面的也就够用了,问题也就完美解决了,但是生活总是不尽人意,对,服务器需要嵌套数据的情况,那么你会发现粘包,半包问题又来了!!!!!
如图,data_p是LoginInfo类型的,然后又是Data的属性,这个就是嵌套数据,如果你和服务器通信不需要传这个值的话,OK,一切完美!!!但是要传的话,你就会发现上面那套不管用
产生问题的原因
经过多次排查后,发现问题的根源还是处于数据长度上,由于服务器使用的netty框架,所以没有数据长度这个字段,而数据包的大小只能客户端自己去获取,这就无异于大海捞针了,由于传了data_p后,服务端的头部字节大小也相应发生变化,这就导致了无法获取正确的数据包大小,从而解析不出来data_p这个数据
解决方案:
这就必须服务端增加一个数据长度的字段,告知客户端数据长度