Google出品的Protocol Buffer,别只会用Json和XML了

来源:互联网 发布:网络语言兔子 编辑:程序博客网 时间:2024/05/16 11:30

原文地址:http://blog.csdn.net/carson_ho/article/details/70037693

前言

  • 习惯用 Json、XML 数据存储格式的你们,相信大多都没听过Protocol Buffer
  • Protocol Buffer 其实 是 Google出品的一种轻量 & 高效的结构化数据存储格式,性能比 Json、XML 真的强!太!多! 
    由于 Google出品,我相信Protocol Buffer已经具备足够的吸引力
  • 今天,我将献上一份 Protocol Buffer的介绍 & 使用攻略,希望你们会喜欢。 

目录

目录


1. 定义

一种 结构化数据 的数据存储格式(类似于 `XML、Json` )
  1. Google 出品 (开源)
  2. Protocol Buffer 目前有两个版本:proto2 和 proto3
  3. 因为proto3 还是beta 版,所以本次讲解是 proto2

2. 作用

通过将 结构化的数据 进行 串行化(**序列化**),从而实现 **数据存储 / RPC 数据交换**的功能
  1. 序列化: 将 数据结构或对象 转换成 二进制串 的过程
  2. 反序列化:将在序列化过程中所生成的二进制串 转换成 数据结构或者对象 的过程

3. 特点

  • 对比于 常见的 XML、Json 数据存储格式,Protocol Buffer有如下特点:

Protocol Buffer 特点


4. 应用场景

传输数据量大 & 网络环境不稳定 的数据存储、RPC 数据交换 的需求场景

如 即时IM (QQ、微信)的需求场景


总结

在 传输数据量较大的需求场景下,Protocol BufferXML、Json 更小、更快、使用 & 维护更简单!


5. 使用流程

使用 Protocol Buffer 的流程如下:

Protocol Buffer使用流程

5.1 环境配置

  • 要使用Protocol Buffer ,需要先在电脑上安装Protocol Buffer

  • 整个 安装过程 只需要按照以下步骤进行即可:

    整个安装过程请 自备梯子 以保证 网络畅通

步骤1:下载 Protocol Buffer 安装包


  • 下载方式1:官网下载(需要翻墙)
  • 下载方式2:贴心的我 已经给你们准备好了,请移步百度网盘,密码:paju 

此处选择 较稳定的版本 protobuf-2.6.1.tar.gz 进行演示 
下载成功后,对文件进行解压,如下图: 
安装包 & 解压后文件

步骤2:安装 HOMEBREW(已安装的可以跳过)

// 打开 终端 输入以下指令/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
  • 1
  • 2
  • 1
  • 2

步骤3:安装 Protocol Buffer

打开 您的终端 依次输入 下列指令 即可:

brew install autoconf automake libtool curl// Step1:安装 Protocol Buffer 依赖// 注:Protocol Buffer 依赖于  autoconf、automake、libtool、curlcd Desktop/protobuf-2.6.1// Step2:进入 Protocol Buffer安装包 解压后的文件夹(我的解压文件放在桌面)./autogen.sh// Step3:运行 autogen.sh 脚本 ./configure// Step4:运行 configure.sh 脚本 make// Step5:编译未编译的依赖包 make check// Step6:检查依赖包是否完整make install// Step7:开始安装Protocol Buffer
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

步骤4:检查 Protocol Buffer 是否安装成功

// 在 终端 下输入protoc - - version
  • 1
  • 2
  • 1
  • 2

出现 libprotoc 2.6.1 提示即表示 安装成功,如下图

安装成功提示

特别注意:

  • protoc = Protocol Buffer的编译器
  • 作用:将 .proto文件 编译成对应平台的 头文件和源代码文件
  • 在下面会详细介绍

至此, Protocol Buffer已经安装完成。下面将讲解如何具体使用Protocol Buffer


5.2 构建 Protocol Buffer 消息对象模型

5.2.1 构建步骤

构建步骤

下面将通过一个实例(Android(Java) 平台为例)详细介绍每个步骤。

5.2.2 详细介绍

  • 实例说明:构建一个Person类的数据结构,包含成员变量name、id、email等等
// Java类public class Person{    private String name;    private Int id;    private String email;...}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 平台使用:以 Android(Java) 平台为例来进行演示

步骤1:通过 Protocol Buffer 语法 描述 需要存储的数据结构

  • 新建一个文件,命名规则为:文件名 = 类名,后缀为 .proto 

    此处叫Demo.proto


Demo.proto
  • 根据上述数据结构的需求,在Demo.proto里 通过 Protocol Buffer 语法写入对应 .proto对象模型的代码,如下:
package protocobuff_Demo;// 关注1:包名option java_package = "com.carson.proto";option java_outer_classname = "Demo";// 关注2:option选项// 关注3:消息模型// 下面详细说明// 生成 Person 消息对象(包含多个字段,下面详细说明)message Person {  required string name = 1;  required int32 id = 2;  optional string email = 3;  enum PhoneType {    MOBILE = 0;    HOME = 1;    WORK = 2;  }  message PhoneNumber {    required string number = 1;    optional PhoneType type = 2 [default = HOME];  }  repeated PhoneNumber phone = 4;}message AddressBook {  repeated Person person = 1;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 下面将结合 上述例子 对 Protocol Buffer 语法 进行详细介绍

Protocol Buffer语法

关注1:包名

package protocobuff_Demo;// 关注1:包名
  • 1
  • 2
  • 1
  • 2
  • 作用:防止不同 .proto 项目间命名 发生冲突
  • Protocol buffer包的解析过程如下: 
    1. Protocol buffer 的类型名称解析与 C++ 一致:从 最内部 开始查找,依次 向外 进行 
      每个包会被看作是其父类包的内部类
    2. Protocol buffer 编译器会解析 .proto文件中定义的所有类型名
    3. 生成器会根据 不同语言 生成 对应语言 的代码文件 
      a. 即对 不同语言 使用了 不同的规则 进行处理 
      b. Protoco Buffer提供 C++、Java、Python 三种语言的 API

关注2:Option选项

option java_package = "com.carson.proto";option java_outer_classname = "Demo";// 关注2:option选项
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3
  • 作用:影响 特定环境下 的处理方式

    但不改变整个文件声明的含义

  • 常用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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 在 ProtocolBuffers 中允许 自定义选项 并 使用
  • 该功能属于高级特性,使用频率很低,此处不过多描述。有兴趣可查看官方文档

关注3:消息模型

  • 作用:真正用于描述 数据结构
// 消息对象用message修饰message Person {  required string name = 1;  required int32 id = 2;  optional string email = 3;  enum PhoneType {    MOBILE = 0;    HOME = 1;    WORK = 2;  }  message PhoneNumber {    optional PhoneType type = 2 [default = HOME];  }  repeated PhoneNumber phone = 4;}message AddressBook {  repeated Person person = 1;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 组成:在 ProtocolBuffers 中: 
    1. 一个 .proto 消息模型 = 一个 .proto文件 = 消息对象 + 字段
    2. 一个消息对象(Message) = 一个 结构化数据
    3. 消息对象(Message)里的 字段 = 结构化数据 里的成员变量

结构化数据 & 消息对象 对比

下面会详细介绍 .proto 消息模型里的 消息对象 & 字段

消息模型

1. 消息对象

在 ProtocolBuffers 中:

  • 一个消息对象(Message) = 一个 结构化数据
  • 消息对象用 修饰符 message 修饰
  • 消息对象 含有 字段:消息对象(Message)里的 字段 = 结构化数据 里的成员变量

结构化数据 & 消息对象 对比

特别注意:

特别注意

a. 添加:在一个 .proto文件 中可定义多个 消息对象

  • 应用场景:尽可能将与 某一消息类型 对应的响应消息格式 定义到相同的 .proto文件 中
  • 实例:
message SearchRequest {  required string query = 1;  optional int32 page_number = 2;  optional int32 result_per_page = 3;}// 与SearchRequest消息类型 对应的 响应消息类型SearchResponsemessage SearchResponse { …}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

b. 一个消息对象 里 可以定义 另外一个消息对象(即嵌套)

message Person {  required string name = 1;  required int32 id = 2;  optional string email = 3;// 该消息类型 定义在 Person消息类型的内部// 即Person消息类型 是 PhoneNumber消息类型的父消息类型  message PhoneNumber {    required string number = 1;  }}<-- 多重嵌套 -->message Outer {   // Level 0  message MiddleAA {  // Level 1    message Inner {   // Level 2      required int64 ival = 1;      optional bool  booly = 2;    }  }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

2. 字段

  • 消息对象的字段 组成主要是:字段 = 字段修饰符 + 字段类型 +字段名 +标识号

字段组成

  • 下面将对每一项详细介绍

a. 字段修饰符

  • 作用:设置该字段解析时的规则
  • 具体类型如下:

字段修饰符类型

b. 字段类型

字段类型主要有 三 类:

  • 基本数据 类型
  • 枚举 类型
  • 消息对象 类型
message Person {  // 基本数据类型 字段  required string name = 1;  required int32 id = 2;  optional string email = 3;  enum PhoneType {    MOBILE = 0;    HOME = 1;    WORK = 2;  }  message PhoneNumber {    optional PhoneType type = 2 [default = HOME];    // 枚举类型 字段  }  repeated PhoneNumber phone = 4;  // 消息类型 字段}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

1. 基本数据类型

.proto基本数据类型 对应于 各平台的基本数据类型如下: 
基本数据类型对应表

2. 枚举类型

  • 作用:为字段指定一个 可能取值的字段集合 
    该字段只能从 该指定的字段集合里 取值
  • 说明:如下面例子,电话号码 可能是手机号、家庭电话号或工作电话号的其中一个,那么就将PhoneType定义为枚举类型,并将加入电话的集合( MOBILE、 HOMEWORK
// 枚举类型需要先定义才能进行使用// 枚举类型 定义 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
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

额外说明

当对一个 使用了枚举类型的.proto文件 使用 Protocol Buffer编译器编译时,生成的代码文件中:


  • 对 Java 或 C++来说,将有一个对应的 enum 文件
  • 对 Python 来说,有一个特殊的EnumDescriptor 类 

被用来在运行时生成的类中创建一系列的整型值符号常量(symbolic constants)

3. 消息对象 类型

一个消息对象 可以将 其他消息对象类型 用作字段类型,情况如下:

消息对象 类型情况

3.1 使用同一个 .proto 文件里的消息类型

a. 使用 内部消息类型

  • 目的:先在 消息类型 中定义 其他消息类型 ,然后再使用

    即嵌套,需要 用作字段类型的 消息类型 定义在 该消息类型里

  • 实例:

message Person {  required string name = 1;  required int32 id = 2;  optional string email = 3;// 该消息类型 定义在 Person消息类型的内部// 即Person消息类型 是 PhoneNumber消息类型的父消息类型  message PhoneNumber {    required string number = 1;  }  repeated PhoneNumber phone = 4;  // 直接使用内部消息类型}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

b. 使用 外部消息类型

即外部重用,需要 用作字段类型的消息类型 定义在 该消息类型外部

message Person {  required string name = 1;  required int32 id = 2;  optional string email = 3;}message AddressBook {  repeated Person person = 1;  // 直接使用了 Person消息类型作为消息字段}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

c. 使用 外部消息的内部消息类型

message Person {  required string name = 1;  required int32 id = 2;  optional string email = 3;// PhoneNumber消息类型 是 Person消息类型的内部消息类型  message PhoneNumber {    required string number = 1;    optional PhoneType type = 2 [default = HOME];  }}// 若父消息类型外部的消息类型需要重用该内部消息类型// 需要以 Parent.Type 的形式去使用// Parent = 需要使用消息类型的父消息类型,Type = 需要使用的消息类型// PhoneNumber父消息类型Person 的外部 OtherMessage消息类型 需要使用 PhoneNumber消息类型message OtherMessage {  optional Person.PhoneNumber phonenumber = 1;// 以 Parent.Type = Person.PhoneNumber  的形式去使用}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

3.2 使用不同 .proto 文件里的消息类型

  • 目的:需要在 A.proto文件 使用 B.proto文件里的消息类型
  • 解决方案:在 A.proto文件 通过导入( import) B.proto文件中来使用 B.proto文件 里的消息类型
import "myproject/other_protos.proto"// 在A.proto 文件中添加 B.proto文件路径的导入声明// ProtocolBuffer编译器 会在 该目录中 查找需要被导入的 .proto文件// 如果不提供参数,编译器就在 其调用的目录下 查找
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

当然,在使用 不同 .proto 文件里的消息类型 时 也会存在想 使用同一个 .proto 文件消息类型的情况,但使用都是一样,此处不作过多描述。

3.3 将 消息对象类型 用在 RPC(远程方法调用)系统

  • 解决方案:在 .proto 文件中定义一个 RPC 服务接口,Protocol Buffer编译器会根据所选择的不同语言平台 生成服务接口代码
  • 由于使用得不多,此处不作过多描述,具体请看该文档

c. 字段名

该字段的名称,此处不作过多描述。


d. 标识号

  • 作用:通过二进制格式唯一标识每个字段

    1. 一旦开始使用就不能够再改变
    2. 标识号使用范围:[1,2的29次方 - 1]
    3. 不可使用 [19000-19999] 标识号, 因为 Protobuf 协议实现中对这些标识号进行了预留。假若使用,则会报错
  • 编码占有内存规则: 
    每个字段在进行编码时都会占用内存,而 占用内存大小 取决于 标识号:

    1. 范围 [1,15] 标识号的字段 在编码时占用1个字节;
    2. 范围 [16,2047] 标识号的字段 在编码时占用2个字节
  • 使用建议

    1. 为频繁出现的 消息字段 保留 [1,15] 的标识号
    2. 为将来有可能添加的、频繁出现的 消息字段预留 [1,15] 标识号

关于 字段 的高级用法

高级用法

1. 更新消息对象 的字段

  • 目的:为了满足新需求,需要更新 消息类型 而不破坏已有消息类型代码 

    即新、老版本需要兼容

  • 更新字段时,需要符合下列规则:

更新规则

2. 扩展消息对象 的字段

  • 作用:使得其他人可以在自己的 .proto 文件中为 该消息对象 声明新的字段而不必去编辑原始文件 
    1. 注:扩展 可以是消息类型也可以是字段类型 
    2. 以下以 扩展 消息类型 为例

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实现中预留}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

现在,其他人 就可以在自己的 .proto文件中 添加新字段到Request里。如下:

B.proto

extend Request {  optional int32 bar = 126;  // 添加字段的 标识号必须要在指定的范围内  // 消息Request 现在有一个名为 bar 的 optional int32 字段  // 当Request消息被编码时,数据的传输格式与在Request里定义新字段的效果是完全一样的  //  注:在同一个消息类型中一定要确保不会扩展新增相同的标识号,否则会导致数据不一致;可以通过为新项目定义一个可扩展标识号规则来防止该情况的发生}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 要访问 扩展字段 的方法与 访问普通的字段 不同:使用专门的扩展访问函数
  • 实例:
// 如何在C++中设置 bar 值Request request;request.SetExtension(bar, 15);// 类似的模板函数 HasExtension(),ClearExtension(),GetExtension(),MutableExtension(),以及 AddExtension()// 与对应的普通字段的访问函数相符
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

嵌套的扩展

可以在另一个 消息对象里 声明扩展,如:

message Carson {  extend Request {    optional int32 bar = 126;  }  …}// 访问此扩展的C++代码:Request request;request.SetExtension(Baz::bar, 15);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 对于嵌套的使用,一般的做法是:在扩展的字段类型的范围内定义该扩展
  • 实例:一个 Request 消息对象需要扩展(扩展的字段类型是Car 消息类型),那么,该扩展就定义在 Car消息类型 里:
message Car {  extend Request {    optional Car request_ext = 127;// 注:二者并没有子类、父类的关系  }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 至此,Protoco Buffer的语法已经讲解完毕
  • 关于如何根据需求 通过Protoco Buffer语法 去构建 数据结构 相信大家已经非常熟悉了。
  • 在将 .proto文件保存后,进入下一个步骤

步骤2:通过 Protocol Buffer 编译器 编译 .proto 文件

  • 作用:将 .proto 文件 转换成 对应平台的代码文件 

    Protoco Buffer提供 C++、Java、Python 三种开发语言的 API

  • 具体生成文件与平台有关:

对应平台生成文件
  • 编译指令说明
// 在 终端 输入下列命令进行编译protoc -I=$SRC_DIR --xxx_out=$DST_DIR   $SRC_DIR/addressbook.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会根据不同平台生成对应的代码文件
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 具体实例
// 编译说明// 1. 生成Java代码// 2. 需要编译的.proto文件在桌面,希望编译后生成的代码也放在桌面protoc -I=/Users/Carson_Ho/Desktop --java_out=/Users/Carson_Ho/Desktop /Users/Carson_Ho/Desktop/Demo.proto// 编译通过后,Protoco Buffer会按照标准Java风格,生成Java类及目录结构
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

在指定的目录能看到一个Demo的包文件(含 java类文件)

生成的文件

编译功能的拓展

a. 使用Android Studio插件进行编译

  • 需求场景:每次手动执行 Protocol Buffer 编译器将 .proto 文件转换为 Java 文件 操作不方便
  • 解决方案:使用 Android Studio的 gradle 插件 protobuf-gradle-plugin,以便于在项目编译时 自动执行 Protocol Buffers 编译器

关于protobuf-gradle-plugin插件有兴趣的读者可自行了解,但个人还是建议使用 命令行,毕竟太过折腾插件没必要

b. 动态编译

  • 需求场景:某些情况下,人们无法预先知道 .proto 文件,他们需要动态处理一些未知的 .proto 文件 

    如一个通用的消息转发中间件,它无法预先知道需要处理什么类型的数据结构消息

  • 解决方案:动态编译.proto文件

由于使用得不多,此处不作过多描述,具体请看官方文档

c. 编写新的 .proto 编译器

  • 需求场景: Protocol Buffer 仅支持 C++、java 和 Python 三种开发语言,一旦超出该三种开发语言,Protocol Buffer将无法使用
  • 解决方案:使用 Protocol Buffer 的 Compiler 包 开发出支持其他语言的新的.proto编译器

由于使用得不多,此处不作过多描述,具体请看官方文档


5.3 应用到具体平台(Android平台)

  • 终于到了应用到具体平台项目中的步骤了。 

    此处以 Android平台 为例

  • 具体步骤如下:

具体步骤

步骤1:将生成的 代码文件 放入到项目中

  • 对于Android(Java)平台,即将编译.proto文件生成的Java包文件 整个复制到 Android 项目中
  • 放置路径: app/src/main/java的 文件夹里

项目结构示意图

步骤2:在 Gradle 添加 Protocol Buffer 版本依赖

compile 'com.google.protobuf:protobuf-java:2.6.1'// 注:protobuf-java的版本 一定要和 安装protocobuffer的版本 一致
  • 1
  • 2
  • 1
  • 2

步骤3:具体在Android项目中使用

3.1 消息对象类介绍

通过.proto文件 转换的 Java源代码 = Protocol Buffer 类 + 消息对象类(含Builder内部类)

消息对象类 是 Protocol Buffer 类的内部类

由于最常用的都是 消息对象类 和其内部类Builder类 的方法&成员变量,所以此处主要讲解这两者。

3.1.1 消息对象类(Message类)
  • 消息对象类 类通过 二进制数组 写 和 读 消息类型
  • 使用方法包括:
<-- 方式1:直接序列化和反序列化 消息 -->protocolBuffer.toByteArray();// 序列化消息 并 返回一个包含它的原始字节的字节数组protocolBuffer.parseFrom(byte[] data);// 从一个字节数组 反序列化(解析) 消息<-- 方式2:通过输入/ 输出流(如网络输出流) 序列化和反序列化消息 -->protocolBuffer.writeTo(OutputStream output);output.toByteArray();// 将消息写入 输出流 ,然后再 序列化消息 protocolBuffer.parseFrom(InputStream input);// 从一个 输入流 读取并 反序列化(解析)消息// 只含包含字段的getters方法// required string name = 1;public boolean hasName();// 如果字段被设置,则返回truepublic java.lang.String getName();// required int32 id = 2;public boolean hasId();public int getId();// optional string email = 3;public boolean hasEmail();public String getEmail();// repeated .tutorial.Person.PhoneNumber phone = 4;// 重复(repeated)字段有一些额外方法public List<PhoneNumber> getPhoneList();public int getPhoneCount();// 列表大小的速记// 作用:通过索引获取和设置列表的特定元素的getters和setters
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

常用的如上,更多请看官方文档

3.1.2 Builder

作用:创建 消息构造器 & 设置/ 获取消息对象的字段值 & 创建 消息类 实例

属于 消息对象类 的内部类

a. 创建 消息构造器

Demo.Person.Builder person = Person.newBuilder();
  • 1
  • 1

b. 设置/ 获取 消息对象的字段值 具体方法如下:

// 标准的JavaBeans风格:含getters和setters// required string name = 1;public boolean hasName();// 如果字段被设置,则返回truepublic java.lang.String getName();public Builder setName(String value);public Builder clearName(); // 将字段设置回它的空状态// required int32 id = 2;public boolean hasId();public int getId();public Builder setId(int value);public Builder clearId();// optional string email = 3;public boolean hasEmail();public String getEmail();public Builder setEmail(String value);public Builder clearEmail();// repeated .tutorial.Person.PhoneNumber phone = 4;// 重复(repeated)字段有一些额外方法public List<PhoneNumber> getPhoneList();public int getPhoneCount();// 列表大小的速记// 作用:通过索引获取和设置列表的特定元素的getters和setterspublic PhoneNumber getPhone(int index);public Builder setPhone(int index, PhoneNumber value);public Builder addPhone(PhoneNumber value);// 将新元素添加到列表的末尾public Builder addAllPhone(Iterable<PhoneNumber> value);// 将一个装满元素的整个容器添加到列表中public Builder clearPhone();public Builder isInitialized() // 检查所有 required 字段 是否都已经被设置public Builder toString() :// 返回一个人类可读的消息表示(用于调试)public Builder mergeFrom(Message other)// 将 其他内容 合并到这个消息中,覆写单数的字段,附接重复的。public Builder clear()// 清空所有的元素为空状态。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

3.2 具体使用

  • 使用步骤如下: 
    步骤1:通过 消息类的内部类Builder类 构造 消息构造器 
    步骤2:通过 消息构造器 设置 消息字段的值 
    步骤3:通过 消息构造器 创建 消息类 对象 
    步骤4:序列化 / 反序列化 消息

  • 具体使用如下:(注释非常清晰)

public class MainActivity extends AppCompatActivity {    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        // 步骤1:通过 消息类的内部类Builder类 构造 消息类的消息构造器        Demo.Person.Builder personBuilder =  Demo.Person.newBuilder();        // 步骤2:设置你想要设置的字段为你选择的值        personBuilder.setName("Carson");// 在定义.proto文件时,该字段的字段修饰符是required,所以必须赋值        personBuilder.setId(123);// 在定义.proto文件时,该字段的字段修饰符是required,所以必须赋值        personBuilder.setEmail("carson.ho@foxmail.com"); // 在定义.proto文件时,该字段的字段修饰符是optional,所以可赋值 / 不赋值(不赋值时将使用默认值)        Demo.Person.PhoneNumber.Builder phoneNumber =  Demo.Person.PhoneNumber.newBuilder();        phoneNumber.setType( Demo.Person.PhoneType.HOME);// 直接采用枚举类型里的值进行赋值        phoneNumber.setNumber("0157-23443276");        // PhoneNumber消息是嵌套在Person消息里,可以理解为内部类        // 所以创建对象时要通过外部类来创建        // 步骤3:通过 消息构造器 创建 消息类 对象        Demo.Person person = personBuilder.build();        // 步骤4:序列化和反序列化消息(两种方式)        /*方式1:直接 序列化 和 反序列化 消息 */        // a.序列化        byte[] byteArray1 = person.toByteArray();        // 把 person消息类对象 序列化为 byte[]字节数组        System.out.println(Arrays.toString(byteArray1));        // 查看序列化后的字节流        // b.反序列化        try {            Demo.Person person_Request = Demo.Person.parseFrom(byteArray1);            // 当接收到字节数组byte[] 反序列化为 person消息类对象            System.out.println(person_Request.getName());            System.out.println(person_Request.getId());            System.out.println(person_Request.getEmail());            // 输出反序列化后的消息        } catch (IOException e) {            e.printStackTrace();        }        /*方式2:通过输入/ 输出流(如网络输出流) 序列化和反序列化消息 */        // a.序列化        ByteArrayOutputStream output = new ByteArrayOutputStream();         try {            person.writeTo(output);            // 将消息序列化 并写入 输出流(此处用 ByteArrayOutputStream 代替)        } catch (IOException e) {            e.printStackTrace();        }        byte[] byteArray = output.toByteArray();        // 通过 输出流 转化成二进制字节流        // b. 反序列化        ByteArrayInputStream input = new ByteArrayInputStream(byteArray);        // 通过 输入流 接收消息流(此处用 ByteArrayInputStream 代替)        try {            Demo.Person person_Request = Demo.Person.parseFrom(input);            // 通过输入流 反序列化 消息            System.out.println(person_Request.getName());            System.out.println(person_Request.getId());            System.out.println(person_Request.getEmail());            // 输出消息        } catch (IOException e) {            e.printStackTrace();        }    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82

Demo 地址

Carson_Ho的Github :https://github.com/Carson-Ho/ProtocolBuffer

高级功能

  • 贴心的Google还提供将Protocol Buff 编码方式 转化为 其他编码方式,如 JsonXML等等

    即将 Protocol Buff 对象 转化为其他编码方式的数据存储对象

  • 下面展示的是 将 Protocol Buff 对象 转化为 Json对象

// 步骤1:在Gradle加入依赖compile 'com.googlecode.protobuf-java-format:protobuf-java-format:1.4'// 步骤2:将`Protocol Buff` 对象 序列化 为 `Json`对象JsonFormat jsonFormat = new JsonFormat();  String person2json = jsonFormat.printToString(mProtoBuffer); 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

6. 总结

  • 在 传输数据量较大的需求场景下,Protocol BufferXML、Json 更小、更快、使用 & 维护更简单!
  • 下面用 一张图 总结在 Android平台中使用 Protocol Buffer 的整个步骤流程:

总结


0 0
原创粉丝点击