Google Protobuf 编解码
Google Protobuf 优点:
- 在谷歌内部长期使用, 产品成熟度高.
- 跨语言、支持多种语言, 包括 C++、Java 和 Python.
- 编码后的消息更小, 更加有利于存储和传输.
- 编解码的性能非常高.
- 支持不同协议版本的前向兼容.
- 支持定义可选和必选字段.
Protobuf 的入门
Protobuf 是一个灵活、高效、结构化的数据序列化框架, 相比与 xml 等传统的序列化工具, 它更小、更快、更简单.
Protobuf 支持数据结构化一次可以到处使用, 甚至跨语言使用, 通过代码生成工具可以自动生成不同语言版本的源代码, 甚至可以在使用不同版本的数据结构进程间进行数据传递, 实现数据结构前向兼容.
定义消息类型
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}该文件的第一行指定使用 proto3 语法, 如果不写的话表示 proto2.
分配字段编号
string query = 1; 1 就是字段编号, 字段号主要用来标识二进制格式字段的. 1 到 15 字段号占一个字节. 16 到 2047 字段号需要两个字节.
我们将对象转换为报文的时候, 是按照字段编号进行报文封装的; 我们接收到数据之后框架会帮我们按照字段号进行赋值.
不能使用数字19000到19999, 因为它们是为 Google Protobuf 保留的.
字段类型对应
| .proto Type | Notes | C++ Type | Java Type |
|---|---|---|---|
| double | double | double | |
| float | float | float | |
| int32 | 使用可变长度编码, 对负数编码效率低下 如果您的字段可能有负值, 则使用sint32代替. | int32 | int |
| int64 | 使用可变长度编码, 对负数编码效率低下 如果您的字段可能有负值, 则使用sint64代替. | int64 | long |
| uint32 | 使用可变长度编码 | uint32 | int |
| uint64 | 使用可变长度编码 | uint64 long | |
| sint32 | 使用可变长度编码 有符号的int值这些编码比常规int32更有效地编码负数 | uint32 | int |
| sint64 | 使用可变长度编码 有符号的int值这些编码比常规int64更有效地编码负数 | int64 | long |
| fixed32 | 四个字节, 如果值通常大于2的28次方, 则比uint32更有效 | uint32 | int |
| fixed64 | 四个字节, 如果值通常大于2的56次方, 则比uint64更有效 | uint64 | long |
| sfixed32 | 四个字节 | int32 | int |
| sfixed64 | 四个字节 | int64 | long |
| bool | bool | boolean | |
| string | 字符串必须始终包含UTF-8编码或7位ASCII文本 | string | String |
| bytes | 字符串必须始终包含UTF-8编码或7位ASCII文本 | string | ByteString |
默认值
- 对于字符串, 默认值是空字符串.
- 对于字节, 默认值为空字节.
- 对于bool, 默认值为false.
- 对于数字类型, 默认值为零.
- 对于枚举, 默认值是第一个定义的枚举值, 必须为0.
允许嵌套
Protocol Buffers 定义 message 允许嵌套组合成更加复杂的消息
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}更多的例子:
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
message SomeOtherMessage {
SearchResponse.Result result = 1;
}message Outer { // Level 0
message MiddleAA { // Level 1
message Inner { // Level 2
int64 ival = 1;
bool booly = 2;
}
}
message MiddleBB { // Level 1
message Inner { // Level 2
int32 ival = 1;
bool booly = 2;
}
}
}导入定义
可以在文件的顶部添加一个import语句:
import "myproject/other_protos.proto";
未知字段
未知字段就是解析器无法识别的字段. 例如, 当服务端使用新消息发送数据, 客户端使用旧消息解析数据, 那么这些新字段将成为旧消息中的未知字段.
在3.5和更高版本中, 未知字段在解析过程中被保留, 并包含在序列化中输出.
Map 类型
repeated 类型可以用来表示数组, Map 类型则可以用来表示字典.
map<key_type, value_type> map_field = N; map<string, Project> projects = 3;
key_type 可以是任何 int 或者 string 类型(任何的标量类型, 具体可以见上面标量类型对应表格, 但是要除去 float、double 和 bytes)
key_type 可以是除去 Map 以外的任何类型.
需要特别注意的是:
- map 是不能用
repeated修饰的. - map 迭代顺序的是不确定的, 所以你不能确定 map 是一个有序的.
- 为
.proto生成文本格式时, map 按 key 排序. 数字的 key 按数字排序. - 从数组中解析或合并时, 如果有重复的 key, 则使用所看到的最后一个 key(覆盖原则).从文本格式解析映射时, 如果有重复的 key, 解析可能会失败.
Protocol Buffer 虽然不支持 map 类型的数组, 但是可以转换一下, 用以下思路实现 maps 数组:
message MapFieldEntry {
key_type key = 1;
value_type value = 2;
}
repeated MapFieldEntry map_field = N;上述写法和 map 数组是完全等价的,所以用 repeated 巧妙的实现了 maps 数组的需求.
Protocol Buffer 命名规范
message 采用驼峰命名法. message 首字母大写开头. 字段名采用下划线分隔法命名.
message SongServerRequest {
required string song_name = 1;
}枚举类型采用驼峰命名法. 枚举类型首字母大写开头. 每个枚举值全部大写, 并且采用下划线分隔法命名.
enum Foo {
FIRST_VALUE = 0;
SECOND_VALUE = 1;
}每个枚举值用分号结束, 不是逗号.
服务名和方法名都采用驼峰命名法. 并且首字母都大写开头.
service FooService {
rpc GetSomething(FooRequest) returns (FooResponse);
}常用方法
getDefaultInstance(): 返回单例实例, 它与 newBuilder().build() 实例相同getDescriptor(): 返回类型的描述符. 包括具有哪些字段以及类型. 这可以与 Message 的反射方法一起使用, 例如getField().parseFrom(...): 返回反序列化后的 Message. 注意不会抛出 UninitializedMessageException 和 InvalidProtocolBufferException 异常.Message.Builder: 中的 mergeFrom() 放会将数据解析为此类型的消息, 并进行消息合并.newBuilder(): 创建一个新的构建器.
Any
Any类型允许包装任意的message类型:
import "google/protobuf/any.proto";
message Response {
google.protobuf.Any data = 1;
}总结
message SubscribeReq {
int32 subReqID = 1;
string userName = 2;
string productName = 3;
string address = 4;
}可以通过 pack() 和 unpack()(方法名在不同的语言中可能不同)方法装箱/拆箱,以下是Java的例子:
People people = People.newBuilder().setName("proto").setAge(1).build();
// protoc编译后生成的message类
Response r = Response.newBuilder().setData(Any.pack(people)).build();
// 使用Response包装people
System.out.println(r.getData().getTypeUrl());
// type.googleapis.com/example.protobuf.people.People
System.out.println(r.getData().unpack(People.class).getName());
// protoOneof
如果你有一些字段同时最多只有一个能被设置, 可以使用 oneof 关键字来实现, 任何一个字段被设置, 其它字段会自动被清空(被设为默认值):
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}默认值
比如我们创建了上面的消息类型, 我们在代码中设置 builder.setSubReqID(0); 为 0, 零是数值类型的默认值; 所以我们会看到序列化后的数据中, 没有对此字段进行序列化.
byte[] arry = builder.build().toByteArray();
arry 长度为 0. 对于字段类型是 string 类型的也是一样的; 也就是说显示赋值默认值也不会对其进行序列化.
保留字段
message SubscribeReq {
reserved 2;
int32 subReqID = 1;
string userName = 2;
string productName = 3;
string address = 4;
}顾名思义, 就是此字段会被保留可能在以后会使用此字段. 使用关键字 reserved 表示我要保留字段数 2.
上面代码我们在生成 Java 文件的时候会出现 ubscribeReqPeoro.proto: Field "userName" uses reserved number 2 错误信息, 所以我们需要将 string userName = 2; 注释, 或者删除.
保留后我们无法对其设置或序列化和反序列化.