[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 map_field = N;
map projects = 3;
key_type
可以是任何 int
或者 string
类型(任何的标量类型, 具体可以见上面标量类型对应表格, 但是要除去 float
、double
和 bytes
)
枚举值也不能作为 key.
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());
// proto
Oneof
如果你有一些字段同时最多只有一个能被设置, 可以使用 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;
注释, 或者删除.
保留后我们无法对其设置或序列化和反序列化.