Android进阶:Protocol Buffer协议的使用

一、背景

说起数据交互协议,相信大家最熟悉的就是xml和json了,尤其是json,广泛应用于web项目和移动端项目中。其实,还有一种协议,Protocol Buffer,简称Protobuf,得益于它的一些特性,越来越多的公司在开发中使用Protobuf代替json。

二、简介

1、概念

Protobuf,在官网上的定义描述是:

Protocol buffers are Google’s language-neutral, platform-neutral, extensible mechanism for serializing structured data – think XML, but smaller, faster, and simpler. You define how you want your data to be structured once, then you can use special generated source code to easily write and read your structured data to and from a variety of data streams and using a variety of languages.

翻译一下,大概意思:

Protobuf是Google的一种无关语言、无关平台、可扩展的序列化结构化数据机制。类似XML,但更轻量、更快、更简单。只需定义一次数据的结构化方式,就可利用Protobuf框架生成源代码,轻松地在各种数据流和各种语言之间写入和读取结构化数据。

总结一下,它是一种轻量且高效的数据结构化序列化协议。

数据结构化序列化协议,是通过将结构化的数据进行序列化,再在使用端反序列化,完成数据存储或RPC数据交换的一种协议。

根据官网上的指引,目前Protobuf有proto2和proto3两个版本。两个版本差不多,但proto3支持更多语言,去掉了一些复杂的语法和特性,更强调约定而弱化语法。如果是首次使用 Protobuf ,建议使用 proto3 。如果求稳定,建议使用proto2 ,它的兼容和扩展能力更好。

2、特性

优点

  • 编译自动生成相应类,提供序列化和反序列化api
  • 性能优,传输体积更小,传输更快(比xml和json快20-100倍)
  • 加密性好,Http传输内容抓包只能看到字节
  • 支持向后兼容和向前兼容,不破坏旧的数据格式,易于扩展和升级
  • 支持更多语言如java,python,c++,和更多数据类型,如枚举和方法

缺点

  • 通用性较差。xml和json 已经成为多种行业标准的编写工具,Protobuf 只是 Google 公司内部使用的工具,在通用性上差一些
  • 可读性差,缺乏自描述。Protobuf以二进制的方式存储,除非你有 .proto 定义,否则你没法直接读出Protobuf 的任何内容

3、适用场景

传输数据量大、网络环境不稳定

三、原理

为什么Protobuf性能这么好,主要来源于它的编码机制,具体细节推荐看这篇博客:Protocol Buffer 序列化原理大揭秘 - 为什么Protocol Buffer性能这么好?,博主写得非常好。

总结一下,体积小的原因是

  • 采用了独特的编码方式,如Varint、Zigzag编码方式等
  • 采用T - L - V 的数据存储方式。减少了分隔符的使用,数据存储得紧凑

速度快的原因是

  • 编码 / 解码 方式简单(只需要简单的数学运算 = 位移等等)
  • 采用 Protocol Buffer 自身的框架代码 和 编译器 共同完成

四、Protobuf语法

贴一个.proto文件,举例说明(以下部分例子及说明来自博客:这是一份很有诚意的 Protocol Buffer 语法详解

//注1:版本
syntax = "proto2";
//注2:包名
package tutorial;
//注3:option选项
option java_multiple_files = true;
option java_package = "com.example.tutorial.protos";
option java_outer_classname = "AddressBookProtos";
//注4:消息模型
message Person {
  optional string name = 1;
  optional int32 id = 2;
  optional string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    optional string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }

  repeated PhoneNumber phones = 4;
}

message AddressBook {
  repeated Person people = 1;
}

注1:版本

指定proto版本为proto2(默认版本),不同版本之间会有差异,建议使用同一版本。

注2:包名

声明包名,防止不同.proto项目间命名冲突。

注3:option选项

option选项会影响 特定环境 的处理方式。

常用选项说明:

option java_package = "com.carson.proto";
// 定义:Java包名
// 作用:指定生成的类应该放在什么Java包名下
// 注:如不显式指定,默认包名为:按照应用名称倒序方式进行排序

option java_outer_classname = "Demo";
// 定义:类名
// 作用:生成对应.java 文件的类名(不能跟下面message的类名相同)
// 注:如不显式指定,则默认为把.proto文件名转换为首字母大写来生成
// 如.proto文件名="my_proto.proto",默认情况下,将使用 "MyProto" 做为类名

option optimize_for = ***;
// 作用:影响 C++  & java 代码的生成
// ***参数如下:
// 1. SPEED (默认)::protocol buffer编译器将通过在消息类型上执行序列化、语法分析及其他通用的操作。(最优方式)
// 2. CODE_SIZE::编译器将会产生最少量的类,通过共享或基于反射的代码来实现序列化、语法分析及各种其它操作。
  // 特点:采用该方式产生的代码将比SPEED要少很多, 但是效率较低;
  // 使用场景:常用在 包含大量.proto文件 但 不追求效率 的应用中。
//3.  LITE_RUNTIME::编译器依赖于运行时 核心类库 来生成代码(即采用libprotobuf-lite 替代libprotobuf)。
  // 特点:这种核心类库要比全类库小得多(忽略了 一些描述符及反射 );编译器采用该模式产生的方法实现与SPEED模式不相上下,产生的类通过实现 MessageLite接口,但它仅仅是Messager接口的一个子集。
  // 应用场景:移动手机平台应用

option cc_generic_services = false;
option java_generic_services = false;
option py_generic_services = false;
// 作用:定义在C++、java、python中,protocol buffer编译器是否应该 基于服务定义 产生 抽象服务代码(2.3.0版本前该值默认 = true)
// 自2.3.0版本以来,官方认为通过提供 代码生成器插件 来对 RPC实现 更可取,而不是依赖于“抽象”服务

optional repeated int32 samples = 4 [packed=true];
// 如果该选项在一个整型基本类型上被设置为真,则采用更紧凑的编码方式(不会对数值造成损失)
// 在2.3.0版本前,解析器将会忽略 非期望的包装值。因此,它不可能在 不破坏现有框架的兼容性上 而 改变压缩格式。
// 在2.3.0之后,这种改变将是安全的,解析器能够接受上述两种格式。

optional int32 old_field = 6 [deprecated=true];
// 作用:判断该字段是否已经被弃用
// 作用同 在java中的注解@Deprecated

注4:消息模型

.proto中真正用于描述数据结构的部分,一个消息模型=消息对象+字段

消息对象用message修饰

需要注意的是,在一个.proto文件中可定义多个消息对象,在一个消息对象照片那个也可以定义另外一个消息对象(即嵌套)。

消息对象的字段组成主要是:字段 = 字段修饰符 + 字段类型 +字段名 +标识号,例如 optional string name = 1;

字段修饰符(required、optional、repeated三种):

在这里插入图片描述

字段类型

有基本数据类型、枚举类型、消息对象类型三种,基本数据类型及对应关系如下:

在这里插入图片描述

枚举类型说明:

// 枚举类型需要先定义才能进行使用

// 枚举类型 定义
 enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
// 电话类型字段 只能从 这个集合里 取值
  }

// 特别注意:
// 1. 枚举类型的定义可在一个消息对象的内部或外部
// 2. 都可以在 同一.proto文件 中的任何消息对象里使用
// 3. 当枚举类型是在一消息内部定义,希望在 另一个消息中 使用时,需要采用MessageType.EnumType的语法格式

  message PhoneNumber {
    required string number = 1;
    optional PhoneType type = 2 [default = HOME];
    // 使用枚举类型的字段(设置了默认值)
  }

// 特别注意:
// 1.  枚举常量必须在32位整型值的范围内
// 2. 不推荐在enum中使用负数:因为enum值是使用可变编码方式的,对负数不够高

关于字段的高级用法

1、更新字段

更新字段时需要注意兼容,符合以下规则
在这里插入图片描述

2、扩展字段

原则:使得其他人可以在自己的 .proto 文件中为该消息对象声明新的字段而不必去编辑原始文件,扩展 可以是消息类型也可以是字段类型。

举例,A.proto:

message Request {
…
  extensions 100 to 199;
  // 将一个范围内的标识号 声明为 可被第三方扩展所用
  // 在消息Request中,范围 [100,199] 的标识号被保留为扩展用

  // 如果标识号需要很大的数量时,可以将可扩展标符号的范围扩大至max
  // 其中max是2的29次方 - 1(536,870,911)。
  message Request {
    extensions 1000 to max;

  // 注:请避开[19000-19999] 的标识号,因为已被Protocol Buffers实现中预留
}

B.proto

extend Request {
  optional int32 bar = 126;
  // 添加字段的 标识号必须要在指定的范围内
  // 消息Request 现在有一个名为 bar 的 optional int32 字段
  // 当Request消息被编码时,数据的传输格式与在Request里定义新字段的效果是完全一样的
  //  注:在同一个消息类型中一定要确保不会扩展新增相同的标识号,否则会导致数据不一致;可以通过为新项目定义一个可扩展标识号规则来防止该情况的发生
}

这样,别人就可以在自己的 .proto文件中添加新字段到Request里了。

但要注意的是访问扩展字段的方法与访问普通的字段。访问扩展字段需要使用专门的扩展访问函数。

Request request;
request.SetExtension(bar, 15);
// 类似的模板函数 HasExtension(),ClearExtension(),GetExtension(),MutableExtension(),以及 AddExtension()
// 与对应的普通字段的访问函数相符

五、编译及使用

两种方式可以选择

1、命令行方式

下载安装protobuf,配置好环境变量(此处不做赘述),执行cmd指令:

protoc -I=#SRC_DIR -xxx_out=$DST_DIR $SRC_DIR/xxx.proto
// 参数说明
// 1. $SRC_DIR:指定需要编译的.proto文件目录 (如没有提供则使用当前目录)
// 2. --xxx_out:xxx根据需要生成代码的类型进行设置
// 对于 Java ,xxx =  java ,即 -- java_out
// 对于 C++ ,xxx =  cpp ,即 --cpp_out
// 对于 Python,xxx =  python,即 --python_out

// 3. $DST_DIR :编译后代码生成的目录 (通常设置与$SRC_DIR相同)
// 4. 最后的路径参数:需要编译的.proto 文件的具体路径

// 编译通过后,Protoco Buffer会根据不同平台生成对应的代码文件

即可在指定目录下生成编译好的对应的文件。

2、Android Studio插件方式

配置工程的build.gradle:

buildscript {
    ..
    dependencies {
        ..
        classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.10'
    }
}

在app的build.gradle中(每个版本的配置都不一样,这里是3.8.0)

//Protobuf的Gradle插件,帮助我们在编译时通过语义分析自动生成源码,提供数据结构的初始化、序列化以及反序列等接口。
apply plugin: 'com.google.protobuf'

android {
    sourceSets {
        main {
            // 定义proto文件目录
            proto {
                srcDir 'src/main/proto'
                include '**/*.proto'
            }
        }
    }
}

dependencies {
  // You need to depend on the lite runtime library, not protobuf-java
  // lite是轻量版,在原有的基础上,用public替换set、get方法,减少Protobuf生成代码的方法数目。
  compile 'com.google.protobuf:protobuf-javalite:3.8.0'
}

protobuf {
  protoc {
    artifact = 'com.google.protobuf:protoc:3.8.0'
  }
  generateProtoTasks {
    all().each { task ->
      task.builtins {
        java {
          option "lite"
        }
      }
    }
  }
}

配置好之后直接build即可,自动生成文件在 build/generated/source/proto中。

在项目中使用序列化和反序列化:

//创建对象
AddressProto.Person.Builder builder = AddressProto.Person.newBuilder();
AddressProto.Person person = builder.setEmail("xxx").build();
//序列化
byte[] bytes = person.toByteArray();
//反序列化
try{
    AddressProto.Person person1 = AddressProto.Person.parseFrom(bytes);
}catch(InvalidPriticilBufferException e){
    e.prientStackTrace()
}

//序列化
ByteArrayOutputStream output = new ByteArrayOutputStream();
try(){
    person.witeTo(output);
    byte[] byte1 = output.toByteArray();
}catch(IOException e){
    e.prientStackTrace()
}
//反序列化
try(){
   AddressProto.Person person1 = AddressProto.Person.parseFrom(new ByteArrayInputStream(byte));
}catch(IOException e){
    e.prientStackTrace()
}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

KWMax

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值