由于 Google出品,我相信Protocol Buffer已经具备足够的吸引力
今天,我将献上一份 Protocol Buffer的介绍 & 使用攻略,希望你们会喜欢。一种 结构化数据 的数据存储格式(类似于 XML、Json )
Google 出品 (开源)Protocol Buffer 目前有两个版本:proto2 和 proto3因为proto3 还是beta 版,所以本次讲解是 proto2通过将 结构化的数据 进行 串行化(序列化),从而实现 数据存储 / RPC 数据交换的功能
序列化: 将 数据结构或对象 转换成 二进制串 的过程反序列化:将在序列化过程中所生成的二进制串 转换成 数据结构或者对象 的过程传输数据量大 & 网络环境不稳定 的数据存储、RPC 数据交换 的需求场景
如 即时IM (QQ、微信)的需求场景
在 传输数据量较大的需求场景下,Protocol Buffer比XML、Json 更小、更快、使用 & 维护更简单!
使用 Protocol Buffer 的流程如下:
Protocol Buffer使用流程要使用Protocol Buffer ,需要先在电脑上安装Protocol Buffer
整个 安装过程 只需要按照以下步骤进行即可:
整个安装过程请 自备梯子 以保证 网络畅通
此处选择 较稳定的版本 protobuf-2.6.1.tar.gz 进行演示
下载成功后,对文件进行解压,如下图:
安装包 & 解压后文件打开 您的终端 依次输入 下列指令 即可:
brew install autoconf automake libtool curl // Step1:安装 Protocol Buffer 依赖 // 注:Protocol Buffer 依赖于 autoconf、automake、libtool、curl cd 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出现 libprotoc 2.6.1 提示即表示 安装成功,如下图
安装成功提示特别注意:
protoc = Protocol Buffer的编译器作用:将 .proto文件 编译成对应平台的 头文件和源代码文件在下面会详细介绍至此, Protocol Buffer已经安装完成。下面将讲解如何具体使用Protocol Buffer
下面将通过一个实例(Android(Java) 平台为例)详细介绍每个步骤。
此处叫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; } 下面将结合 上述例子 对 Protocol Buffer 语法 进行详细介绍 Protocol Buffer语法每个包会被看作是其父类包的内部类
Protocol buffer 编译器会解析 .proto文件中定义的所有类型名生成器会根据 不同语言 生成 对应语言 的代码文件a. 即对 不同语言 使用了 不同的规则 进行处理b. Protoco Buffer提供 C++、Java、Python 三种语言的 API
作用:影响 特定环境下 的处理方式
但不改变整个文件声明的含义
常用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 在 ProtocolBuffers 中允许 自定义选项 并 使用该功能属于高级特性,使用频率很低,此处不过多描述。有兴趣可查看官方文档下面会详细介绍 .proto 消息模型里的 消息对象 & 字段
消息模型在 ProtocolBuffers 中:
一个消息对象(Message) = 一个 结构化数据消息对象用 修饰符 message 修饰消息对象 含有 字段:消息对象(Message)里的 字段 = 结构化数据 里的成员变量 结构化数据 & 消息对象 对比特别注意:
特别注意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; // 消息类型 字段 }.proto基本数据类型 对应于 各平台的基本数据类型如下:
基本数据类型对应表该字段只能从 该指定的字段集合里 取值
说明:如下面例子,电话号码 可能是手机号、家庭电话号或工作电话号的其中一个,那么就将PhoneType定义为枚举类型,并将加入电话的集合( MOBILE、 HOME、WORK) // 枚举类型需要先定义才能进行使用 // 枚举类型 定义 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值是使用可变编码方式的,对负数不够高额外说明当对一个 使用了枚举类型的.proto文件 使用 Protocol Buffer编译器编译时,生成的代码文件中:
对 Java 或 C++来说,将有一个对应的 enum 文件对 Python 来说,有一个特殊的EnumDescriptor 类被用来在运行时生成的类中创建一系列的整型值符号常量(symbolic constants)
一个消息对象 可以将 其他消息对象类型 用作字段类型,情况如下:
消息对象 类型情况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; // 直接使用内部消息类型 }b. 使用 外部消息类型
即外部重用,需要 用作字段类型的消息类型 定义在 该消息类型外部
message Person { required string name = 1; required int32 id = 2; optional string email = 3; } message AddressBook { repeated Person person = 1; // 直接使用了 Person消息类型作为消息字段 }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 的形式去使用 }当然,在使用 不同 .proto 文件里的消息类型 时 也会存在想 使用同一个 .proto 文件消息类型的情况,但使用都是一样,此处不作过多描述。
该字段的名称,此处不作过多描述。
作用:通过二进制格式唯一标识每个字段
一旦开始使用就不能够再改变标识号使用范围:[1,2的29次方 - 1]不可使用 [19000-19999] 标识号, 因为 Protobuf 协议实现中对这些标识号进行了预留。假若使用,则会报错编码占有内存规则:每个字段在进行编码时都会占用内存,而 占用内存大小 取决于 标识号:
范围 [1,15] 标识号的字段 在编码时占用1个字节;范围 [16,2047] 标识号的字段 在编码时占用2个字节使用建议
为频繁出现的 消息字段 保留 [1,15] 的标识号为将来有可能添加的、频繁出现的 消息字段预留 [1,15] 标识号即新、老版本需要兼容
更新字段时,需要符合下列规则: 更新规则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实现中预留 }现在,其他人 就可以在自己的 .proto文件中 添加新字段到Request里。如下:
B.proto
extend Request { optional int32 bar = 126; // 添加字段的 标识号必须要在指定的范围内 // 消息Request 现在有一个名为 bar 的 optional int32 字段 // 当Request消息被编码时,数据的传输格式与在Request里定义新字段的效果是完全一样的 // 注:在同一个消息类型中一定要确保不会扩展新增相同的标识号,否则会导致数据不一致;可以通过为新项目定义一个可扩展标识号规则来防止该情况的发生 } 要访问 扩展字段 的方法与 访问普通的字段 不同:使用专门的扩展访问函数实例: // 如何在C++中设置 bar 值 Request request; request.SetExtension(bar, 15); // 类似的模板函数 HasExtension(),ClearExtension(),GetExtension(),MutableExtension(),以及 AddExtension() // 与对应的普通字段的访问函数相符可以在另一个 消息对象里 声明扩展,如:
message Carson { extend Request { optional int32 bar = 126; } … } // 访问此扩展的C++代码: Request request; request.SetExtension(Baz::bar, 15); 对于嵌套的使用,一般的做法是:在扩展的字段类型的范围内定义该扩展实例:一个 Request 消息对象需要扩展(扩展的字段类型是Car 消息类型),那么,该扩展就定义在 Car消息类型 里: message Car { extend Request { optional Car request_ext = 127; // 注:二者并没有子类、父类的关系 } } 至此,Protoco Buffer的语法已经讲解完毕关于如何根据需求 通过Protoco Buffer语法 去构建 数据结构 相信大家已经非常熟悉了。在将 .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. 生成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类及目录结构在指定的目录能看到一个Demo的包文件(含 java类文件)
生成的文件关于protobuf-gradle-plugin插件有兴趣的读者可自行了解,但个人还是建议使用 命令行,毕竟太过折腾插件没必要
如一个通用的消息转发中间件,它无法预先知道需要处理什么类型的数据结构消息
解决方案:动态编译.proto文件由于使用得不多,此处不作过多描述,具体请看官方文档
由于使用得不多,此处不作过多描述,具体请看官方文档
此处以 Android平台 为例
具体步骤如下: 具体步骤通过.proto文件 转换的 Java源代码 = Protocol Buffer 类 + 消息对象类(含Builder内部类)
消息对象类 是 Protocol Buffer 类的内部类
由于最常用的都是 消息对象类 和其内部类Builder类 的方法&成员变量,所以此处主要讲解这两者。
常用的如上,更多请看官方文档
作用:创建 消息构造器 & 设置/ 获取消息对象的字段值 & 创建 消息类 实例
属于 消息对象类 的内部类
a. 创建 消息构造器
Demo.Person.Builder person = Person.newBuilder();b. 设置/ 获取 消息对象的字段值 具体方法如下:
// 标准的JavaBeans风格:含getters和setters // required string name = 1; public boolean hasName();// 如果字段被设置,则返回true public 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和setters public 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:通过 消息类的内部类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(); } } }Carson_Ho的Github :https://github.com/Carson-Ho/ProtocolBuffer
贴心的Google还提供将Protocol Buff 编码方式 转化为 其他编码方式,如 Json、XML等等
即将 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);转载于:https://www.cnblogs.com/xinmengwuheng/p/7070393.html
相关资源:JAVA上百实例源码以及开源项目