Protocol Buffer是Google的语言中立的,平台中立的,可扩展机制的,用于序列化结构化数据 - 对比XML,但更小,更快,更简单。您可以定义数据的结构化,然后可以使用特殊生成的源代码轻松地在各种数据流中使用各种语言编写和读取结构化数据。
1. 定义消息类型
先来看一个非常简单的例子。假设你想定义一个“搜索请求”的消息格式,每一个请求含有一个查询字符串、你感兴趣的查询结果所在的页数,以及每一页多少条查询结果。可以采用如下的方式来定义消息类型的.proto文件了:
1 | syntax = "proto3"; |
- 该文件的第一行指定您正在使用
proto3
语法:如果您不这样做,protobuf 编译器将假定您正在使用proto2。这必须是文件的第一个非空的非注释行。 - 所述
SearchRequest
消息定义指定了三个字段(名称/值对),一个用于要在此类型的消息中包含的每个数据片段。每个字段都有一个名称和类型。
1.1 指定字段类型
在上面的示例中,所有字段都是标量类型:两个整数(page_number
和result_per_page
)和一个字符串(query
)。但是,您还可以为字段指定合成类型,包括枚举和其他消息类型。
1.2 分配标识号
正如上述文件格式,在消息定义中,每个字段都有唯一的一个数字标识符。这些标识符是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变。注:[1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 [1,15]之内的标识号。切记:要为将来有可能添加的、频繁出现的标识号预留一些标识号。
最小的标识号可以从1开始,最大到2^29 - 1, or 536,870,911。不可以使用其中的[19000-19999]的标识号, Protobuf协议实现中对这些进行了预留。如果非要在.proto文件中使用这些预留标识号,编译时就会报警。
1.3 指定字段规则
消息字段可以是以下之一:
- 单数:格式良好的消息可以包含该字段中的零个或一个(但不超过一个)。
repeated
:此字段可以在格式良好的消息中重复任意次数(包括零)。将保留重复值的顺序。在proto3中,repeated
数字类型的字段默认使用packed
编码。packed
您可以在协议缓冲区编码中找到有关编码的更多信息。
限定修饰符包含 required\optional\repeated
Required: 表示是一个必须字段,必须相对于发送方,在发送消息之前必须设置该字段的值,对于接收方,必须能够识别该字段的意思。发送之前没有设置required字段或者无法识别required字段都会引发编解码异常,导致消息被丢弃。
Optional:表示是一个可选字段,可选对于发送方,在发送消息时,可以有选择性的设置或者不设置该字段的值。对于接收方,如果能够识别可选字段就进行相应的处理,如果无法识别,则忽略该字段,消息中的其它字段正常处理。—因为optional字段的特性,很多接口在升级版本中都把后来添加的字段都统一的设置为optional字段,这样老的版本无需升级程序也可以正常的与新的软件进行通信,只不过新的字段无法识别而已,因为并不是每个节点都需要新的功能,因此可以做到按需升级和平滑过渡。
Repeated:表示该字段可以包含0~N个元素。其特性和optional一样,但是每一次可以包含多个值。可以看作是在传递一个数组的值。
1.4 添加更多消息类型
可以在单个.proto
文件中定义多种消息类型。如果要定义多个相关消息,这很有用
例如,如果要定义与SearchResponse
消息类型对应的回复消息格式,可以将其添加到相同的消息.proto
:
1 | message SearchRequest { |
1.5 添加注释
要为.proto
文件添加注释,请使用C / C ++ - 样式//
和/* ... */
语法。
1 | /* SearchRequest表示搜索查询,带有分页选项 |
1.6 保留字段
如果通过完全删除字段或将其注释来更新消息类型,则未来用户可以在对类型进行自己的更新时重用字段编号。
如果以后加载相同的旧版本,这可能会导致严重问题.proto
,包括数据损坏,隐私错误等。确保不会发生这种情况的一种方法是指定已删除字段的字段编号(和/或名称,这也可能导致JSON序列化问题)reserved
。如果将来的任何用户尝试使用这些字段标识符,协议缓冲编译器将会抱怨。
1 | message Foo { |
请注意,您不能在同一reserved
语句中混合字段名称和字段编号。
1.7 你的生成是什么.proto
?
当您在a上运行协议缓冲区编译器时.proto
,编译器会生成您所选语言的代码,您需要使用您在文件中描述的消息类型,包括获取和设置字段值,将消息序列化为输出流,并从输入流解析您的消息。
- 对于**C ++**,编译器会从每个文件生成一个
.h
和一个.cc
文件.proto
,并为您文件中描述的每种消息类型提供一个类。 - 对于Java,编译器生成一个
.java
文件,其中包含每种消息类型的类,以及Builder
用于创建消息类实例的特殊类。 - Python有点不同 - Python编译器生成一个模块,其中包含每个消息类型的静态描述符,
.proto
然后与元类一起使用,以在运行时创建必要的Python数据访问类。 - 对于Go,编译器会为
.pb.go
文件中的每种消息类型生成一个类型的文件。 - 对于Ruby,编译器生成一个
.rb
包含消息类型的Ruby模块的文件。 - 对于Objective-C,编译器从每个文件生成一个
pbobjc.h
和一个pbobjc.m
文件.proto
,其中包含文件中描述的每种消息类型的类。 - 对于C#,编译器会
.cs
从每个文件生成一个文件.proto
,其中包含文件中描述的每种消息类型的类。
您可以按照所选语言的教程(即将推出的proto3版本)了解有关为每种语言使用API的更多信息。有关更多API详细信息,请参阅相关API参考(proto3版本即将推出)。
2. 标量值类型
标量消息字段可以具有以下类型之一 - 该表显示.proto
文件中指定的类型,以及自动生成的类中的相应类型:
.proto type | notes | C ++ type | Java type | Python type [2] | Go type | Ruby type | C# type | PHP type |
---|---|---|---|---|---|---|---|---|
double | double | double | float | float64 | float | double | float | |
float | float | float | float | FLOAT32 | float | float | float | |
INT32 | 使用可变长度编码。编码负数的效率低 - 如果您的字段可能有负值,请改用sint32。 | INT32 | INT | INT | INT32 | Fixnum or Bignum (as needed) | INT | Integer |
Int64 | 使用可变长度编码。编码负数的效率低 - 如果您的字段可能有负值,请改用sint64。 | Int64 | long | int / long [3] | Int64 | TWINS | long | Integer/string[5] |
UINT32 | 使用可变长度编码。 | UINT32 | int [1] | int / long [3] | UINT32 | Fixnum or Bignum (as needed) | UINT | Integer |
UINT64 | 使用可变长度编码。 | UINT64 | Long [1] | int / long [3] | UINT64 | TWINS | ULONG | Integer/string[5] |
SINT32 | 使用可变长度编码。签名的int值。这些比常规int32更有效地编码负数。 | INT32 | INT | INT | INT32 | Fixnum or Bignum (as needed) | INT | Integer |
sint64 | 使用可变长度编码。签名的int值。这些比常规int64更有效地编码负数。 | Int64 | long | int / long [3] | Int64 | TWINS | long | Integer/string[5] |
fixed32 | 总是四个字节。如果值通常大于2 28,则比uint32更有效。 | UINT32 | int [1] | int / long [3] | UINT32 | Fixnum or Bignum (as needed) | UINT | Integer |
fixed64 | 总是八个字节。如果值通常大于2 56,则比uint64更有效。 | UINT64 | Long [1] | int / long [3] | UINT64 | TWINS | ULONG | Integer/string[5] |
sfixed32 | 总是四个字节。 | INT32 | INT | INT | INT32 | Fixnum or Bignum (as needed) | INT | Integer |
sfixed64 | 总是八个字节。 | Int64 | long | int / long [3] | Int64 | TWINS | long | Integer/string[5] |
Boolean | Boolean | Boolean | Boolean | Boolean | TrueClass / FalseClass | Boolean | Boolean | |
string | 字符串必须始终包含UTF-8编码或7位ASCII文本。 | string | string | str / unicode[4] | string | String (UTF-8) | string | string |
byte | 可以包含任意字节序列。 | string | Byte string | Strait | []byte | String (ASCII-8BIT) | Byte string | string |
在协议缓冲区编码中序列化消息时,您可以找到有关如何编码这些类型的更多信息。
[1]在Java中,无符号的32位和64位整数使用它们的带符号对应表示,最高位只是存储在符号位中。
[2]在所有情况下,将值设置为字段将执行类型检查以确保其有效。
[3] 64位或无符号32位整数在解码时始终表示为long,但如果在设置字段时给出int,则可以为int。在所有情况下,该值必须适合设置时表示的类型。见[2]。
[4] Python字符串在解码时表示为unicode,但如果给出了ASCII字符串,则可以是str(这可能会发生变化)。
[5] Integer用于64位计算机,字符串用于32位计算机。
3. 默认值
解析消息时,如果编码消息不包含特定的单数元素,则解析对象中的相应字段将设置为该字段的默认值。这些默认值是特定于类型的:
- 对于字符串,默认值为空字符串。
- 对于字节,默认值为空字节。
- 对于bools,默认值为false。
- 对于数字类型,默认值为零。
- 对于枚举,默认值是第一个定义的枚举值,该值必须为0。
- 对于消息字段,未设置该字段。它的确切值取决于语言。有关详细信息, 请参阅生成的代码指
重复字段的默认值为空(通常是相应语言的空列表)。
请注意,对于标量消息字段,一旦解析了消息,就无法确定字段是否显式设置为默认值(例如,是否设置了布尔值false
)或者根本没有设置:您应该记住这一点在定义消息类型时。例如,false
如果您不希望默认情况下也发生这种行为,那么在设置为时,没有一个布尔值可以启用某些行为。还要注意的是,如果一个标消息字段被设置为默认值,该值将不会在电线上连载。
有关默认值如何在生成的代码中工作的更多详细信息,请参阅所选语言的生成代码指南。
4. 枚举
在定义消息类型时,您可能希望其中一个字段只有一个预定义的值列表。例如,假设你想添加一个 corpus
字段每个SearchRequest
,其中语料库可以 UNIVERSAL
,WEB
,IMAGES
,LOCAL
,NEWS
,PRODUCTS
或VIDEO
。您可以非常简单地通过enum
为每个可能的值添加一个常量来定义消息定义。
在下面的示例中,我们添加了一个带有所有可能值的enum
调用Corpus
,以及一个类型的字段Corpus
:
1 | message SearchRequest { |
如您所见,Corpus
枚举的第一个常量映射为零:每个枚举定义必须包含一个映射到零的常量作为其第一个元素。这是因为:
您可以通过为不同的枚举常量指定相同的值来定义别名。为此,您需要将allow_alias
选项设置为true
,否则协议编译器将在找到别名时生成错误消息。
1 | enum EnumAllowingAlias { |
枚举器常量必须在32位整数范围内。由于enum
值在线上使用varint编码,因此负值效率低,因此不建议使用。您可以enum
在消息定义中定义s,如上例所示,enum
也可以在外部定义 - 这些可以在.proto
文件的任何消息定义中重用。您还可以使用enum
语法将一个消息中声明的类型用作另一个消息中的字段类型。 *MessageType*.*EnumType*
当你在.proto
使用a的协议缓冲编译器上运行时enum
,生成的代码将具有enum
Java或C ++ 的相应代码,这EnumDescriptor
是Python的一个特殊类,用于在运行时生成的类中创建一组带有整数值的符号常量。
在反序列化期间,将在消息中保留无法识别的枚举值,但是当反序列化消息时,如何表示这种值取决于语言。在支持具有超出指定符号范围的值的开放枚举类型的语言中,例如C ++和Go,未知的枚举值仅作为其基础整数表示存储。在具有封闭枚举类型(如Java)的语言中,枚举中的大小写用于表示无法识别的值,并且可以使用特殊访问器访问基础整数。在任何一种情况下,如果消息被序列化,则仍然会使用消息序列化无法识别的值。
有关如何enum
在应用程序中使用消息的详细信息,请参阅所选语言的生成代码指南。
4.1 保留值
如果通过完全删除枚举条目或将其注释掉来更新枚举类型,则未来用户可以在对类型进行自己的更新时重用该数值。如果以后加载相同的旧版本,这可能会导致严重问题.proto
,包括数据损坏,隐私错误等。确保不会发生这种情况的一种方法是指定已删除条目的数值(和/或名称,这也可能导致JSON序列化问题)reserved
。如果将来的任何用户尝试使用这些标识符,协议缓冲编译器将会抱怨。您可以使用max
关键字指定保留的数值范围达到最大可能值。
1 | enum Foo { |
请注意,您不能在同一reserved
语句中混合字段名称和数值。
5. 使用其他消息类型
您可以使用其他消息类型作为字段类型。例如,假设你想包括Result
每个消息的SearchResponse
消息-要做到这一点,你可以定义一个Result
在同一个消息类型.proto
,然后指定类型的字段Result
中SearchResponse
:
1 | message SearchResponse { |
5.1 导入定义
在上面的示例中,Result
消息类型在同一文件中定义SearchResponse
- 如果要用作字段类型的消息类型已在另一个.proto
文件中定义,该怎么办?
您可以.proto
通过导入来使用其他文件中的定义。要导入其他.proto
人的定义,请在文件顶部添加import语句:
1 | import "myproject/other_protos.proto"; |
默认情况下,您只能使用直接导入.proto
文件中的定义。但是,有时您可能需要将.proto
文件移动到新位置。.proto
现在,您可以.proto
在旧位置放置一个虚拟文件,以使用该import public
概念将所有导入转发到新位置,而不是直接移动文件并在一次更改中更新所有调用站点。import public
任何导入包含该import public
语句的proto的人都可以传递依赖关系。例如:
1 | // new.proto |
协议编译器使用-I
/ --proto_path
flag 在协议编译器命令行中指定的一组目录中搜索导入的文件 。如果没有给出标志,它将查找调用编译器的目录。通常,您应该将--proto_path
标志设置为项目的根目录,并对所有导入使用完全限定名称。
5.2 使用proto2消息类型
可以导入proto2消息类型并在proto3消息中使用它们,反之亦然。但是,proto2枚举不能直接用于proto3语法(如果导入的proto2消息使用它们就可以了)。
6. 嵌套类型
您可以在其他消息类型中定义和使用消息类型,如下例所示 - 此处Result
消息在消息中定义SearchResponse
:
1 | message SearchResponse { |
如果要在其父消息类型之外重用此消息类型,请将其称为: *Parent*.*Type*
1 | message SomeOtherMessage { |
您可以根据需要深入嵌套消息:
1 | message Outer { // Level 0 |