protobuf v3
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 基础
官方推荐新代码采用proto3,这个教程主要介绍proto3的开发。
使用protobuf需要一个.proto文件,在这里定义要序列化的格式。可以理解为我们工作中和服务端定义的接口文档或者java bean。
举例: user.proto
syntax = "proto3";
//生成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";
字段规则
所指定的消息字段修饰符必须是如下之一: singular:一个格式良好的消息应该有0个或者1个这种字段(但是不能超过1个)。这是proto3语法的默认字段规则。 repeated:在一个格式良好的消息中,这种字段可以重复任意多次(包括0次)。重复的值的顺序会被保留。
protobuf2中的required、optional规则已经不能使用了。
字段类型
.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文件中使用这些预留标识号,编译时就会报警。
保留标识符(Reserved)
如果你通过删除或者注释所有域,以后的用户在更新这个类型的时候可能重用这些标识号。如果你使用旧版本加载相同的.proto文件会导致严重的问题,包括数据损坏、隐私错误等等。现在有一种确保不会发生这种情况的方法就是为字段tag(reserved name可能会JSON序列化的问题)指定reserved标识符,protocol buffer的编译器会警告未来尝试使用这些域标识符的用户。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
注:不要在同一行reserved声明中同时声明域名字和tag number。
类型嵌套及导入
在一个.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的形式使用它
枚举
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];
}
protbuf3中第一个字段必须是0,枚举类的第一个值总是默认值。
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。
- 序列化后的顺序和map迭代器的顺序是不确定的,所以你不要期望以固定顺序处理Map
定义服务(Service)
如果想要将消息类型用在RPC(远程方法调用)系统中,可以在.proto文件中定义一个RPC服务接口。
service SearchService {
rpc Search (SearchRequest) returns (SearchResponse);
}