目录
- protobuf介绍
- protobuf使用
- protoc命令
- 语法
- 定义消息类型
- 指定字段类型
- 分配字段编号
- 指定字段规则
- 添加更多消息类型
- 注释
- 保留字段
- 从.proto文件生成了什么?
- 值类型
- 默认值
- 枚举
- 使用其他消息类型
- 导入定义
- 嵌套类型
- 更新消息类型
- 未知字段
- any任意类型
- oneof
- oneof 特性
- 兼容性问题
- Maps
- Packages
- 定义服务
- JSON 映射
- Options
- 参考文档
protobuf介绍
Protobuf是Protocol Buffer的简称,它是Google公司于2008年开源的一种高效的平台无关、语言无关、可扩展的数据格式,目前Protobuf作为接口规范的描述语言,可以作为Go语言RPC接口的基础工具。
protobuf使用
protobuf是一个与语言无关的一个数据协议,所以我们需要先编写IDL文件然后借助专用工具生成指定语言的代码,从而实现数据的序列化与反序列化过程。
大致开发流程如下: 1. IDL编写 2. 生成指定语言的代码 3. 序列化和反序列化
protoc命令
protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto
- IMPORT_PATH指定寻找proto 的目录去解决import 带来的依赖问题,如果省略,默认是当前文件夹。多个import 文件夹可以通过参数使用–proto_path多次来解决,编译器将会按顺序搜索。也可以用简写-I=IMPORT_PATH来表示–proto_path
- –cpp_out,–java_out,–go_out等等代表指定生成的语言,可以生成多个语言
- path/to/file.proto 代表输入的proto 文件,可以用*.proto 代表输入文件夹内多个文件
语法
定义消息类型
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
- 文件第一行指定您使用的语法:如果不这样做,编译器将假定使用的是proto2。
- 这必须是.proto3文件的第一个非空、非注释行。
- 消息定义指定了三个字段(名称/值对),每个字段对应于要包含在该类型消息中的数据段。
指定字段类型
在上面的示例中,所有字段都是标量类型(scalar types): 两个整数(page_number和 result_per_page)和一个字符串(query)。但是也可以为字段指定组合类型,包括枚举和其他消息类型。
分配字段编号
- 消息定义中的每个字段都有一个唯一编号。
- 这些字段编号用于标识消息二进制格式的字段,并且在消息类型投入使用后不应更改。
- 请注意,1到15范围内的字段编号需要一个字节进行编码,编码内包括字段号和字段类型(参考协议缓冲区编码)。
- 16到2047范围内的字段编号需要两个字节(进行编码)。因此,您应该把1到15的消息编号留给非常频繁出现的消息元素。
- 请记住为将来可能添加的频繁出现的元素留出一些空间。可以指定的最小字段号为1,最大字段号为
(2**29)-1
(字段数字会作为key,key最后三位是类型)或536870911
。您也不能使用数字19000
到19999
(字段描述符),因为它们是协议缓冲区的保留数字,如果你在你的.proto中使用了这些数字,编译器会报错。同样,不能使用任何以前保留的字段号。
指定字段规则
消息字段可以是下列字段之一:
- singular: 格式正确的消息可以有这个字段的零个或一个(但不能多于一个)。这是 proto3语法的默认字段规则。
- repeated: 该字段可以在格式正确的消息中重复任意次数(包括零次)。重复值的顺序将被保留。
在 proto3中,标量数值类型的repeated
字段默认使用packed
编码。(参考Protocol Buffer Encoding,可以找到关于packed编码的更多信息)
添加更多消息类型
- 可以在一个.proto 文件中定义多个消息类型。
- 如果想定义与 SearchRequest 消息类型对应的应答消息格式SearchResponse,就可以将其添加到同一个.proto文件中。
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
message SearchResponse {
...
}
注释
.proto文件使用了C/C++风格的//
和/* ... */
语法。
/* SearchRequest 表示一个分页查询
* 其中有一些字段指示响应中包含哪些结果 */
message SearchRequest {
string query = 1;
int32 page_number = 2; // 页码数
int32 result_per_page = 3; // 每页返回的结果数
}
保留字段
- 如果通过完全删除某个字段或把它注释掉来更新消息类型,则将来的用户可以在对该类型进行自己的更新时重用该字段编号。
- 如果以后加载相同.proto的旧版本,这可能会导致数据损坏、隐私漏洞等严重问题。
- 确保不会发生这种情况的一种方法是使用reserved关键字指定已删除字段的字段编号为保留编号(也要指定已删除字段名称为保留名称(name),以规避JSON序列化问题)。将来有任何用户试图使用这些字段标识符时,协议缓冲区编译器将报错。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
注意,不能在同一个reserved语句中混合字段名和字段编号。
从.proto文件生成了什么?
当使用 protocol buffer 编译器来运行.proto文件时,编译器用选择的语言生成描述的消息类型
,包括获取和设置字段值,将消息序列化为输出流,以及从输入流解析消息的代码。
-
对于Python:Python 编译器为.proto文件中的每个消息类型生成一个带静态描述符的模块,然后与 metaclass 一起使用,在运行时创建必要的 Python 数据访问类。
-
对于 Go:编译器为文件中的每种消息类型生成一个类型(type)到一个.pb.go 文件。
-
其他…
值类型
.proto Type | Notes | C++ Type | Java/Kotlin Type[1] | Python Type[3] | Go Type | PHP Type |
---|---|---|---|---|---|---|
double | double | double | float | float64 | float | |
float | float | float | float | float32 | float | |
int32 | 使用可变长度编码。编码负数效率低下——如果你的字段可能有负值,则使用 sint32代替。 | int32 | int | int | int32 | integer |
int64 | 使用可变长度编码。编码负数效率低下——如果你的字段可能有负值,则使用 sint64代替。 | int64 | long | int/long[4] | int64 | integer/string[6] |
uint32 | 使用变长编码。 | uint32 | int[2] | int/long[4] | uint32 | integer |
uint64 | 使用变长编码。 | uint64 | long[2] | int/long[4] | uint64 | integer/string[6] |
sint32 | 使用可变长度编码。带符号的 int 值。这些编码比普通的 int32更有效地编码负数。 | int32 | int | int | int32 | integer |
sint64 | 使用可变长度编码。带符号的 int 值。这些编码比普通的 int64更有效地编码负数。 | int64 | long | int/long[4] | int64 | integer/string[6] |
fixed32 | 总是四个字节。如果值经常大于228,则比 uint32更有效率。 | uint32 | int[2] | int/long[4] | uint32 | integer |
fixed64 | 总是8字节。如果值经常大于256,则比 uint64更有效率。 | uint64 | integer/string[6] | |||
sfixed32 | 总是四个字节。 | int32 | int | int | int32 | integer |
sfixed64 | 总是八个字节。 | int64 | integer/string[6] | |||
bool | bool | boolean | bool | bool | boolean | |
string | 字符串必须始终包含 UTF-8编码的或7位 ASCII 文本,且不能长于232。 | string | String | str/unicode[5] | string | string |
bytes | 可以包含任何不超过232字节的任意字节序列。 | string | ByteString | str (Python 2) bytes (Python 3) | []byte | string |
默认值
当解析消息时,如果编码消息不包含特定的 singular 元素,则解析对象中的相应字段将设置为该字段的默认值。
- string:默认值为空字符串
- bytes:默认值为空字节
- boolean:默认值为false
- 数值类型:默认值为0
- 枚举:默认值为第一个定义的枚举值,该值必须是0
- 消息字段:不设默认值,它的确切值取决于语言。
- repeated 字段的默认值为空(通常是适配语言中的空列表)。
枚举
定义消息类型时,可能希望其中一个字段只包含预定义值列表中的一个
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
Corpus corpus = 4;
}
Corpus enum 的第一个常量映射为零,每个 enum 定义必须包含一个常量,该常量映射为零且作为它的第一个元素。这是因为:
- 必须有一个零值,这样我们就可以使用0作为数值默认值。
- 零值必须是第一个元素,以便与 proto2语义兼容,其中第一个枚举值总是默认值。
可以通过将相同的值赋给不同的枚举常量来定义别名。为此,需要将allow_alias选项设置为true,否则协议编译器将在找到别名时将生成错误消息。
message MyMessage1 {
enum EnumAllowingAlias {
// 允许别名
option allow_alias = true;
UNKNOWN = 0;
STARTED = 1;
RUNNING = 1;
}
}
message MyMessage2 {
enum EnumNotAllowingAlias {
UNKNOWN = 0;
STARTED = 1;
// RUNNING = 1; // 取消对此行的注释将导致内部出现编译错误,外部出现警告消息。
}
}
- 枚举器常量必须在32位整数的范围内。由于enum使用可变编码,因此负值效率很低,所以不建议使用。您可以在定义的消息内部定义枚举,如上面的示例所示,也可以在外部定义枚举——这些枚举可以在.proto文件中的任何消息定义中重用。您还可以使用
_MessageType_._EnumType_
语法将一条消息中声明的枚举类型用作另一条消息中的字段类型。 - 当使用协议缓冲区编译器编译一个使用了枚举的.proto文件时,对于Python,会生成一个特殊EnumDescriptor类,用于在运行时生成的类中创建一组具有整数值的符号常量。
syntax = "proto3";
option go_package = "protos/pbs";
enum TestType {
Hello1=0;
Hello2=1;
Hello3=2;
Hello4=3;
}
message HelloRequest {
string greeting = 1;
TestType en=2;
}
message HelloResponse {
string reply = 1;
}
使用其他消息类型
当使用其他消息的时候,如果在本文件,直接使用就可以了。Result代表自定义消息类型
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
// []string
repeated string snippets = 3;
}
导入定义
可以通过 import 来使用来自其他 .proto 文件的定义。
import "myproject/other_protos.proto";
默认情况下,只能从直接导入的 .proto 文件中使用定义。但是,有时你可能需要将 .proto 文件移动到新的位置,这样就可以在旧目录放一个占位的.proto文件,使用import public
将所有导入转发到新位置,而不必直接移动.proto文件并修改所有的地方。
任何导入包含import public语句的proto的人都可以传递地依赖import public依赖项
// new.proto
// All definitions are moved here
-----------------------------------------------
// old.proto
// This is the proto that all clients are importing.
import public "new.proto";
import "other.proto";
-----------------------------------------------
// client.proto
import "old.proto";
// You use definitions from old.proto and new.proto, but not other.proto
protocol 编译器使用命令行-I/–proto_path参数指定的一组目录中搜索导入的文件。如果没有给该命令行参数,则查看调用编译器的目录。
一般来说,应该将 --proto_path 参数设置为项目的根目录并为所有导入使用正确的名称。
// user_business.proto
syntax = "proto3";
option go_package = "protos/pbs";
import "share/user.proto";
//获取角色信息请求
message GetUserRequest {
}
//获取角色信息响应
message GetUserResponse {
User user=1;
}
// user.proto
syntax = "proto3";
option go_package = "protos/pbs";
//用户定义
message User {
string Id=1;
string Name=2;
string Age=3;
}
嵌套类型
可以在其他消息类型中定义和使用消息类型,在 SearchResponse消息中定义Result
:
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
如果要在其父消息类型之外重用此消息类型,请通过_Parent_._Type_
使用:
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;
}
}
}
更新消息类型
如果现有的消息类型不再满足你的所有需要(例如,消息格式需要增加一个字段),但是仍然希望使用用旧格式创建的代码。在不破坏任何现有代码的情况下更新消息类型非常简单,只需遵守以下规则:
- 不要更改任何现有字段的字段编号
- 如果添加新字段,则使用“旧”消息格式的代码序列化的任何消息仍然可以由新生成的代码进行解析。你应该记住这些元素的默认值,以便新代码可以正确地与旧代码生成的消息交互。类似地,由新代码创建的消息也可以由旧代码解析:旧二进制文件在解析时忽略新字段。
- 可以删除字段,前提是在更新的消息类型中不再使用此字段编号。您可能需要重命名字段,或者添加前缀
OBSOLETE_
,或者使用reserved保留字段
编号,以便.proto的未来用户不会意外地重用该编号。 int32
、uint32
、int64
、uint64
和bool
都是兼容的——这意味着你可以在不破坏向前或向后兼容性的情况下将一个字段从这些类型中的一个更改为另一个。- 如果一个数字被解析到一个并不适当的类型中,你会得到与在 C++ 中将数字转换为该类型相同的效果(例如,如果一个64位的数字被读作 int32,它将被截断为32位)
sint32
和sint64
相互兼容,但与其他整数类型不兼容。string
和bytes
是兼容的,只要字节是有效的 UTF-8。- 如果
bytes
包含消息的编码版本,则嵌入的消息与字节兼容。 fixed32
与sfixed32
兼容fixed64
与sfixed64
兼容。- 对于
string
、bytes
和消息字段,optional
字段与repeated
字段兼容。给定重复字段的序列化数据作为输入,如果该字段是基元类型
字段,则期望该字段为optional(可选的)
字段的客户端将获取最后一个输入值;如果该字段是消息类型字段,则合并所有输入元素。请注意,对于数值类型(包括bool
和enum
),这通常是不安全的。数字类型的重复字段可以按压缩格式序列化,当需要可选字段时,将无法正确解析压缩格式。 enum
在格式方面与int32
、uint32
、int64
和uint64
兼容(请注意,如果不适合,值将被截断)。但是要注意,当消息被反序列化时,客户端代码可能会对它们进行不同的处理:例如,未识别的proto3枚举类型将保留在消息中,但在反序列化消息时如何表示这些类型取决于客户端语言。整型字段总是保持它们的值。- 将单个值更改为新的
oneof
成员是安全的,并且二进制兼容。如果确保没有代码一次设置多个字段,那么将多个字段移动到新的oneof
字段中可能是安全的。将任何字段移到现有oneof
字段中都不安全。
未知字段
- 未知字段是格式良好的协议缓冲区序列化数据,表示解析器无法识别的字段。例如,当一个旧二进制代码解析一个带有新字段的新二进制代码发送的数据时,这些新字段在旧二进制代码中成为未知字段。
- 最初,proto3消息在解析过程中总是丢弃未知字段,但在3.5版中,重新引入了未知字段的保留,以匹配proto2的行为。
- 在版本3.5和更高版本中,解析期间保留未知字段,并将其包含在序列化输出中。
any任意类型
- Any 消息类型允许你将消息作为嵌入类型使用,而不需要其 .proto 定义。
- Any包含一个以字节表示的任意序列化消息,以及一个URL,该URL充当该消息的全局唯一标识符并解析为该消息的类型。
- 要使用 Any类型,需要导入google/protobuf/any.proto。
- 如果编译报错,将上述文件下载好复制到自己的proto 目录再编译。
import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}
给定消息类型的默认类型 URL 是type.googleapis.com/_packagename_._messagename_
。
不同的语言实现将支持运行库助手以类型安全的方式打包和解包 Any值。例如在java中,Any类型会有特殊的pack()和unpack()访问器,在C++中会有PackFrom()和UnpackTo()方法。
oneof
如果在平时在一个消息有许多字段,但是最多设置一个字段,我们可以使用oneof 来执行并节省内存。
Oneof 字段类似于常规字段,除了Oneof共享内存的所有字段之外,最多可以同时设置一个字段。设置Oneof 的任何成员都会自动清除所有其他成员。您可以使用case()或WhichOneof()方法检查Oneof 中的哪个值被设置(如果有的话),具体取决于选择的语言。
syntax = "proto3";
option go_package = "protos/pbs";
message SubMessage {
int32 Id=1;
string Age2=2;
}
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
要定义 oneof 字段需要在你的.proto文件中使用oneof关键字并在后面跟上名称,然后将其中一个字段添加到该字段的定义中。你可以添加任何类型的字段,除了map字段和repeated字段。
oneof 特性
- 设置一个字段将自动清除该字段的所有其他成员。如果设置了多个 oneof字段,那么只保留最后的一个值。
SampleMessage message;
message.set_name("name");
CHECK(message.has_name());
message.mutable_sub_message(); // Will clear name field.
CHECK(!message.has_name());
- oneof 不支持repeated。
- 反射作用于oneof的字段。
package main
import (
"fmt"
"grpcdemo/protobuf/any/protos/pbs"
)
func main() {
p:=&pbs.SampleMessage{
TestOneof: &pbs.SampleMessage_Name{Name: "hello"},
}
fmt.Println(p)
fmt.Println(p.GetTestOneof())
p.TestOneof=&pbs.SampleMessage_SubMessage{SubMessage: &pbs.SubMessage{Id: 1}}
fmt.Println(p)
fmt.Println(p.GetTestOneof())
}
兼容性问题
添加或删除其中一个字段时要小心。如果检查 oneof 的值返回 None/NOT_SET,则可能意味着 oneof 尚未设置或已设置为 oneof 的另一个字段。这种情况是无法区分的,因为无法知道未知字段是否是 oneof 成员。
标签重用问题
- 将 optional 可选字段移入或移出 oneof:在序列化和解析 message 后,你可能会丢失一些信息(某些字段将被清除)。但是,你可以安全地将单个字段移动到新的 oneof 中,并且如果已知只有一个字段被设置,则可以移动多个字段。
- 删除 oneof 字段并将其重新添加回去:在序列化和解析 message 后,这可能会清除当前设置的 oneof 字段。
- 拆分或合并 oneof:这与移动常规的 optional 字段有类似的问题。
Maps
map<key_type, value_type> map_field = N;
message Project {
key_type key = 1;
value_type value = 2;
}
// 比如key是string val是Project类型
map<string, Project> projects = 3;
其中key_type
可以是任何整型或字符串类型(因此,除了浮点类型和字节之外的任何标量类型)。注意enum
不是一个有效的key_type
。value_type
可以是除其他map
以外的任何类型。
- 映射字段不能使用repeated关键字。
- 映射值的Wire格式排序和映射迭代排序未定义,因此不能依赖特定顺序的映射项。
为.proto生成文本格式时,映射按键排序。数字键按数字排序。 - 如果为映射字段提供键但没有值,则序列化字段时的行为与语言有关。在C++、Java和Python中,类型的默认值被序列化,而在其他语言中没有任何序列化。
- 生成的map API目前适用于所有proto3支持的语言。
Packages
可以向.proto文件中添加可选的package明符,以防止协议消息类型之间的名称冲突,
可以理解为go的包。
package foo.bar;
message Open { ... }
定义服务
如果要在RPC(Remote Procedure Call,远程过程调用)系统中使用消息类型,可以在.proto文件中定义RPC服务接口。
service SearchService {
rpc Search(SearchRequest) returns (SearchResponse);
}
JSON 映射
proto3 | JSON | JSON example | 描述【译】 |
---|---|---|---|
message | object | {"fooBar": v, | 生成JSON对象。消息字段名被映射到lowerCamelCase并成为JSON对象键。如果指定了json_name 选项,则指定的值将用作键。解析器接受小驼峰命秘法名称(或由json_name 选项指定的名称)和原始proto字段名称。null是所有字段类型的可接受值,并被视为相应字段类型的默认值。 |
enum | string | "FOO_ | 使用proto中指定的枚举值的名称。解析器接受枚举名和整数值。 |
map<K,V> | object | {"k": v, | 所有键都转换为字符串。 |
repeated V | array | [v, | null被接受为空列表[]。 |
bool | true, false | true, | |
string | string | "Hello World!" | |
bytes | base64 string | "YWJjMTIzIT8kKiYoKSctPUB+" | JSON值将是使用带填充的标准base64编码的字符串编码的数据。包含或不包含填充的标准或url安全base64编码都可以接受。 |
int32, fixed32, uint32 | number | 1, | JSON值将是一个十进制数。可接受数字或字符串。 |
int64, fixed64, uint64 | string | "1", | JSON值将是十进制字符串。可接受数字或字符串。 |
float, double | number | 1. | JSON值将是一个数字或特殊字符串值"NaN", "Infinity",和"-Infinity"中的一个。数字或字符串都可以接受。指数符号也被接受。-0被认为等于0。 |
Any | object | {"@type": "url", | 如果`Any`类型包含一个具有特殊JSON映射的值,它将按如下方式转换:{"@type":xxx,"value":yyy} 。否则,该值将转换为JSON对象,并插入“@type”字段以指示实际的数据类型。 |
Timestamp | string | "1972-01-01T10:00:20. | 使用RFC3339,其中生成的输出将始终是**Z**规格化的,并使用0、3、6或9个小数位数。也接受“Z”以外的偏移。 |
Duration | string | "1. | 生成的输出总是包含0、3、6或9个小数位数(取决于所需的精度),后跟“s”后缀。接受任何小数位数(没有小数也可以),只要它们符合纳秒精度,并且需要“s”后缀。 |
Struct | object | { … } | 任何JSON对象。见struct.proto 。 |
Wrapper types | various types | 2, | 包装器在JSON中使用与包装原语类型相同的表示形式,只是在数据转换和传输期间允许并保留null。 |
FieldMask | string | "f. | 见field_mask.proto |
ListValue | array | [foo, | |
Value | value | 任何JSON值。详见google.protobuf.Value | |
NullValue | null | JSON null | |
Empty | object | {} | 空的JSON对象 |
Options
-
.proto文件中的单个声明可以使用许多 选项 进行注释。选项不会更改声明的总体含义,但可能会影响在特定上下文中处理声明的方式。可用选项的完整列表在google/protobuf/descriptor.proto中定义。
-
deprecated选项: 设为true 代表字段被废弃,在新代码不应该被使用,在大多数语言都没有实际的效果
option go_package = "tmp/pb"; // 指定生成的Go代码在你项目中的导入路径
int32 old_field = 6 [deprecated = true];
参考文档
[1] 深入解析protobuf