iOS 整理基于socket集成Protobuf相关环境,以及将Protobuf文件转成OC文件,以及使用Protobuf
来源:互联网 发布:gta5低配优化 编辑:程序博客网 时间:2024/05/21 09:24
Protobuf简介
Protocol Buffer是google 的一种数据交换的格式,已经在Github开源,目前最新版本为3.4.0
说明
- protobuf3.0.0以上才官方支持Objective-C,低于3.0.0的请忽略或使用第三方转换工具
- 开发环境:32bit & 64bit iOS, 64bit OS X,Xcode7.0+
- 基于性能原因没有使用ARC,但可以被ARC代码调用
1 配置环境
步骤
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到下载的目录,依次执行:
- $ ./autogen.sh
- $ ./configure
- $ make
- $ make check
- $ sudo make install
再执行 - objectivec/DevTools/full_mac_build.sh 执行完后会看到src目录下生成了protoc二进制文件
2 集成Protobuf
创建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)/项目名/后接文件地址 不然头文件会报错
3 使用Protobuf
在这里要提两个概念序列化与反序列化
序列化
我们在使用socket与服务器通信时,是以二进制数据流的形式进行传输的,因此我们要将Pbdata.pbobjc创建的对象转为二进制数据流,这个过程就称之为序列化
如图,其中delimitedData方法就是对per对象进行序列化,通过观察源码可以发现,序列化过程中,内部会自动设置数据长度,以便告知服务器数据包的长度
将序列化好化的data调用下面方法便可向服务器发送消息了
顺便提一下socket我用的第三方框架CocoaAsyncSocket,因为这个比较简单,这里就不再赘述了
反序列化
了解完序列化,反序列也是比较好理解了,同样的,服务器给我们传输的也是二进制数据流,所以我们需要不断拼接数据包,直到拼接成服务器规定的大小,将其转为OC对象,那么这个过程就是反序列化
4 粘包半包问题
粘包
在了解完反序列化后,相信大家都有一个问题,那就是服务器返回数据包的长度到底多少?只要在知道数据包大小的情况下,才能反序列出一个完整的对象,假设服务器给我们返回的数据包大小为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); }
4 额外的坑
如题
如果你们服务器不要求嵌套数据的话,那么上面的也就够用了,问题也就完美解决了,但是生活总是不尽人意,对,服务器需要嵌套数据的情况,那么你会发现粘包,半包问题又来了!!!!!
嵌套数据
如图,data_p是LoginInfo类型的,然后又是Data的属性,这个就是嵌套数据,如果你和服务器通信不需要传这个值的话,OK,一切完美!!!但是要传的话,你就会发现上面那套不管用
产生问题的原因
经过多次排查后,发现问题的根源还是处于数据长度上,由于服务器使用的netty框架,所以没有数据长度这个字段,而数据包的大小只能客户端自己去获取,这就无异于大海捞针了,由于传了data_p后,服务端的头部字节大小也相应发生变化,这就导致了无法获取正确的数据包大小,从而解析不出来data_p这个数据
解决方案:
这就必须服务端增加一个数据长度的字段,告知客户端数据长度
- iOS 整理基于socket集成Protobuf相关环境,以及将Protobuf文件转成OC文件,以及使用Protobuf
- iOS 整理基于socket集成Protobuf相关环境,以及将Protobuf文件转成OC文件,以及使用Protobuf
- iOS 基于Socket使用Protobuf进行数据传输
- 在lua环境中使用protobuf ,编译protobuf文件
- iOS 集成Protobuf,转换proto文件
- Mac上安装Protobuf以及生成lua文件
- IOS ProtoBuf 环境搭建
- protobuf 使用相关
- protobuf 使用相关
- protobuf eclipse使用相关
- protobuf 使用相关
- protobuf
- ProtoBuf
- protobuf
- protobuf
- protobuf
- Protobuf
- protobuf
- [caffe的python接口学习二]:生成配置文件
- c# dataGridView绑定string数组数据源
- 描述cookies,sessionStorage和localStorage的区别
- LeetCode-Regular Expression Matching
- openSSL 公私钥加解密和签名验证
- iOS 整理基于socket集成Protobuf相关环境,以及将Protobuf文件转成OC文件,以及使用Protobuf
- Oracle的存储结构
- NYOJ 678 最小K个数之和
- JFinal+maven+freemarker 入门教程
- Deep Learning论文笔记之(四)CNN卷积神经网络推导和实现
- spark rdd api详解(待补充版)
- js-循环的方法
- 大数(高精度)运算
- Android中将Activity转换成View使用.