iOS 整理基于socket集成Protobuf相关环境,以及将Protobuf文件转成OC文件,以及使用Protobuf

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二进制文件

iOS 整理基于socket集成Protobuf相关环境,以及将Protobuf文件转成OC文件,以及使用Protobuf_第1张图片


2 集成Protobuf

创建proto文件,这里是服务端给的

iOS 整理基于socket集成Protobuf相关环境,以及将Protobuf文件转成OC文件,以及使用Protobuf_第2张图片

  • 需要注意的是要指明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文件。
iOS 整理基于socket集成Protobuf相关环境,以及将Protobuf文件转成OC文件,以及使用Protobuf_第3张图片


集成

将生成的Ojective-C文件(上面例子的Pbdata.pbobjc.h和Pbdata.pbobjc.m)放到项目中,如果项目使用了ARC,要将.m(例子的Person.pbobjc.m)的Complier Flags设为-fno-objc-arc。(protobuf基于性能原因没有使用ARC)
iOS 整理基于socket集成Protobuf相关环境,以及将Protobuf文件转成OC文件,以及使用Protobuf_第4张图片

加入protobuf库,有两种方式

第一种是使用CocoaPods集成
用CocoaPods集成,有一个现成的pod可以使用–Protobuf,可       以pod search Protobuf搜索查看详情,pod内容为 
     pod 'Protobuf', '~> 3.1.0'

iOS 整理基于socket集成Protobuf相关环境,以及将Protobuf文件转成OC文件,以及使用Protobuf_第5张图片

 需要注意的是 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创建的对象转为二进制数据流,这个过程就称之为序列化

iOS 整理基于socket集成Protobuf相关环境,以及将Protobuf文件转成OC文件,以及使用Protobuf_第6张图片

  如图,其中delimitedData方法就是对per对象进行序列化,通过观察源码可以发现,序列化过程中,内部会自动设置数据长度,以便告知服务器数据包的长度

iOS 整理基于socket集成Protobuf相关环境,以及将Protobuf文件转成OC文件,以及使用Protobuf_第7张图片

将序列化好化的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 额外的坑

如题

如果你们服务器不要求嵌套数据的话,那么上面的也就够用了,问题也就完美解决了,但是生活总是不尽人意,对,服务器需要嵌套数据的情况,那么你会发现粘包,半包问题又来了!!!!!

嵌套数据
iOS 整理基于socket集成Protobuf相关环境,以及将Protobuf文件转成OC文件,以及使用Protobuf_第8张图片

如图,data_p是LoginInfo类型的,然后又是Data的属性,这个就是嵌套数据,如果你和服务器通信不需要传这个值的话,OK,一切完美!!!但是要传的话,你就会发现上面那套不管用

产生问题的原因

 经过多次排查后,发现问题的根源还是处于数据长度上,由于服务器使用的netty框架,所以没有数据长度这个字段,而数据包的大小只能客户端自己去获取,这就无异于大海捞针了,由于传了data_p后,服务端的头部字节大小也相应发生变化,这就导致了无法获取正确的数据包大小,从而解析不出来data_p这个数据

解决方案:

 这就必须服务端增加一个数据长度的字段,告知客户端数据长度

你可能感兴趣的:(ios开发)