protobuf v2
protobuf 是什么
Protocol Buffer (简称Protobuf) 是Google出品的性能优异、跨语言、跨平台的序列化库。
2001年初,Protobuf首先在Google内部创建,很多项目也采用Protobuf进行消息的通讯,还有基于Protobuf的微服务框架GRPC。
可以看做xml、json等序列化的又一种形式,只不过序列化后它是二进制的。
Protobuf支持很多语言,比如C++、C#、Dart、Go、Java、Python、Rust等,同时也是跨平台的。
序列化(serialization、marshalling)的过程是指将数据结构或者对象的状态转换成可以存储(比如文件、内存)或者传输的格式(比如网络)。反向操作就是反序列化(deserialization、unmarshalling)的过程。
protobuf 为什么要有
二十世纪九十年代后期,XML开始流行,它是一种人类易读的基于文本的编码方式,易于阅读和理解。
JSON是一种更轻量级的基于文本的编码方式,经常用在client/server端的通讯中。
除此之外还有很多序列化格式。
protobuf序列化和反序列化速度更快; 文件更小存储需要更少的空间,传输时间短。
protobuf 基础
这个教程主要介绍proto2的开发。
使用protobuf需要一个.proto文件,在这里定义要序列化的格式。可以理解为我们工作中和服务端定义的接口文档或者java bean。
举例: user.proto
syntax = "proto2";
//生成java类所在的包名
package com.example.protobuftest.bean;
message SearchRequest {
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3;
}
第一行指定protobuf的版本,可以指定为proto2或3。如果没有指定,默认以proto2
格式定义。
在这里我们定义了一个User类型,包括name和age字段。
package
package是可选的。对于生成的java语言对于Java,包声明符会变为java的一个包,除非在.proto文件中提供了一个明确有java_package; 如果不写package,默认是文件名作为包名。 写了包名后,就可以用包名加以区分。比如test.model.UserInfo。 显示设置包名后生成的对应语言文件就按照这个来生成包名了。
option java_package = "test.protobuf.sample";
option go_package = "test.protobuf.sample";
字段规则
所指定的消息字段修饰符必须是如下之一:
required:一个格式良好的消息一定要含有1个这种字段。表示该值是必须要设置的; optional:消息格式中该字段可以有0个或1个值(不超过1个)。 repeated:在一个格式良好的消息中,这种字段可以重复任意多次(包括0次)。重复的值的顺序会被保留。表示该值可以重复,相当于java中的List。
required是永久性的:在将一个字段标识为required的时候,应该特别小心。如果在某些情况下不想写入或者发送一个required的字段,将原始该字段修饰符更改为optional可能会遇到问题——旧版本的使用者会认为不含该字段的消息是不完整的,从而可能会无目的的拒绝解析。
换句话说,字段设置了required,如果不设置就会序列化失败,同理,如果也会反序列化失败。
字段类型
.proto Type | Notes | Java Type | Go Type |
---|---|---|---|
double | double | float64 | |
float | float | float32 | |
int32 | 使用可变长度编码。编码负数的效率低 - 如果您的字段可能有负值,请改用sint32。 | int | int32 |
int64 | 使用可变长度编码。编码负数的效率低 - 如果您的字段可能有负值,请改用sint64。 | long | int64 |
uint32 | 使用可变长度编码 | int | uint32 |
uint64 | 使用可变长度编码. | long | uint64 |
sint32 | 使用可变长度编码。签名的int值。这些比常规int32更有效地编码负数。 | int | int32 |
sint64 | 使用可变长度编码。签名的int值。这些比常规int64更有效地编码负数。 | long | int64 |
fixed32 | 总是四个字节。如果值通常大于228,则比uint32更有效。 | int | uint32 |
fixed64 | 总是八个字节。如果值通常大于256,则比uint64更有效 | long | uint64 |
sfixed32 | 总是四个字节 | int | int32 |
sfixed64 | 总是八个字节 | long | int64 |
bool | boolean | bool | |
string | String | string | |
bytes | 可以包含不超过232的任意字节序列。 | String | []byte |
标识号
正如上述文件格式,在消息定义中,每个字段都有唯一的一个数字标识符。这些标识符是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变。注:[1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 [1,15]之内的标识号。切记:要为将来有可能添加的、频繁出现的标识号预留一些标识号。
最小的标识号可以从1开始,最大到2^29 - 1, or 536,870,911。不可以使用其中的[19000-19999]的标识号, Protobuf协议实现中对这些进行了预留。如果非要在.proto文件中使用这些预留标识号,编译时就会报警。
类型嵌套及导入
在一个.proto文件中可以定义多个消息类型,可以引用,也可以嵌套
message SearchResponse {
repeated Result result = 1;
}
message Result {
required string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
要导入其他.proto文件的定义,你需要在你的文件中添加一个导入声明,如:
import "myproject/other_protos.proto";
嵌套使用:
message SearchResponse {
message Result {
required string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
repeated Result result = 1;
}
如果你想在它的父消息类型的外部重用这个消息类型,你需要以Parent.Type的形式使用它
Optional的字段和默认值
如上所述,消息描述中的一个元素可以被标记为“可选的”(optional)。一个格式良好的消息可以包含0个或一个optional的元素。当解 析消息时,如果它不包含optional的元素值,那么解析出来的对象中的对应字段就被置为默认值。默认值可以在消息描述文件中指定。
optional int32 result_per_page = 3 [default = 10];
如果没有为optional的元素指定默认值,就会使用与特定类型相关的默认值:对string来说,默认值是空字符串。对bool来说,默认值是false。对数值类型来说,默认值是0。对枚举来说,默认值是枚举类型定义中的第一个值。
枚举
message SearchRequest {
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3 [default = 10];
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
optional Corpus corpus = 4 [default = UNIVERSAL];
}
枚举常量必须在32位整型值的范围内。因为enum值是使用可变编码方式的,对负数不够高效,因此不推荐在enum中使用负数。
扩展
通过扩展,可以将一个范围内的字段标识号声明为可被第三方扩展所用。然后,其他人就可以在他们自己的.proto文件中为该消息类型声明新的字段,而不必去编辑原始文件了。
message Foo {
// ...
extensions 100 to 199;
}
在消息Foo中,范围[100,199]之内的字段标识号被保留为扩展用。现在,其他人就可以在他们自己的.proto文件中添加新字段到Foo里了。
extend Foo {
optional int32 bar = 126;
}
消息Foo现在有一个名为bar的optional int32字段。然而,要在程序代码中访问扩展字段的方法与访问普通的字段稍有不同。
如果你的消息中有很多可选字段, 并且同时至多一个字段会被设置, 你可以加强这个行为,使用oneof特性节省内存.
Oneof
Oneof字段就像可选字段, 除了它们会共享内存, 至多一个字段会被设置。 设置其中一个字段会清除其它oneof字段。
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
oneof中字段不能使用 required, optional, repeated 关键字.
Map
如果你希望创建一个关联映射,protocol buffer提供了一种快捷的语法:
map<key_type, value_type> map_field = N;
其中key_type可以是任意Integer或者string类型(所以,除了floating和bytes的任意标量类型都是可以的)value_type可以是任意类型。
- Map的字段不可以是repeated,optional,required。
- 序列化后的顺序和map迭代器的顺序是不确定的,所以你不要期望以固定顺序处理Map
定义服务(Service)
如果想要将消息类型用在RPC(远程方法调用)系统中,可以在.proto文件中定义一个RPC服务接口。
service SearchService {
rpc Search (SearchRequest) returns (SearchResponse);
}