- 1:定义消息类型
- 1.1:分配字段编号
- 1.2:指定字段标签
- 1.3:更多消息类型
- 1.4:注释
- 1.5:删除某个字段
- 1.6:保留字段
- 1.7:.proto生成了什么
- 2:标量值类型
- 2.1:默认值
- 2.2:枚举
- 3:使用其他消息类型
- 3.1:导入
- 4:未知类型
- 4.1:Any
- 4.2:Oneof
- 4.3:map
- 4.4:Packages
- 5:JSON映射
本文介绍如何使用protocol buffer语言来构建protocol buffer数据,包括.proto
文件语法以及如何从.proto
文件生成数据访问类。它涵盖了proto3版本的protocol buffers语言。
定义消息类型
首先让我们看一个非常简单的例子。假设要定义搜索请求消息格式,其中每个搜索请求都有一个查询字符串、感兴趣的特定结果页以及每页的结果数。这是.proto
用来定义消息类型的文件。
<pre tabindex="0"><code class="language-proto" data-lang="proto">syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
}
- 文件第一行指定正在使用proto3语法,如果不指定,编译器将认为你正在使用proto2协议。
- 消息SearchRequest定义3个字段,一个字段对应要包含在此类消息中的每一条数据。每个字段都有一个名称和类型。
在上面的实例中,所有字段都是标量类型:2个整数和一个字符串。还可以为字段指定枚举和复合类型。
分配字段编号
必须为消息定义中的每个字段指定一个介于 1-
536,870,911
之间的数字有以下限制:
- 给定的编号在该消息的所有字段中必须是唯一的。
- 字段号
19,000
to19,999
是为协议缓冲区实现保留的。如果您在消息中使用这些保留字段号之一,协议缓冲区编译器将发出警告。 - 不能使用任何先前保留的字段编号或任何已分配给分机的字段编号。
一旦消息类型被使用,该数字就无法更改,因为它标识消息传输格式中的字段 。“更改”字段编号相当于删除该字段并创建一个具有相同类型但新编号的新字段。 有关如何正确执行此操作的信息,请参阅删除字段。
字段编号决不能重复使用。切勿将字段编号从 保留列表中取出以供新字段定义重用。
应该使用字段编号 1 到 15 作为最常设置的字段。较低的字段数值在有线格式中占用较少的空间。例如,1 到 15 范围内的字段编号需要一个字节进行编码。16 到 2047 范围内的字段编号占用两个字节。
重复使用字段编号的后果
重复使用字段编号会使解码有线格式消息变得不明确。
protobuf 线路格式很精简,并且不提供一种方法来检测使用一种定义编码并使用另一种定义解码的字段。
使用一种定义对字段进行编码,然后使用不同的定义对同一字段进行解码可能会导致:
- 开发人员因调试而损失的时间
- 解析/合并错误(最好的情况)
- 泄露的 PII/SPII
- 数据损坏
字段号重复使用的常见原因:
- 对字段重新编号(有时这样做是为了使字段的编号顺序更美观)。重新编号实际上会删除并重新添加重新编号中涉及的所有字段,从而导致不兼容的有线格式更改。
- 删除字段并且不保留号码以防止将来重复使用。
最大字段为 29 位,而不是更典型的 32 位,因为三个较低位用于有线格式。有关这方面的更多信息。
指定字段标签
消息字段可以是以下之一:
optional
:optional
字段处于两种可能状态之一:- 该字段已设置,并且包含显式设置或从线路解析的值。它将被序列化到线路。
- 该字段未设置,将返回默认值。它不会被序列化到线路。
可以检查该值是否已明确设置。
repeated
:此字段类型可以在格式良好的消息中重复零次或多次。将保留重复值的顺序。map
:这是成对的键/值字段类型。有关此字段类型的更多信息。- 如果未应用显式字段标签,则假定使用默认字段标签,称为“隐式字段存在”。(您不能显式地将字段设置为此状态。)格式正确的消息可以有零个或一个此字段(但不能超过 1 个)。您也无法确定是否从线路中解析了该类型的字段。隐式存在字段将被序列化到线路,除非它是默认值。有关此主题的更多信息。
在proto3中,repeated
标量数字类型的字段packed
默认使用编码。
更多消息类型
可以在单个文件中定义多种消息类型.proto
。如果要定义多个相关消息,这非常有用 - 例如,如果想定义与消息SearchResponse
类型相对应的回复消息格式,可以将其添加到相同的.proto
:
<pre tabindex="0"><code class="language-proto" data-lang="proto">message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
}
message SearchResponse {
...
}
组合消息会导致文件膨胀,在单个文件中定义大量具有不同依赖关系的消息时,也可能导致依赖关系膨胀。建议每个.proto
文件包含尽可能少的消息类型。
注释
要向.proto
文件添加注释,请使用 C/C++ 样式//
和/* ... */
语法。
<pre tabindex="0"><code class="language-proto" data-lang="proto">/* 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 results_per_page = 3; // Number of results to return per page.
}
删除某个字段
如果操作不当,删除字段可能会导致严重问题。
当不再需要某个字段并且所有引用都已从客户端代码中删除时,可以从消息中删除该字段定义。但是,必须 保留已删除的字段号。如果不保留字段编号,开发人员将来可以重复使用该编号。还应该保留字段名称,以允许消息的 JSON 和 TextFormat 编码继续解析。
保留字段
如果通过完全删除字段或注释掉字段来更新消息类型,则未来的开发人员可以在自己更新类型时重复使用字段编号。这可能会导致严重的问题,如重用字段编号的后果中所述 。
为确保不会发生这种情况,需要将已删除的字段编号添加到列表中 reserved
。为了确保仍然可以解析消息的 JSON 和 TextFormat 实例,还将已删除的字段名称添加到列表中reserved
。
如果任何未来的开发人员尝试使用这些保留的字段编号或名称,编译器将会进行提示。
<pre tabindex="0"><code class="language-proto" data-lang="proto">message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
保留字段编号范围包括在内(9 to 11
与 相同9, 10, 11
)。请注意,不能在同一 reserved
语句中混合字段名称和字段编号。
.proto生成了什么
当编译器编译.proto
时,编译器会以选择的语言生成代码,需要使用文件中描述的消息类型,包括获取和设置字段值、将消息序列化为输出流,并从输入流解析消息。
- 对于Java,编译器会生成一个
.java
文件,其中包含每个消息类型的类,以及Builder
用于创建消息类实例的特殊类。 - 对于Kotlin,除了 Java 生成的代码之外,编译器还会
.kt
为每种消息类型生成一个文件,其中包含可用于简化创建消息实例的 DSL。
标量值类型
标量消息字段可以具有以下类型之一 - 该表显示文件中指定的类型.proto
以及自动生成的类中的相应类型:
.proto 类型 | notes | Java/Kotlin类型 |
double | double | |
int32 | 使用可变长度编码。 编码负数效率低下 - 如果字段可能有负值,请改用 sint32 | int |
int64 | 使用可变长度编码。 编码负数效率低下 - 如果字段可能有负值,请改用 sint64 | long |
uint32 | 使用可变长度编码 | int |
uint64 | 使用可变长度编码 | long |
sint32 | 使用可变长度编码。 有符号 int 值。 这些比常规 int32 更有效地编码负数 | int |
sint64 | 使用可变长度编码。 有符号 int 值。 这些比常规 int64 更有效地编码负数 | long |
fixed32 | 始终是四个字节。 如果值通常大于 2^28,则比 uint32 更高效 | int |
fixed64 | 始终为8个字节。 如果值通常大于 2^56,则比 uint64 更高效 | long |
sfixed32 | 始终是四个字节 | int |
sfixed64 | 始终为8个字节 | long |
bool | boolean | |
string | 字符串必须始终包含 UTF-8 编码或 7 位 ASCII 文本,并且长度不能超过 2^32。 | String |
bytes | 可以包含不超过 2^32 的任何任意字节序列。 | ByteString |
默认值
解析消息时,如果编码的消息不包含特定的隐式存在元素,则访问解析对象中的相应字段将返回该字段的默认值。这些默认值是特定于类型的:
- 对于string,默认值为空字符串
- 对于bytes,默认值为空字节
- 对应bool,默认值是false
- 对于数字类型,默认值为0
- 对于枚举类型,默认值是第一个定义的枚举值,该值为0
枚举
当定义消息类型时,可能希望其字段之一仅具有预定义值列表之一。例如,假设您要corpus
为每个 添加一个字段SearchRequest
,其中语料库可以是UNIVERSAL
, WEB
, IMAGES
, LOCAL
, NEWS
,PRODUCTS
或VIDEO
。enum
您可以非常简单地通过在消息定义中添加一个常量来表示每个可能的值来完成此操作。
在下面的示例中,我们添加了一个包含所有可能值的enum
调用Corpus
,以及一个类型为 的字段Corpus
:
<pre tabindex="0"><code class="language-proto" data-lang="proto">enum Corpus {
CORPUS_UNSPECIFIED = 0;
CORPUS_UNIVERSAL = 1;
CORPUS_WEB = 2;
CORPUS_IMAGES = 3;
CORPUS_LOCAL = 4;
CORPUS_NEWS = 5;
CORPUS_PRODUCTS = 6;
CORPUS_VIDEO = 7;
}
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
Corpus corpus = 4;
}
正如所看到的,Corpus
枚举的第一个常量映射到零:每个枚举定义都必须包含一个映射到零作为其第一个元素的常量。这是因为:
- 必须有一个零值,这样我们就可以使用 0 作为数字 默认值
- 零值必须是第一个元素,以便与 proto2语义兼容,其中第一个枚举值是默认值,除非显式指定不同的值。
枚举常量必须在 32 位整数范围内。由于值在线路上enum
使用 varint 编码,因此负值效率低下,因此不建议使用。您可以 enum
在消息定义内定义 s,如前面的示例所示,也可以在外部定义 – 这些enum
可以在文件中的任何消息定义中重用.proto
。您还可以使用enum
在一条消息中声明的类型作为另一条消息中字段的类型,使用语法_MessageType_._EnumType_
。
使用其他消息类型
可以使用其他消息类型作为字段类型。例如,假设您想Result
在每条SearchResponse
消息中包含消息 - 为此,您可以Result
在同一消息中定义消息类型,然后在 中.proto
指定类型字段:Result
SearchResponse
<pre tabindex="0"><code class="language-proto" data-lang="proto">message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
导入
在前面的示例中,Result
消息类型是在同一文件中定义的 SearchResponse
- 如果要用作字段类型的消息类型已在另一个.proto
文件中定义怎么办?
可以通过导入.proto
其他文件来使用它们的定义。要导入另一个定义,可以在文件顶部添加一条导入语句:.proto
import "myproject/other_protos.proto";
嵌套类型
可以在其他消息类型中定义和使用消息类型,如下例所示 - 这里消息Result
是在 SearchResponse
消息中定义的:
<pre tabindex="0"><code class="language-proto" data-lang="proto">message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
如果您想在其父消息类型之外重用此消息类型,请将其称为_Parent_._Type_
:
<pre tabindex="0"><code class="language-proto" data-lang="proto">message SomeOtherMessage {
SearchResponse.Result result = 1;
}
未知类型
未知字段是格式正确的协议缓冲区序列化数据,表示解析器无法识别的字段。例如,当旧二进制文件使用新字段解析新二进制文件发送的数据时,这些新字段将成为旧二进制文件中的未知字段。
最初,proto3 消息在解析过程中总是丢弃未知字段,但在 3.5 版本中,重新引入了保留未知字段以匹配 proto2 行为。在 3.5 及更高版本中,未知字段在解析期间被保留并包含在序列化输出中。
Any
消息Any
类型允许将消息用作嵌入类型,而无需其 .proto 定义。AnAny
包含任意序列化消息 bytes
,以及充当该消息类型的全局唯一标识符并解析为该消息类型的 URL。要使用该Any
类型,需要 导入 google/protobuf/any.proto
.
<pre tabindex="0"><code class="language-proto" data-lang="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()
访问器。
目前,用于处理类型的运行时库Any
正在开发中。
Oneof
如果消息包含多个字段,并且最多同时设置一个字段,则可以使用 oneof 功能强制执行此行为并节省内存。
oneof 字段与常规字段类似,只是所有字段都位于 oneof 共享内存中,并且最多可以同时设置一个字段。设置 oneof 的任何成员都会自动清除所有其他成员。case()
可以使用特殊的or方法检查 oneof 中设置的值(如果有)WhichOneof()
,具体取决于选择的语言。
请注意,如果设置了多个值,则按原型中的顺序确定的最后设置的值将覆盖所有先前的值。
其中一个字段的字段编号在封闭消息中必须是唯一的。
使用Oneof
要在定义 oneof,.proto
可以使用oneof
关键字后跟 oneof 名称,在本例中test_oneof
:
<pre tabindex="0"><code class="language-proto" data-lang="proto">message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
然后,将 oneof 字段添加到 oneof 定义中。可以添加除map
字段和repeated
字段之外的任何类型的字段。如果需要向 oneof 添加重复字段,可以使用包含重复字段的消息。
在生成的代码中,oneof 字段具有与常规字段相同的 getter 和 setter。您还可以获得一种特殊的方法来检查 oneof 中设置了哪个值(如果有)。
map
如果想创建关联映射作为数据定义的一部分,协议缓冲区提供了一种方便的快捷语法:
map<key_type, value_type> map_field = N;
其中key_type
可以是任何整型或字符串类型(因此,可以是 除浮点类型 和 之外的任何标量bytes
类型)。请注意 enum 不是有效的key_type
. 可以value_type
是除其他地图之外的任何类型。
因此,例如,如果想创建一个项目映射,其中每条Project
消息都与一个字符串键关联,可以这样定义它:
<div class="highlight">
<pre tabindex="0"><code class="language-proto" data-lang="proto">map<string, Project> projects = 3;
</div>
map的功能
- map字段不能是
repeated
. - 映射值的有线格式排序和映射迭代排序未定义,因此不能依赖于特定顺序的映射项。
- 为 a 生成文本格式时
.proto
,map按键排序。数字键按数字排序。 - 当从线路解析或合并时,如果存在重复的映射键,则使用最后看到的键。从文本格式解析地图时,如果存在重复的键,解析可能会失败。
- 如果为映射字段提供键但没有提供值,则序列化字段时的行为取决于语言。在 C++、Java、Kotlin 和 Python 中,类型的默认值是序列化的,而在其他语言中,则不会序列化任何内容。
Packages
package
可以向文件添加可选说明符.proto
,以防止协议消息类型之间发生名称冲突。
<pre tabindex="0"><code class="language-proto" data-lang="proto">package foo.bar;
message Open { ... }
然后,可以在定义消息类型的字段时使用包说明符:
<pre tabindex="0"><code class="language-proto" data-lang="proto">message Foo {
...
foo.bar.Open open = 1;
...
}
在Java和Kotlin中,package用作 Java 包,除非option java_package
在.proto
文件中明确提供 。
JSON映射
Proto3 支持 JSON 规范编码,使得系统之间的数据共享更加容易。下表中按类型描述了编码。
将 JSON 编码的数据解析到协议缓冲区时,如果某个值丢失或者其值为null
,则会将其解释为相应的 默认值。
从协议缓冲区生成 JSON 编码的输出时,如果 protobuf 字段具有默认值并且该字段不支持字段存在,则默认情况下将从输出中省略该字段。实现可以提供选项以在输出中包含具有默认值的字段。
使用关键字定义的 proto3 字段optional
支持字段存在。设置了值并且支持字段存在的字段始终在 JSON 编码输出中包含该字段值,即使它是默认值也是如此。
proto3 | JSON | JSON 示例 | 笔记 |
---|---|---|---|
message | object | {"fooBar": v, "g": null, ...} |
生成 JSON 对象。消息字段名称映射为小驼峰命名法并成为 JSON 对象键。如果 json_name 指定了 field 选项,则指定的值将用作键。解析器接受小驼峰名称(或由选项指定的名称json_name )和原始原型字段名称。null 是所有字段类型可接受的值,并被视为相应字段类型的默认值。但是,null 不能用于该 json_name 值。有关原因的更多信息,请参阅 对 json_name 进行更严格的验证。 |
enum | string | "FOO_BAR" |
使用 proto 中指定的枚举值的名称。解析器接受枚举名称和整数值。 |
map<K,V> | object | {"k": v, ...} |
所有键都转换为字符串。 |
repeated V | array | [v, ...] |
null 被接受为空列表[] 。 |
bool | true, false | true, false |
|
string | string | "Hello World!" |
|
bytes | Base64 字符串 | "YWJjMTIzIT8kKiYoKSctPUB+" |
JSON 值将是使用带填充的标准 Base64 编码编码为字符串的数据。接受带/不带填充的标准或 URL 安全的 base64 编码。 |
int32, fixed32, uint32 | number | 1, -10, 0 |
JSON 值将是一个十进制数。接受数字或字符串。 |
int64, fixed64, uint64 | string | "1", "-10" |
JSON 值将是一个十进制字符串。接受数字或字符串。 |
float, double | number | 1.1, -10.0, 0, "NaN", "Infinity" |
JSON 值将是一个数字或特殊字符串值“NaN”、“Infinity”和“-Infinity”之一。接受数字或字符串。指数表示法也被接受。-0 被认为等同于 0。 |
Any | object |
{"@type": "url", "f": v, ... } |
如果Any 包含具有特殊 JSON 映射的值,它将按如下方式转换:{"@type": xxx, "value": yyy} 。否则,该值将被转换为 JSON 对象,并且该"@type" 字段将被插入以指示实际的数据类型。 |
Timestamp | string | "1972-01-01T10:00:20.021Z" |
使用 RFC 3339,其中生成的输出将始终进行 Z 归一化并使用 0、3、6 或 9 个小数位。除“Z”之外的偏移量也被接受。 |
Duration | string | "1.000340012s", "1s" |
生成的输出始终包含 0、3、6 或 9 个小数位,具体取决于所需的精度,后跟后缀“s”。接受任何小数位(也可以不接受),只要它们符合纳秒精度,并且需要后缀“s”。 |
Struct | object |
{ ... } |
任何 JSON 对象。见struct.proto 。 |
包装类型 | 各种类型 | 2, "2", "foo", true, "true", null, 0, ... |
包装器在 JSON 中使用与包装的基元类型相同的表示形式,只不过null 在数据转换和传输期间允许并保留这种表示形式。 |
FieldMask | string | "f.fooBar,h" |
见field_mask.proto 。 |
ListValue | array | [foo, bar, ...] |
|
Value | Value | 任何 JSON 值。检查 google.protobuf.Value 了解详细信息。 | |
NullValue | null | JSON 空 | |
Empty | object | {} |
空 JSON 对象 |