一、概述
协议缓冲区(protocol-buffers
)是一种与语言无关、与平台无关的可扩展机制,用于序列化结构化数据。
协议缓冲区提供了一种与语言无关、与平台无关、可扩展的机制,用于以向前兼容和向后兼容的方式序列化结构化数据。它类似于JSON,只是它更小更快,并且它生成本地语言绑定。您只需定义数据的结构方式一次,然后就可以使用特殊的生成源代码,使用各种语言轻松地在各种数据流之间读写结构化数据。
协议缓冲区是定义语言(在.proto
文件中创建)、proto编译器生成的用于与数据交互的代码、特定于语言的运行时库以及写入文件(或通过网络连接发送)的数据的序列化格式的组合。
1.1 协议缓冲区能解决什么问题?
协议缓冲区为类型化的结构化数据数据包提供了一种序列化格式,这些数据包的大小可达几兆字节。该格式适用于短暂的网络流量和长期的数据存储。可以使用新信息扩展协议缓冲区,而无需使现有数据无效或要求更新代码。
协议缓冲区是谷歌中最常用的数据格式。它们广泛用于服务器间通信以及磁盘上的数据归档存储。协议缓冲区消息(messages
)和服务(services
)由工程师编写的.proto
文件描述。如下是一个message
示例:
message Person {
optional string name = 1;
optional int32 id = 2;
optional string email = 3;
}
在构建时对.proto
文件调用proto编译器,以生成各种编程语言的代码(本主题后面的跨语言兼容性将介绍),以操作相应的协议缓冲区。每个生成的类都包含针对每个字段的简单访问器
和用于序列化和解析整个结构到原始字节的方法
。下面展示了一个使用这些生成方法的例子:
Person john = Person.newBuilder()
.setId(1234)
.setName("John Doe")
.setEmail("jdoe@example.com")
.build();
output = new FileOutputStream(args[0]);
john.writeTo(output);
由于协议缓冲区在谷歌的各种服务中被广泛使用,并且其中的数据可能会持续一段时间,因此保持向后兼容性是至关重要的。协议缓冲区允许无缝地支持对任何协议缓冲区的更改,包括添加新字段和删除现有字段,而不会中断现有服务。有关此主题的更多信息,请参阅本主题后面的“在不更新代码的情况下更新Proto定义”。
1.2 使用协议缓冲区的好处是什么?
对于需要以与语言无关、与平台无关、可扩展的方式序列化结构化、类记录的、类型化数据的任何情况,协议缓冲区都是理想的。它们最常用于定义通信协议(与gRPC一起)和数据存储。
使用协议缓冲区的一些优点包括:
- 紧凑的数据存储
- 快速解析
- 支持多种编程语言
- 通过自动生成的类优化功能
1.2.1 跨语言的兼容性
用任何受支持的编程语言编写的代码都可以读取相同的消息。您可以让一个平台上的Java程序从一个软件系统捕获数据,根据.proto
定义将其序列化,然后在另一个平台上运行的单独的Python应用程序中从序列化的数据中提取特定的值。
协议缓冲区编译器protoc直接支持以下语言:
-
C++
-
C#
-
Java
-
Kotlin
-
Objective-C
-
PHP
-
Python
-
Ruby
谷歌支持以下语言,但项目源代码驻留在GitHub存储库中。协议编译器为这些语言使用插件:
谷歌不直接支持其他语言,而是由其他GitHub项目支持。这些语言在协议缓冲区的第三方外接程序中涵盖
Rust:
https://github.com/tokio-rs/prost
https://github.com/stepancheg/rust-protobuf/
https://github.com/tafia/quick-protobuf
1.2.2 跨项目的支持
通过在.proto
文件中定义位于特定项目代码库之外的message
类型,您可以跨项目使用协议缓冲区。如果您正在定义的message
类型或枚举预计将在您的直接团队之外广泛使用,则可以将它们放在它们自己的文件中,没有依赖关系。
谷歌中广泛使用的几个原型定义示例是 timestamp.proto and status.proto
1.2.3 更新Proto定义而不更新代码
向后兼容是软件产品的标准,但向前兼容则不太常见。只要在更新.proto
定义时遵循一些简单的实践,旧代码将读取新消息而不会出现问题,忽略任何新添加的字段。对于旧代码,已删除的字段将有其默认值,已删除的重复字段将为空。有关什么是“重复”字段的信息,请参阅本主题后面的协议缓冲区定义语法。
新代码还将透明地读取旧消息。新字段将不会出现在旧消息中;在这些情况下,协议缓冲区提供了一个合理的默认值。
1.2.4 什么时候协议缓冲区不适合?
协议缓冲区不适合所有数据。特别是:
- 协议缓冲区倾向于假设整个消息可以一次加载到内存中,并且不会比对象图大。
对于超过几兆字节的数据,考虑不同的解决方案
;在处理较大的数据时,由于序列化的副本,您可能最终会得到多个数据副本,这可能会导致内存使用的惊人峰值。 当协议缓冲区被序列化时,相同的数据可以有许多不同的二进制序列化。如果不完全解析两条消息,就不能比较它们是否相等
。消息没有被压缩
。尽管消息可以像任何其他文件一样被压缩(zipped )或gzipped ,JPEG和PNG所使用的专用压缩算法将为适当类型的数据生成更小的文件。- 对于许多涉及大型多维浮点数数组的科学和工程应用,协议缓冲区消息在大小和速度上都没有达到最大效率。对于这些应用程序,FITS和类似格式的开销较小。
- 在科学计算中流行的非面向对象语言(如Fortran和IDL)中,协议缓冲区没有得到很好的支持。
- 协议缓冲区消息本身并不自我描述它们的数据,但是它们有一个完全反射的模式,您可以使用它来实现自我描述。也就是说,如果不能访问它对应的
.proto
文件,就不能完全解释它。 - 协议缓冲区不是任何组织的正式标准。这使得它们不适合在具有基于标准构建的法律或其他要求的环境中使用。
1.3 谁使用协议缓冲区?
许多项目使用协议缓冲区,包括:
gRPC
Google Cloud
Envoy Proxy
1.4 协议缓冲区如何工作?
下图展示了如何使用协议缓冲区处理数据。
协议缓冲区生成的代码提供了实用方法,用于从文件和流中检索数据、从数据中提取单个值、检查数据是否存在、将数据序列化回文件或流,以及其他有用的函数。
下面的代码示例向您展示了Java中的此流程示例。如前所述,这是一个.proto
定义:
message Person {
optional string name = 1;
optional int32 id = 2;
optional string email = 3;
}
编译这个.proto
文件会创建一个Builder
类,你可以用它来创建新的实例,如下面的Java代码所示:
Person john = Person.newBuilder()
.setId(1234)
.setName("John Doe")
.setEmail("jdoe@example.com")
.build();
output = new FileOutputStream(args[0]);
john.writeTo(output);
然后,您可以使用协议缓冲区在其他语言(如c++)中创建的方法来反序列化数据:
Person john;
fstream input(argv[1], ios::in | ios::binary);
john.ParseFromIstream(&input);
int id = john.id();
std::string name = john.name();
std::string email = john.email();
1.5 协议缓冲区定义语法
在定义.proto
文件时,可以指定字段是可选的(optional
)或重复的(repeated
)(proto2和proto3)或单数的(singular
)(proto3)。(在proto3中不存在将字段设置为required
的选项,在proto2中也不推荐。有关更多信息,请参见指定字段规则中的“Required is Forever”。)
在设置字段的可选性/可重复性之后,您可以指定数据类型。协议缓冲区支持常见的基本数据类型,如整数、布尔值和浮点数。有关完整列表,请参见标量值类型。
字段也可以是:
message
类型,以便您可以嵌套定义的部分,例如用于重复数据集。- 枚举(
enum
)类型,因此您可以指定一组值进行选择。 oneof
类型,当消息有许多可选字段,同时最多设置一个字段时,可以使用这种类型。- 映射(
map
)类型,用于向定义中添加键-值对。
在proto2中,消息可以允许extensions
在消息本身之外定义字段。例如,protobuf库的内部消息模式允许扩展定制的、特定于用途的选项。
有关可用选项的更多信息,请参阅proto2或proto3的语言指南。
设置可选性和字段类型后,你可以分配字段编号(field number
)。字段编号不能被repurposed 或重用。如果您删除了一个字段,您应该保留它的字段号,以防止某人意外地重用该号码。
1.6 其他数据类型支持
协议缓冲区支持许多标量值类型,包括使用变长编码和固定大小的整数。您还可以通过定义消息来创建自己的复合数据类型,这些消息本身就是可以分配给字段的数据类型。除了简单值类型和复合值类型外,还发布了几种常见类型
。
常见类型
Duration是有符号的固定长度的时间跨度,例如42s。
Timestamp is一个独立于任何时区或日历的时间点,如 2017-01-15T01:30:15.01Z
.
Interval 独立于时区或日历的时间间隔,如 2017-01-15T01:30:15.01Z - 2017-01-16T02:30:15.01Z
.
Date is a whole calendar date, such as 2025-09-19.
DayOfWeek is a day of the week, such as Monday.
TimeOfDay is a time of day, such as 10:42:23.
LatLng 纬度/经度对(latitude/longitude pair
),例如纬度37.386051和经度-122.083855。
Money 与货币类型相对应的金额,如 42 USD。
PostalAddress is a postal address, such as 1600 Amphitheatre Parkway Mountain View, CA 94043 USA
.
Color is a color in the RGBA color space.
Month is a month of the year, such as April.
1.7 协议缓冲开源哲学
协议缓冲区在2008年是开源的,作为一种向谷歌外部的开发人员提供与我们从内部获得的相同好处的方式。我们通过定期更新语言来支持开源社区,因为我们做出了这些改变来支持我们的内部需求。虽然我们接受来自外部开发人员的选择拉请求,但我们不能总是优先考虑不符合谷歌特定需求的功能请求和错误修复。
二、编程指南 (proto 3)
本主题介绍如何在项目中使用协议缓冲区版本3。它包含与语言无关的内容。有关您正在使用的语言的特定信息,请参阅相应的语言文档。
本指南描述了如何使用协议缓冲语言来构建协议缓冲数据,包括.proto
文件语法以及如何从.proto
文件生成数据访问类。它涵盖了协议缓冲区语言的proto3
版本:有关proto2
语法的信息,请参阅proto2语言指南。
这是一个参考指南-关于使用本文档中描述的许多特性的逐步示例,请参阅所选语言的教程。
2.1 定义一个 Message 类型
首先让我们看一个非常简单的例子。假设您想要定义一个搜索请求消息格式,其中每个搜索请求都有一个查询字符串、您感兴趣的结果的特定页面以及每页的若干结果。下面是用于定义message 类型的.proto
文件。
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
文件的第一行指定你使用的是proto3语法:如果你不这样做,协议缓冲区编译器会认为你使用的是proto2语法。这必须是文件的第一个非空、非注释行。
SearchRequest消息定义指定了三个字段(名称/值对),每个字段对应希望包含在此类消息中的数据。每个字段都有一个名称和类型。
2.1.1 指定字段类型
在上面的示例中,所有字段都是标量类型:两个整数(page_number
和result_per_page
)和一个字符串(query
)。但是,您也可以为字段指定复合类型,包括枚举和其他消息类型。
2.1.2 分配字段编号
如您所见,消息定义中的每个字段都有一个唯一的编号(unique number)。这些字段编号用于在消息二进制格式标识字段,一旦使用了消息类型,就不应更改它们。请注意,1到15范围内的字段编号需要一个字节进行编码,包括字段编号和字段类型(您可以在协议缓冲区编码中找到更多关于此的信息)。16到2047范围内的字段号占用两个字节。因此,您应该为非常频繁出现的消息元素保留数字1到15。请记住,为将来可能添加的频繁出现的元素留出一些空间
。
可以指定的最小字段编号为1,最大字段编号为229 - 1,即536,870,911。你也不能使用数字19000到19999 (FieldDescriptor::kFirstReservedNumber到FieldDescriptor::kLastReservedNumber)
,因为它们是为协议缓冲区实现保留的——如果你在.proto
中使用这些保留数字之一,协议缓冲区编译器会报错。类似地,您不能使用任何先前保留的字段编号。
2.1.3 指定字段规则
消息字段规则可以是以下字段之一:
-
singular
格式良好的消息可以有零或一个此字段(但不能超过一个)
。当使用proto3语法时,当没有为给定字段指定其他字段规则时,这是默认的字段规则。您无法确定它是否是从连接中解析的。除非它是默认值,否则它将被序列化到连接。有关此主题的更多信息,请参见字段呈现。 -
optional
与singular
相同,只是您可以检查该值是否显式设置。optional
字段有两种可能的状态:- 字段已设置,并包含从连接中显式设置或解析的值。它将被序列化到网络。
- 该字段未设置,将返回默认值。它不会被序列化到网络。
-
repeated
在格式良好的消息中,此字段类型可以重复0次或多次。重复值的顺序将被保留。 -
map
这是一个键/值对字段类型。有关此字段类型的更多信息,请参见Maps。
在proto3中,标量数字类型的repeated
字段默认使用packed
编码。您可以在协议缓冲区编码中找到关于packed编码的更多信息。
2.1.4 添加更多消息类型
在一个.proto
文件中可以定义多种message
类型。这在定义多个相关消息时非常有用——例如,如果你想定义对应于SearchResponse
消息类型的回复消息格式,你可以将它添加到相同的.proto
中:
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
message SearchResponse {
...
}
2.1.5 添加注释
要向.proto
文件添加注释,请使用C/C+±style //
和/*…* /
语法。
/* 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.
}
2.1.6 保留字段
如果您通过完全删除字段或将其注释掉来更新消息类型,那么将来的用户在对类型进行更新时可以重用字段编号。如果他们后来加载相同的.proto
的旧版本,这可能会导致严重的问题,包括数据损坏、隐私漏洞等等。确保不会发生这种情况的一种方法是指定已删除字段的字段编号(和/或名称,这也可能导致JSON序列化问题)为reserved
。如果将来任何用户尝试使用这些字段标识符,协议缓冲区编译器将报错。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
Note that you can’t mix field names and field numbers in the same reserved
statement.
2.1.7 从你的.proto
生成什么?
当您在.proto
上运行协议缓冲区编译器时,编译器将以您所选择的语言生成代码,您将需要处理在文件中描述的消息类型,包括获取和设置字段值,将消息序列化到输出流,以及从输入流解析消息。
- For C++, 编译器从每个
.proto
生成一个.h
和.cc
文件,并为文件中描述的每种消息类型生成一个类。 - For Java, 编译器生成一个
.java
文件,其中包含针对每种消息类型的类,以及用于创建消息类实例的特殊Builder
类。 - For Kotlin, in addition to the Java generated code, the compiler generates a .kt file for each message type, containing a DSL which can be used to simplify creating message instances.
- Python is a little different — the Python compiler generates a module with a static descriptor of each message type in your .proto, which is then used with a metaclass to create the necessary Python data access class at runtime.
- For Go, 编译器生成一个
.pb.go
为文件中的每个消息类型指定一个类型。 - For Ruby, the compiler generates a .rb file with a Ruby module containing your message types.
- For Objective-C, the compiler generates a pbobjc.h and pbobjc.m file from each .proto, with a class for each message type described in your file.
- For C#, the compiler generates a .cs file from each .proto, with a class for each message type described in your file.
- For Dart, the compiler generates a .pb.dart file with a class for each message type in your file.
您可以根据所选语言的教程(proto3版本即将发布)了解关于为每种语言使用api的更多信息。有关更多API细节,请参阅相关API参考(proto3版本也即将推出)。
2.2 标量值类型
标量消息字段可以有以下类型之一-表中显示了.proto
文件中指定的类型,以及自动生成的类中对应的类型:
.proto Type | Notes | C++ Type | Java/Kotlin Type[1] | Python Type[3] | Go Type | Ruby Type |
---|---|---|---|---|---|---|
double | double | double | float | float64 | Float | |
float | float | float | float | float32 | Float | |
int32 | 使用可变长度编码。编码负数效率很低——如果字段可能有负值,则使用sint32代替。 | int32 | int | int | int32 | Fixnum or Bignum (as required) |
int64 | 使用可变长度编码。编码负数效率很低——如果字段可能有负值,则使用sint64代替。 | int64 | long | int/long[4] | int64 | Bignum |
uint32 | 使用可变长度编码。 | uint32 | int[2] | int/long[4] | uint32 | Fixnum or Bignum (as required) |
uint64 | 使用可变长度编码。 | uint64 | long[2] | int/long[4] | uint64 | Bignum |
sint32 | 使用可变长度编码。有符号的int值。这比常规的int32编码更有效地编码负数。 | int32 | int | int | int32 | Fixnum or Bignum (as required) |
sint64 | 使用可变长度编码。有符号的int值。这比常规的int64编码更有效地编码负数。 | int64 | long | int/long[4] | int64 | Bignum |
fixed32 | 总是四个字节。如果值通常大于228,则比uint32更有效。 | uint32 | int[2] | int/long[4] | uint32 | Fixnum or Bignum (as required) |
fixed64 | 总是8个字节。如果值通常大于256,则比uint64更有效。 | uint64 | long[2] | int/long[4] | uint64 | Bignum |
sfixed32 总是四个字节。 | int32 | int | int | int32 | Fixnum or Bignum (as required) | |
sfixed64 总是8个字节。 | int64 | long | int/long[4] | int64 | Bignum | |
bool | bool | boolean | bool | bool | TrueClass/FalseClass | |
string | 字符串必须包含UTF-8编码或7位ASCII文本,且长度不能超过232。 | string | String | str/unicode[5] | string | String (UTF-8) |
bytes | 可以包含不大于232的任意字节序列。 | string | ByteString | str (Python 2)bytes (Python 3) | []byte | String (ASCII-8BIT) |
在序列化消息时,您可以在协议缓冲区编码中了解有关这些类型是如何编码的更多信息。
[1] Kotlin使用来自Java的相应类型,甚至是无符号类型,以确保在混合Java/Kotlin代码库中的兼容性。
[2] 在Java中,无符号的32位和64位整数使用它们的有符号整数表示,顶部的位简单地存储在符号位中。
[3] 在所有情况下,将值设置为字段将执行类型检查,以确保它是有效的。
[4]64位或无符号32位整数在解码时总是表示为long,但如果在设置字段时给出了int,则可以表示为int。在所有情况下,值必须符合设置时表示的类型。参见[2]。
[5]Python字符串在解码时表示为unicode,但如果给出ASCII字符串则可以表示为str(这可能会更改)。
[6] 整数在64位机器上使用,字符串在32位机器上使用
2.3 默认值
在解析消息时,如果已编码的消息不包含特定的奇异元素,则将已解析对象中的相应字段设置为该字段的默认值。这些默认值是特定于类型的:
- 对于字符串,默认值为空字符串。
- 对于字节,默认值为空字节。
- 对于bool,默认值为false。
- 对于数字类型,默认值为零。
- 对于枚举,默认值是第一个定义的enum值,必须为0。
- 对于消息字段,没有设置该字段。它的确切值取决于语言。有关详细信息,请参阅生成的代码指南。
重复字段的默认值为空(通常是适当语言中的空列表)。
注意,对于标量消息字段,一旦解析了消息,就无法判断字段是否显式设置为默认值(例如布尔值是否设置为false),或者根本没有设置:在定义消息类型时应该记住这一点。例如,如果你不希望某些行为在默认情况下发生,就不要使用一个布尔值,当它被设置为false时,它就会开启某些行为。还要注意,如果将标量消息字段设置为默认值,则该值将不会在连接上序列化。
有关生成的代码中默认值如何工作的详细信息,请参阅所选语言的生成代码指南。
2.4 枚举
在定义消息类型时,可能希望其中一个字段仅具有预定义值列表中的一个。例如,假设您想为每个SearchRequest
添加一个corpus
字段,其中语料库可以是UNIVERSAL
、WEB
、IMAGES
、LOCAL
、NEWS
、PRODUCTS
或VIDEO
。您可以通过向消息定义中添加一个枚举,并为每个可能的值添加一个常量来实现这一点。
在下面的例子中,我们添加了一个名为Corpus
的enum
,包含所有可能的值,以及一个类型为Corpus
的字段:
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 result_per_page = 3;
Corpus corpus = 4;
}
正如您所看到的,Corpus枚举的第一个常量映射到0:每个枚举定义必须包含一个映射到0的常量作为其第一个元素。这是因为:
- 必须有一个0值,这样我们才能使用0作为数值默认值。
- 零值需要是第一个元素,以兼容proto2语义,其中第一个enum值总是默认值。
可以通过将相同的值赋给不同的枚举常量来定义别名。为此,您需要将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; // Uncommenting this line will cause a warning message.
ENAA_FINISHED = 2;
}
枚举数常量必须在32位整数的范围内。由于enum
值在连接上使用可变长编码,所以负值效率较低,因此不建议使用。您可以在消息定义内定义枚举,也可以在消息定义外定义枚举——这些枚举可以在.proto
文件中的任何消息定义中重用,如上面的例子所示。
您还可以使用语法_MessageType_._EnumType_
将一条消息中声明的枚举类型用作另一条消息中字段的类型。
当您在使用枚举的.proto
上运行协议缓冲编译器时,生成的代码将有一个对应的Java、Kotlin或c++枚举,或Python的特殊EnumDescriptor
类,用于在运行时生成的类中创建一组具有整数值的符号常量。
生成的代码可能会受到特定于语言的枚举数的限制(对于一种语言来说只有几千个)。检查您计划使用的语言的限制。
在反序列化期间,消息中将保留无法识别的enum值,不过在消息反序列化时如何表示这些值取决于语言。在c++和Go等支持开放枚举类型(其值超出指定符号范围)的语言中,未知枚举值被简单地存储为其底层整数表示形式。在具有封闭枚举类型的语言(如Java)中,枚举中的大小写用于表示无法识别的值,并且可以使用特殊的访问器访问底层整数。在这两种情况下,如果消息被序列化,未识别的值仍将与消息一起序列化。
有关如何在应用程序中使用消息枚举的更多信息,请参阅所选语言的生成代码指南。
保留值
如果您通过完全删除枚举条目或将其注释掉来更新枚举类型,那么将来的用户在对类型进行更新时可以重用数值。如果他们后来加载相同的.proto
的旧版本,这可能会导致严重的问题,包括数据损坏、隐私漏洞等等。确保不会发生这种情况的一种方法是指定已删除条目的数值(和/或名称,这也可能导致JSON序列化问题)为reserved
。如果将来任何用户尝试使用这些标识符,协议缓冲区编译器将报错。您可以使用max
关键字指定保留数值范围上升到可能的最大值。
enum Foo {
reserved 2, 15, 9 to 11, 40 to max;
reserved "FOO", "BAR";
}
Note that you can’t mix field names and numeric values in the same reserved
statement.
2.5 使用其他消息类型
您可以使用其他消息类型作为字段类型。例如,假设你想在每个SearchResponse
消息中包含Result
消息——为此,你可以在相同的.proto
中定义一个Result
消息类型,然后在SearchResponse
中指定一个Result
类型的字段:
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
2.5.1 导入定义
在上面的示例中,Result
消息类型定义在与SearchResponse
相同的文件中——如果您想用作字段类型的消息类型已经定义在另一个.proto
文件中,该怎么办?
您可以通过导入其他.proto
文件中的定义来使用它们。要导入另一个.proto
的定义,你在文件的顶部添加一个import
语句:
import "myproject/other_protos.proto";
默认情况下,只能使用直接导入的.proto
文件中的定义。然而,有时您可能需要将.proto
文件移动到新的位置。您可以在旧位置放置一个占位符的.proto
文件,使用import public
概念将所有导入转发到新位置,而不是直接移动.proto
文件并在一次更改中更新所有调用点。
注意,Java中没有公共导入功能。
import public
依赖项可以被任何导入包含import public
语句的proto 的代码传递依赖。例如:
// 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
协议编译器使用-I/--proto_path
标志在协议编译器命令行上指定的一组目录中搜索导入的文件。如果没有给出标志,它将查找调用编译器的目录。通常,您应该将--proto_path
标记设置为项目的根,并对所有导入使用完全限定名。
2.5.2 使用proto2消息类型
可以导入proto2消息类型并在proto3消息中使用它们,反之亦然。但是,proto2枚举不能在proto3语法中直接使用(如果导入的proto2消息使用它们是可以的)。
2.6 嵌套类型
你可以在其他消息类型中定义和使用消息类型,如下面的例子所示——这里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;
}
}
}
2.7 更新消息类型
如果现有的消息类型不再满足您的所有需求——例如,您希望消息格式有一个额外的字段——但您仍然希望使用使用旧格式创建的代码,不要担心!更新消息类型非常简单,而不会破坏任何现有代码。只要记住以下规则:
- 不要更改任何现有字段的字段编号。
- 如果添加新字段,使用“旧”消息格式的代码序列化的任何消息仍然可以由新生成的代码解析。您应该记住这些元素的默认值,以便新代码能够正确地与旧代码生成的消息进行交互。类似地,由新代码创建的消息可以由旧代码解析:旧二进制文件在解析时简单地忽略新字段。有关详细信息,请参阅未知字段部分。
- 可以删除字段,只要在更新的消息类型中没有再次使用字段号。您可能希望重命名字段,例如添加前缀“
OBSOLETE_
”,或者字段编号reserved,以便将来使用.proto
的用户不会意外地重用该数字。 int32
,uint32
,int64
,uint64
和bool
都是兼容的——这意味着你可以在不破坏向前或向后兼容性的情况下将一个字段从这些类型中的一种更改为另一种。如果从连接中解析的数字不符合相应的类型,将得到与在c++中将该数字强制转换为该类型相同的效果(例如,如果一个64位数字被读取为int32
,它将被截断为32位)。sint32
andsint64
are compatible with each other but are not compatible with the other integer types.string
andbytes
are compatible as long as the bytes are valid UTF-8.- 如果
bytes
包含消息的编码版本,则嵌入的消息与bytes
兼容。 fixed32
is compatible withsfixed32
, andfixed64
withsfixed64
.- 对于
string
、bytes
和消息字段,optional
与repeat
兼容。给定重复字段的序列化数据作为输入,如果该字段是基本类型字段,则客户端希望该字段是optional
将接受最后一个输入值;如果该字段是消息类型字段,则合并所有输入元素。注意,这对于数字类型(包括bool
和enum
)通常是不安全的。数值类型的重复字段可以以 packed格式序列化,当需要一个optional
字段时,将不会正确地解析该字段。 enum
以wire格式兼容int32
,uint32
,int64
,和uint64
(注意,如果值不符合将被截断)。但是请注意,当消息被反序列化时,客户端代码可能会以不同的方式对待它们:例如,消息中将保留无法识别的proto3enum
类型,但是当消息被反序列化时,这是如何表示的取决于语言。Int字段总是只保留它们的值。- 将单个
optional
字段或扩展更改为一个新oneof
的成员是二进制兼容的,但是对于某些语言(特别是Go),生成的代码的API将以不兼容的方式更改。因此,如AIP-180中所述,谷歌不会在其公共api中进行此类更改。同样要注意源代码兼容性,如果您确定没有代码一次设置多个字段,那么将多个字段移动到一个新的 oneof 字段中可能是安全的。将字段移动到现有的oneof 字段中是不安全的。同样,将单个字段oneof 更改为optional字段或扩展是安全的。
2.8 Unknown Fields
未知字段是格式良好的协议缓冲区序列化数据,表示解析器无法识别的字段。例如,当一个旧二进制文件解析一个带有新字段的新二进制文件发送的数据时,这些新字段在旧二进制文件中成为未知字段。
最初,proto3消息总是在解析过程中丢弃未知字段,但在3.5版中,我们重新引入了保留未知字段以匹配proto2行为。在3.5及更高版本中,未知字段在解析期间保留,并包含在序列化输出中。
2.9 Any
Any消息类型允许您在没有.proto
定义的情况下将消息作为嵌入式类型使用。Any
包含以bytes
表示的任意序列化消息,以及作为该消息类型的全局唯一标识符并解析为该消息类型的URL。要使用Any
类型,需要导入google/protobuf/any.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()
方法:
// Storing an arbitrary message type in Any.
NetworkErrorDetails details = ...;
ErrorStatus status;
status.add_details()->PackFrom(details);
// Reading an arbitrary message from Any.
ErrorStatus status = ...;
for (const google::protobuf::Any& detail : status.details()) {
if (detail.Is<NetworkErrorDetails>()) {
NetworkErrorDetails network_error;
detail.UnpackTo(&network_error);
... processing network_error ...
}
}
目前,用于处理Any
类型的运行时库正在开发中。
如果您已经熟悉了proto2语法,Any可以保存任意的proto3消息,类似于允许扩展的proto2消息
2.10 Oneof
如果你有一个带有多个字段的消息,并且同时最多只能设置一个字段,你可以通过使用oneof
特性强制执行此行为并节省内存。
除了Oneof
中的所有字段共享内存外,其中Oneof
字段就像普通字段一样,并且最多可以同时设置一个字段。设置其中的任何成员将自动清除所有其他成员。您可以使用特殊case()
或WhichOneof()
方法检查其中一个中的哪个值被设置了(如果有的话),这取决于您选择的语言。
注意,如果设置了多个值,最后一个由 proto 中顺序决定的值将覆盖之前的所有值
2.10.1 使用Oneof
要在你的.proto
中定义一个oneof
,你可以使用关键字oneof
后跟你的oneof名字,在本例中是test_oneof
:
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
然后将您的oneof字段添加到oneof定义中。您可以添加任何类型的字段,除了map
字段和repeated
字段。
在生成的代码中,其中一个字段具有与常规字段相同的getter和setter。您还可以获得一个特殊的方法,用于检查其中一个中设置了哪个值(如果有的话)。你可以在相关的API参考中找到更多关于你所选语言的API。
2.10.2 Oneof 特性
- 设置一个
oneof
字段将自动清除该字段的所有其他成员。因此,如果你设置了几个字段,只有你设置的最后一个字段仍然有值。
SampleMessage message;
message.set_name("name");
CHECK_EQ(message.name(), "name");
// Calling mutable_sub_message() will clear the name field and will set
// sub_message to a new instance of SubMessage with none of its fields set.
message.mutable_sub_message();
CHECK(message.name().empty());
- 如果解析器在连接中遇到同一个成员的多个成员,则在解析的消息中只使用最后看到的成员。
- A oneof cannot be
repeated
. - 反射api适用于oneof 字段。
- 如果您将一个oneof字段设置为默认值(例如将一个int32的oneof字段设置为0),则该oneof字段的“case”将被设置,并且该值将在连接上序列化。
- 如果您使用c++,请确保您的代码不会导致内存崩溃。下面的示例代码将崩溃,因为通过调用
set_name()
方法已经删除了sub_message
。
SampleMessage message;
SubMessage* sub_message = message.mutable_sub_message();
message.set_name("name"); // Will delete sub_message
sub_message->set_... // Crashes here
- 同样的,在c++中,如果你
Swap()
两个消息中有一个消息,每个消息将以另一个消息的情况结束:在下面的例子中,msg1
将有一个sub_message
,而msg2
将有一个name
。
SampleMessage msg1;
msg1.set_name("name");
SampleMessage msg2;
msg2.mutable_sub_message();
msg1.swap(&msg2);
CHECK(msg1.has_sub_message());
CHECK_EQ(msg2.name(), "name");
2.10.3 向后兼容性问题
在添加或删除oneof
字段时要小心。如果检查oneof
的值返回None/NOT_SET
,这可能意味着oneof还没有被设置,或者它已经被设置为oneof的不同版本的字段。没有办法区分它们,因为没有办法知道线路上的未知字段是否是oneof
的成员。
2.11 映射 (Maps
)
如果你想创建一个关联映射作为数据定义的一部分,协议缓冲区提供了一个方便的快捷语法:
map<key_type, value_type> map_field = N;
其中key_type
可以是任何整型或字符串类型(因此,任何标量类型,除了浮点类型和bytes
)。注意enum
不是有效的key_type
。value_type
可以是除另一个映射之外的任何类型。
例如,如果你想创建一个项目映射,其中每个项目消息都与一个字符串键相关联,你可以这样定义它:
map<string, Project> projects = 3;
- 映射字段不能
repeated
。 - Wire 格式排序和
map
值的映射迭代排序是未定义的,因此您不能依赖于您的映射项处于特定的顺序。 - 当为
.proto
生成文本格式时,映射按键排序。数字键按数字排序。 - 在从wire 进行解析或合并时,如果存在重复的映射键,则使用最后看到的键。当从文本格式解析映射时,如果有重复的键,解析可能会失败。
- 如果为map字段提供了键但没有值,则该字段序列化时的行为是依赖于语言的。在c++、Java、Kotlin和Python中,该类型的默认值是序列化的,而在其他语言中没有序列化。
生成的map API目前可用于所有proto3支持的语言。您可以在相关API参考中找到有关所选语言的map API的更多信息。
向后兼容
map
语法在wire
上等价于以下内容,因此不支持map的协议缓冲区实现仍然可以处理你的数据:
message MapFieldEntry {
key_type key = 1;
value_type value = 2;
}
repeated MapFieldEntry map_field = N;
任何支持映射的协议缓冲区实现都必须生成并接受可以被上述定义接受的数据。
2.12 包
您可以向.proto
文件添加可选的package
说明符,以防止协议消息类型之间的名称冲突。
package foo.bar;
message Open { ... }
然后你可以在定义你的消息类型的字段时使用包说明符:
message Foo {
...
foo.bar.Open open = 1;
...
}
包说明符影响生成代码的方式取决于你选择的语言:
- 在c++中,生成的类被包装在c++命名空间中。例如,
Open
将在名称空间foo::bar
中。 - 在Java和Kotlin中,这个包被用作Java包,除非您在
.proto
文件中显式地提供了一个option java_package
。 - 在Python中,package指令会被忽略,因为Python模块是根据它们在文件系统中的位置来组织的。
- 在Go中,包被用作Go包的名称,除非你在你的
.proto
文件中显式地提供了一个option go_package
。
包和名称解析
协议缓冲语言中的类型名称解析工作方式类似于c++:首先搜索最里面的作用域,然后是下一个最里面的作用域,依此类类推,每个包都被认为是其父包的“内部”包。前导.
(例如,.foo.bar.Baz
)意味着从最外层的作用域开始。
协议缓冲区编译器通过解析导入的.proto
文件来解析所有类型名。每种语言的代码生成器都知道如何引用该语言中的每种类型,即使它们具有不同的作用域规则。
2.13 服务定义(Defining Services
)
如果您想在RPC(远程过程调用, Remote Procedure Call)系统中使用您的消息类型,您可以在.proto
文件中定义RPC服务接口(RPC service interface),协议缓冲区编译器将用您选择的语言生成服务接口代码和存根。因此,例如,如果你想用一个方法定义一个RPC服务,它接受你的SearchRequest
并返回一个SearchResponse
,你可以在你的.proto
文件中定义它,如下所示:
service SearchService {
rpc Search(SearchRequest) returns (SearchResponse);
}
与协议缓冲区一起使用的最直接的RPC系统是gRPC:一个与语言和平台无关的开源RPC系统,由谷歌开发。gRPC
特别适合使用协议缓冲区,并允许您使用特殊的协议缓冲区编译器插件直接从.proto
文件生成相关的RPC代码。
如果您不想使用gRPC
,也可以在自己的RPC实现中使用协议缓冲区。你可以在Proto2语言指南中找到更多相关信息。
还有一些正在进行的第三方项目为协议缓冲区开发RPC实现。有关我们所知道的项目的链接列表,请参阅第三方插件wiki页面
2.14 JSON Mapping
Proto3支持JSON中的规范编码,使系统之间更容易共享数据。下表中对编码进行了逐个类型的描述。
当将json编码的数据解析到协议缓冲区时,如果一个值缺失或它的值为空,它将被解释为相应的默认值。
当从协议缓冲区生成json编码的输出时,如果一个protobuf字段有默认值并且该字段不支持字段存在,那么默认情况下它将从输出中被省略。实现可以提供选项,在输出中包含具有默认值的字段。
使用optional
关键字定义的 proto3 字段支持字段存在。具有值集且支持字段存在的字段总是在json编码的输出中包含字段值,即使它是默认值。
2.15 Options
.proto
文件中的各个声明可以用许多option
进行注释。选项不会改变声明的整体含义,但可能会影响在特定上下文中处理它的方式。可用选项的完整列表定义在/google/protobuf/descriptor.proto中。
有些选项是文件级选项,这意味着它们应该在顶级范围内编写,而不是在任何消息、枚举或服务定义中。有些选项是消息级选项,这意味着它们应该在消息定义中编写。有些选项是字段级选项,这意味着它们应该在字段定义中编写。选项也可以写在枚举类型、枚举值、字段之一、服务类型和服务方法上;然而,目前没有任何有用的选项。
以下是一些最常用的选项:
java_package
(file option): 要用于生成的Java/Kotlin类的包。如果.proto
文件中没有给出显式的java_package
选项,那么默认情况下将使用proto包(在.proto
文件中使用"package
"关键字指定)。但是,proto包通常不是好的Java包,因为proto包不希望以反向域名开始。如果不生成Java或Kotlin代码,则此选项无效。
option java_package = "com.example.foo";
java_outer_classname
(file option): 希望生成的Java类的类名(以及文件名)。如果.proto
文件中没有显式地指定java_outer_classname
,则类名将通过将.proto
文件名称转换为驼峰格式来构造(因此foo_bar.proto
将变成FooBar.java
)。如果java_multiple_files
选项被禁用,那么为.proto
文件生成所有其他类/enum /等。将在这个外部包装器Java类中生成嵌套类/enum /等。如果不生成Java代码,则此选项无效。
option java_outer_classname = "Ponycopter";
java_multiple_files
(file option):如果为false
,则只会为这个.proto
文件生成一个.Java
文件,以及为顶级消息、服务和枚举生成的所有的Java类/enum /etc 将嵌套在外部类中(请参阅java_outer_classname
)。如果为true
,将为顶级消息、服务和枚举生成单独的Java类/enum /etc生成单独的.Java
文件。为这个.proto
文件生成的包装器Java类将不包含任何嵌套的类/枚举等。这是一个布尔选项,默认为false
。如果不生成Java代码,则此选项无效。
option java_multiple_files = true;
optimize_for
(file option): 可设置为SPEED
,CODE_SIZE
, orLITE_RUNTIME
。这将以以下方式影响c++和Java代码生成器(可能还有第三方生成器):-
SPEED
(default):协议缓冲区编译器将生成用于序列化、解析和对消息类型执行其他常见操作的代码。这段代码是高度优化的。 -
CODE_SIZE
:协议缓冲区编译器将生成最少的类,并依赖于共享的、基于反射的代码来实现序列化、解析和各种其他操作。因此,生成的代码将比使用SPEED
要小得多,但操作将更慢。类仍将实现与SPEED
模式下完全相同的公共API。这种模式在包含大量.proto
文件的应用程序中最有用,而且不需要所有的文件都快得让人盲目。 -
LITE_RUNTIME
:协议缓冲区编译器将生成仅依赖于“lite”运行时库的类(libprotobuf-lite
而不是libprotobuf
)。lite运行时比完整库要小得多(大约小一个数量级),但省略了某些特性,如描述符和反射。这对于运行在受限平台(如手机)上的应用程序特别有用。编译器仍然会生成所有方法的快速实现,就像在SPEED
模式下一样。生成的类将只在每种语言中实现MessageLite
接口,而每种语言只提供完整Message
接口方法的子集。
-
option optimize_for = CODE_SIZE;
cc_enable_arenas
(file option):为c++生成的代码启用 arena allocation。objc_class_prefix
(file option): 设置Objective-C类前缀,该前缀前置于此.proto
生成的所有Objective-C类和枚举。deprecated
(field option): 如果设置为true
,则表示该字段已弃用,不应被新代码使用。在大多数语言中,这是没有实际效果的。在Java中,这变成了@Deprecated
注释。对于c++,每当使用废弃字段时,clang-tidy都会生成警告。将来,其他特定于语言的代码生成器可能会在字段的访问器上生成弃用注释,这反过来会导致在编译试图使用该字段的代码时发出警告。如果该字段没有被任何人使用,并且您希望阻止新用户使用它,请考虑用 reserved 语句替换该字段声明。
int32 old_field = 6 [deprecated = true];
自定义 Options
协议缓冲区还允许您定义和使用自己的选项。这是一个大多数人不需要的高级功能。如果你确实认为你需要创建自己的选项,请参阅Proto2语言指南了解详细信息。注意,使用扩展创建自定义选项,而这只允许用于proto3中的自定义选项。
googleapis
2.16 生成你的类
为了生成Java、Kotlin、Python、c++、Go、Ruby、Objective-C或c#代码,您需要使用.proto
文件中定义的消息类型,您需要在.proto
上运行协议缓冲编译器protoc
。如果您还没有安装编译器,请下载该包并按照 README 中的说明进行操作。对于Go,你还需要为编译器安装一个特殊的代码生成器插件:你可以在GitHub的golang/protobuf存储库中找到这个和安装说明。
协议编译器的调用如下:
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
:指定解析import
指令时查找.proto
文件的目录。如果省略,则使用当前目录
。通过多次传递--proto_path
选项可以指定多个导入目录;他们将按顺序被搜查。-I=_IMPORT_PATH_
可以作为--proto_path
的缩写形式使用。 -
你可以提供一个或多个
output
指令:--cpp_out
在DST_DIR
中生成c++代码。有关更多信息,请参阅c++生成代码参考。--java_out
在DST_DIR
中生成Java代码。有关更多信息,请参阅Java生成代码参考。--go_out
在DST_DIR中生成Go代码。更多信息请参阅Go生成的代码参考。
作为额外的便利,如果DST_DIR
以.zip
或.jar
结尾,编译器将把输出写入一个具有给定名称的zip格式的归档文件。.jar
输出还将按照Java JAR规范的要求给出一个 manifest 文件。注意,如果输出存档已经存在,它将被覆盖;编译器不够聪明,无法将文件添加到现有存档中。
- 您必须提供一个或多个
.proto
文件作为输入。可以同时指定多个.proto
文件。尽管这些文件是相对于当前目录命名的,但每个文件必须位于IMPORT_PATH
中的一个,以便编译器可以确定其规范名称。
2.17 文件位置
最好不要把.proto
文件放在与其他语言源文件相同的目录中。考虑在项目的根包下为.proto
文件创建子包proto
。
位置应该与语言无关
在处理Java代码时,将相关的.proto
文件放在与Java源文件相同的目录中是很方便的。但是,如果任何非java代码使用相同的protos,那么路径前缀就没有意义了。因此,一般来说,将protos放在与语言无关的相关目录中,例如//myteam/mypackage
。
这条规则的例外情况是,很明显protos只能在Java上下文中使用,比如用于测试。