官方文档:https://developers.google.cn/protocol-buffers
文章目录
Protocol Buffers定义消息类型
基本语法
首先让我们看一个非常简单的例子。假设你要定义一个分页搜索请求消息格式,其中每个搜索请求都有一个查询字符串、你感兴趣的特定结果页面以及每页的多个结果。这是.proto
你用来定义消息类型的文件。
syntax = "proto3";
message SearchRequest{
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
-
文件的第一行指定使用的是
proto3
语法:如果不这样做,Protocol Buffers编译器将假定你使用的是proto2
。这必须是文件的第一个非空、非注释行。 -
SearchRequest
消息定义指定了三个字段(名称/值对),每一个字段用于你希望包含在此类消息中的每条数据。每个字段都有一个名称和一个类型。
指定字段类型
在一开始的例子中,所有字段都是标量类型:两个整数(page_number
和result_per_page
)和一个字符串(query
)。但是你也可以为字段指定复合类型,包括枚举和其他消息类型。
分配字段编号
消息定义中的每个字段都有一个唯一的编号(如一开始例子中的1、2、3)。这些字段编号用于在消息二进制格式中标识您的字段,并且在使用您的消息类型后不应更改。
请注意:
- 1 到 15 范围内的字段编号需要一个字节进行编码,包括字段编号和字段类型(您可以在Protocol Buffer Encoding中找到更多相关信息)。
- 16 到 2047 范围内的字段编号占用两个字节。
- 因此,您应该为非常频繁出现的消息元素保留数字 1 到 15。
- 请记住为将来可能添加的频繁出现的元素留出一些空间。
您可以指定的最小字段编号是 1,最大的是 2^29 - 1,即 536,870,911。您也不能使用数字 19000 到 19999 (
FieldDescriptor::kFirstReservedNumber
到FieldDescriptor::kLastReservedNumber
),因为它们是为 Protocol Buffers 实现保留的——如果您在.proto
. 同样,您不能使用任何以前保留的字段编号。
指定字段规则
消息字段可以是以下之一:
- 单数:格式良好的消息可以有零个或一个此字段(但不能超过一个)。这是 proto3 语法的默认字段规则。
repeated
:该字段可以在格式良好的消息中重复任意次数(包括零次)。重复值的顺序将被保留。
在 proto3 中,repeated
标量数值类型的字段packed
默认使用编码。
您可以在Protocol Buffer Encoding中找到有关packed
编码的更多信息。
添加更多消息类型
可以在单个.proto
文件中定义多种消息类型。如果您要定义多个相关消息,这很有用——例如,如果您想定义与您的SearchResponse
消息类型相对应的回复消息格式,您可以将其添加到相同的.proto
:
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
message SearchResponse {
...
}
添加注释
要向.proto
文件添加注释,请使用 C/C++ 样式//
和/* ... */
语法。
/* SearchRequest represents a search query, with pagination options to
* indicate which results to include in the response. */
message SearchRequest {
string query = 1;
int32 page_number = 2; // Which page number do we want?
int32 result_per_page = 3; // Number of results to return per page.
}
保留字段
如果您通过完全删除某个字段或将其注释掉来更新消息类型,未来的用户可以在对类型进行自己的更新时重用该字段编号。如果他们稍后加载相同的旧版本,这可能会导致严重问题.proto
,包括数据损坏、隐私错误等。确保不会发生这种情况的一种方法是指定已删除字段的字段编号(和/或名称,这也可能导致 JSON 序列化问题)为reserved
. 如果将来有任何用户尝试使用这些字段标识符,protocol buffer 编译器会抱怨。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
请注意:您不能在同一reserved
语句中混合字段名称和字段编号。
编译器将生成什么?
当编译一个.proto
文件时,编译器会以您选择的语言生成代码,您需要使用文件中描述的消息类型,包括获取和设置字段值,将消息序列化到输出流,并从输入流中解析您的消息。
- 对于C++,编译器从 each 生成一个
.h
and.cc
文件.proto
,并为文件中描述的每种消息类型提供一个类。 - 对于Java,编译器会
.java
为每种消息类型生成一个包含类的文件,以及Builder
用于创建消息类实例的特殊类。 - 对于Kotlin,除了 Java 生成的代码之外,编译器还会
.kt
为每种消息类型生成一个文件,其中包含可用于简化创建消息实例的 DSL。 - Python有点不同——Python 编译器会生成一个模块*, *
.proto
其中包含您的 . - 对于Go,编译器会为
.pb.go
文件中的每种消息类型生成一个文件类型。 - 对于Ruby,编译器会生成一个
.rb
带有 Ruby 模块的文件,其中包含您的消息类型。 - 对于Objective-C,编译器从 each 生成一个
pbobjc.h
andpbobjc.m
文件.proto
,并为文件中描述的每种消息类型提供一个类。 - 对于C#,编译器会
.cs
从 each 生成一个文件.proto
,其中包含文件中描述的每种消息类型的类。 - 对于Dart,编译器会为
.pb.dart
文件中的每种消息类型生成一个包含类的文件。
Protocol Buffers基本数据类型
标量消息字段可以具有以下类型之一 - 该表显示.proto
文件中指定的类型,以及自动生成的类中的相应类型:
.proto Type | 备注 | C++ Type | JavaType | Python Type | Go Type |
---|---|---|---|---|---|
double | 固定8字节长度 | double | double | float | float64 |
float | 固定4字节长度 | float | float | float | float32 |
int32 | 可变长度编码。对负数进行编码效率低下——如果您的字段可能有负值,请改用 sint32。 | int32 | int | int | int32 |
int64 | 可变长度编码。对负数进行编码效率低下——如果您的字段可能有负值,请改用 sint64。 | int64 | long | int/long | int64 |
uint32 | 可变长度编码,无符号整数 | uint32 | int | int/long | uint32 |
uint64 | 可变长度编码,无符号整数 | uint64 | long | int/long | uint64 |
sint32 | 可变长度编码。带符号的 int 值。这些比常规 int32 更有效地编码负数。 | int32 | int | int | int32 |
sint64 | 可变长度编码。带符号的 int 值。这些比常规 int64 更有效地编码负数 | int64 | long | int/long | int64 |
fixed32 | 总是四个字节。如果值通常大于 2^28 ,则比 uint32 更有效。 | uint32 | int | int/long | uint32 |
fixed64 | 总是八个字节。如果值通常大于 2^56 ,则比 uint64 更有效。 | uint64 | long | int/long | uint64 |
sfixed32 | 总是四个字节。 | int32 | int | int | int32 |
sfixed64 | 总是八个字节。 | int64 | long | int/long | int64 |
bool | bool | boolean | bool | bool | |
string | 字符串必须始终包含 UTF-8 编码或 7 位 ASCII 文本,并且不能超过 2^32。 | string | String | str/unicode | string |
bytes | 可能包含不超过 2^32的任意字节序列。 | string | ByteString | str (Python 2) bytes (Python 3) | []byte |
Protocol Buffers默认值
解析消息时,如果编码的消息不包含特定的奇异元素,则解析对象中的相应字段将设置为该字段的默认值。这些默认值是特定于类型的:
- 对于字符串,默认值为空字符串。
- 对于字节,默认值为空字节。
- 对于布尔值,默认值为 false。
- 对于数字类型,默认值为零。
- 对于enums,默认值是第一个定义的 enum value,它必须是 0。
- 对于消息字段,未设置该字段。它的确切值取决于语言。
重复字段的默认值为空(通常是相应语言的空列表)。
请注意,对于标量消息字段,一旦解析了消息,就无法判断该字段是显式设置为默认值(例如布尔值是否设置为false
)还是根本没有设置:您应该牢记这一点定义消息类型时。例如,false
如果您不希望在默认情况下也发生该行为,则不要在设置为时打开某些行为的布尔值。另请注意,如果标量消息字段设置为其默认值,则该值将不会在线上序列化。
Protocol Buffers枚举类型
在定义消息类型时,您可能希望其字段之一仅具有预定义的值列表之一。例如,假设您要corpus
为每个 添加一个字段,SearchRequest
其中语料库可以是UNIVERSAL
、WEB
、IMAGES
、LOCAL
、NEWS
或。您可以通过在消息定义中添加一个非常简单的方法来做到这一点,并为每个可能的值添加一个常量。PRODUCTS``VIDEO``enum
在下面的示例中,我们添加了一个包含所有可能值的enum
调用Corpus
,以及一个 type 字段Corpus
:
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
enum Corpus{
UNIVERSAL = 0;
WEB= 1 ;
IMAGE= 2 ;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
Corpus corpus = 4;
}
如您所见,Corpus
枚举的第一个常量映射到零:每个枚举定义都必须包含一个映射到零的常量作为其第一个元素。这是因为:
- 必须有一个零值,以便我们可以使用 0 作为数字默认值。
- 零值必须是第一个元素,以便与第一个枚举值始终为默认值的proto2语义兼容。
您可以通过将相同的值分配给不同的枚举常量来定义别名。为此,您需要将allow_alias
选项设置为true
,否则协议编译器将在找到别名时生成错误消息。
enum EnumAllowingAlias {
option allow_alias = true;
EAA_UNSPECIFIED = 0;
EAA_STARTED = 1;
EAA_RUNNING = 1;
EAA_FINISHED = 2;
}
enum EnumNotAllowingAlias {
ENAA_UNSPECIFIED = 0;
ENAA_STARTED = 1;
// ENAA_RUNNING = 1; // 如果取消注释,此行将导致Google内部出现编译错误,外部出现警告消息。
ENAA_FINISHED = 2;
}
枚举器常量必须在 32 位整数范围内。由于enum
值在线路上使用varint 编码,因此负值效率低下,因此不推荐使用。您可以enum
在消息定义中定义 s,如上例所示,也可以在外部定义 - 这些enum
s 可以在.proto
文件中的任何消息定义中重用。您还可以使用enum
在一条消息中声明的类型作为另一条消息中字段的类型,使用语法_MessageType_._EnumType_
.
当您在.proto
使用 的a 上运行协议缓冲区编译器时enum
,生成的代码将有一个对应enum
于 Java、Kotlin 或 C++ 的代码,或者一个EnumDescriptor
用于 Python 的特殊类,用于在运行时创建一组具有整数值的符号常量——生成的类。
警告: 生成的代码可能会受到特定于语言的枚举数限制(一种语言的低千)。请查看您计划使用的语言的限制。
在反序列化期间,无法识别的枚举值将保留在消息中,尽管在反序列化消息时如何表示这取决于语言。在支持具有超出指定符号范围的值的开放枚举类型的语言中,例如 C++ 和 Go,未知的枚举值简单地存储为其底层整数表示。在 Java 等具有封闭枚举类型的语言中,枚举中的 case 用于表示无法识别的值,并且可以使用特殊的访问器访问底层整数。在任何一种情况下,如果消息被序列化,则无法识别的值仍将与消息一起序列化。
Protocol Buffers使用其他消息类型
您可以使用其他消息类型作为字段类型。例如,假设您想Result
在每条SearchResponse
消息中包含消息——为此,您可以Result
在同一条消息中定义一个消息类型,.proto
然后指定一个类型为Result
的字段SearchResponse
:
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
Protocol Buffers嵌套类型
您可以在其他消息类型中定义和使用消息类型,如下例所示 - 这里Result
消息是在消息内部定义的SearchResponse
:
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;
}
}
}