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这个数据

解决方案:

 这就必须服务端增加一个数据长度的字段,告知客户端数据长度
原创粉丝点击