定义消息类型
首先,创建一个*.proto
文件
//声明使用proto3语法;如果没有指定这个,编译器会使用proto2。这个指定语法必须是文件的非空非注释的第一行
syntax = "proto3";
//我们定义一个SearchRequest消息格式有3个字段
message SearchRequest{
//字符串(string)类型的 query 字段
string query = 1;
//32位整型(int32)类型的 page_number 字段
int32 page_number = 2;
//32位整型(int32)类型的result_per_page 字段
int32 result_per_page = 3;
}
复制代码
指定字段类型
在上面的例子中,所有字段都是标量类型(scalar types
)
- 两个整型变量:
page_number
和result_per_page
- 一个字符串变量:
query
当然还可以为字段指定其他和成类型,包括枚举(enumerations
)或其他消息类型
分配标识号
在消息定义的时候每个字段都有一个唯一的编号。这些字段编号用于在消息二进制格式中标识字段,音消息类型被使用,就不应更改。
请注意:1-15
范围内的字段编号占一个字节进行编码,包括字段编号和字段类型
16-2047
范围内的字段编号占用两个字节
因此建议应该更多的使用1-15
可以指定最小字段编号为1
,最大字段编号为2^29 - 1
或 536,870,911
不能使用19000-19999
(FieldDescriptor::kFirstReservedNumber
到FieldDescriptor::kLastReservedNumber
)的标识号,protobuf
协议实现对这些进行了预留,如果非要使用,编译时就会报警,同样,也不能使用任何以前保留(reserved
)的字段编号。
指定字段规则
所指定的消息字段修饰符必须是一下几种:
sigular
:一个格式良好的消息应该有0个或者1个这种字段(但不能超过1个)。这是proto3
语法的默认字段规则。repeated
:在一个格式良好的消息中,这种字段可以重复任意多次(包括0次)。重复的值的顺序会被保留。
在proto3
中,repeated
的标量域默认情况下使用packed
。
添加更多的消息类型
在一个.proto
文件中可以定义多个消息类型
syntax = "proto3";
message SearchRequest{
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
message SearchResponse{
string result = 1;
}
复制代码
添加注释
向.proto
文件中添加注释,可以使用C/C++/Java
风格的(//
)和(/** ... */
)语法格式
//声明使用proto3语法;如果没有指定这个,编译器会使用proto2。这个指定语法必须是文件的非空非注释的第一行
syntax = "proto3";
//我们定义一个SearchRequest消息格式有3个字段
message SearchRequest{
//字符串(string)类型的 query 字段
string query = 1;
//32位整型(int32)类型的 page_number 字段
int32 page_number = 2;
//32位整型(int32)类型的result_per_page 字段
int32 result_per_page = 3;
}
message SearchResponse{
/**
字符串(string)类型的 result 字段
*/
string result = 1;
}
复制代码
保留字段(Reserved
)
如果通过完全删除字段或将其注释掉来更新消息类型,那么用户可以在对该类型进行自己的更新时可能会重用字段号。如果他们以后加载相同.proto
的旧版本,这可能会导致严重的问题,包括数据损坏、隐私错误等。为了确保不会发生这种情况,可以指定保留已删除字段的字段编号(或是名称,这也可能导致JSON
序列化问题)。用户试图使用这些字段标识符,编译器将会报错。
message Foo {
reserved 2, 15, 9 to 11; //保留字段号
reserved "foo", "bar"; //保留字段名
string a = 2; // 编译报错,因为 2 已经被标为保留字段
string foo = 1; //编译报错,因为 "foo" 已经被标位保留字段
}
复制代码
注意,不能在同一个reserved
语句中同时使用字段名和字段号。
.proto
文件生成了什么?
当用protocol buffer
编译器来运行.proto
文件时,编译器将生成所选择语言的代码,这些代码可以操作在.proto
文件中定义的消息类型,包括获取、设置字段值,将消息序列化到一个输出流中,以及从一个输入流中解析消息。
C++
:编译器会为每个.proto
文件生成一个.h
文件和一个.cc
文件,.proto
文件中的每一个消息有一个对应的类。Java
:编译器为每个消息类型生成一个.java
的文件,以及Builder
用于创建消息类实例的特殊类。Kotlin
:除了Java
生成的代码之外,编译器还未每个消息生成一个.kt
文件,其中包含可用于简化创建消息实例的DSL
。Python
:有点不太一样,Python
编译器为.proto
文件中的每个消息类型生成一个含有静态描述符的模块,该模块与一个元类(metaclass
)在运行时(runtime
)被用来创建所需的Python
数据访问类。go
:编译器会位每个消息类型生成了一个.pd.go
文件。Ruby
:编译器会为每个消息类型生成了一个.rb
文件。Objective-C
:编译器会为每个消息类型生成了一个pbobjc.h
文件和pbobjcm
文件,.proto
文件中的每一个消息有一个对应的类。C#
:编译器会为每个消息类型生成了一个.cs
文件,.proto
文件中的每一个消息有一个对应的类。Dart
:编译器会为.pb.dart
文件中的每种消息类型生成一个带有类的文件。
标量类型(scalar types
)
.proto Type | Notes | C++ Type | Java/Kotlin Type[1] | Python Type[3] | Go Type | Ruby Type | C# Type | PHP Type | Dart Type | |
---|---|---|---|---|---|---|---|---|---|---|
double | double | double | float | float64 | Float | double | float | double | ||
float | float | float | float | float32 | Float | float | float | double | ||
int32 | 使用可变长度编码。 编码负数效率低下 – 如果您的字段可能具有负值,请改用 sint32。 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead. | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int |
int64 | 使用可变长度编码。 编码负数效率低下 – 如果您的字段可能具有负值,请改用 sint64。 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead. | int64 | long | int/long[4] | int64 | Bignum | long | integer/string[6] | Int64 |
uint32 | 使用可变长度编码。 | Uses variable-length encoding. | uint32 | int[2] | int/long[4] | uint32 | Fixnum or Bignum (as required) | uint | integer | int |
uint64 | 使用可变长度编码。 | Uses variable-length encoding. | uint64 | long[2] | int/long[4] | uint64 | Bignum | ulong | integer/string[6] | Int64 |
sint32 | 使用可变长度编码。 有符号整数值。 这些比常规 int32 更有效地编码负数。 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s. | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int |
sint64 | 使用可变长度编码。 有符号整数值。 这些比常规 int64 更有效地编码负数。 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s. | int64 | long | int/long[4] | int64 | Bignum | long | integer/string[6] | Int64 |
fixed32 | 总是四个字节。 如果值通常大于 228,则比 uint32 更有效。 | Always four bytes. More efficient than uint32 if values are often greater than 228. | uint32 | int[2] | int/long[4] | uint32 | Fixnum or Bignum (as required) | uint | integer | int |
fixed64 | 总是八个字节。 如果值通常大于 256,则比 uint64 更有效。 | Always eight bytes. More efficient than uint64 if values are often greater than 256. | uint64 | long[2] | int/long[4] | uint64 | Bignum | ulong | integer/string[6] | Int64 |
sfixed32 | 总是四个字节。 | Always four bytes. | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int |
sfixed64 | 总是八个字节。 | Always eight bytes. | int64 | long | int/long[4] | int64 | Bignum | long | integer/string[6] | Int64 |
bool | bool | boolean | bool | bool | TrueClass/FalseClass | bool | boolean | bool | ||
string | 字符串必须始终包含 UTF-8 编码或 7 位 ASCII 文本,并且长度不能超过 232。 | A string must always contain UTF-8 encoded or 7-bit ASCII text, and cannot be longer than 232. | string | String | str/unicode[5] | string | String (UTF-8) | string | string | String |
bytes | 可以包含不超过 232 的任意字节序列。 | May contain any arbitrary sequence of bytes no longer than 232. | string | ByteString | str | []byte | String (ASCII-8BIT) | ByteString | string | List |
- [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
位机器上使用字符串。
默认值
在解析消息时,如果编码的消息不包含特定的singular
元素,则解析对象中的相应字段将设置为该字段的默认值。这些默认值与类型有关:
- 对于
string
,默认值为空字符串。 - 对于
bytes
,默认值是空bytes
。 - 对于
bool
,默认值为false
。 - 对于数字类型,默认值为
0
。 - 对于枚举,默认值为第一个定义的枚举变量,其值必须为
0
。 - 对于消息字段,未设置。它的值取决于语言。
注意:
对于标量消息字段来说,一旦消息被解析,就无法判断该字段是真实被设为默认值(例如bool
变量被设为false
)还是就没有设置:在定义消息类型时需要牢记这一点。例如,如果你不想在默认情况向执行某种行为,那么就不要用boole
被设置为false
来切换这些行为。同时,如果一个标量消息字段被设为它的默认值,那么改值在传输时将不会被序列化。
枚举
当你在定义消息类型时,你可能想让它的字段只是用预定义列表中的一个值。
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
enum Role {
RESIDENT = 0;
CLEANER = 1;
ADMIN = 2;
OWNER = 3;
SU = 4;
}
Role role = 4;
}
复制代码
- 必须有一个零值,这样我们可以把
0
作为数字的默认值 - 零值小时第一个元素,以便于
proto2
语义兼容,其实第一个枚举值始终是默认值。
还可以对枚举常量设置别名。需要设置option allow_alias
为 true
, 否则 protocol
编译器会产生错误信息。
enum Role {
option allow_alias = true;
RESIDENT = 0;
HOUSEHOLDER = 0;
ENFORCER = 1;
GATHERER = 2;
PC_ADMIN = 3;
}
enum UserRole {
RESIDENT = 0;
HOUSEHOLDER = 0;
ENFORCER = 1;
GATHERER = 2;
PC_ADMIN = 3;
}
复制代码
- 枚举常量必须在
32
位整型值的范围内。因为enum
值是使用可变编码方式的,对负数不够高效,因此不推荐在enum
中使用负数。 - 你可以在一个消息定义的内部定义枚举,你也可以在消息的外部定义枚举类型,这样这些枚举值可以在同一
.proto
文件中定义的任何消息中重复使用。当然也可以在一个消息使用在另一个消息中定义的枚举类型——采用MessageType.EnumType
的语法格式。但是在同一个.proto
文件中定义枚举类型,枚举类型的值不能相同。编译器会认为已经存在。 - 当你编译一个使用了
enum
的.proto
文件时,生成的代码中会包含Java
或C++
对应的enum
,针对Python
的特定的EnumDescriptor
类,用来在执行生成的类中创建一系列包含数值的符号常量。
在反序列化期间,无法识别的enum
值将保留在消息中,尽管在反序列化消息时如何表示该值取决于语言。在支持指定符号范围之外使用值的开放枚举类型的语言,如c++
和Go
,未知的枚举值只是作为其基础整数表示形式存储。在具有封闭枚举类型的语言,如Java
,枚举中的大小写用于表示无法识别的值,并且可以使用特殊的访问器访问底层整数。在任何一种情况下,如果消息被序列化,未被识别的值仍将与消息一起序列化。
保留变量
如果你通过完全删除或注释一个字段来更新枚举类型时,那么之后的用户在更新他们自己的类型时将可以重用该字段的序号。如果之后他们使用旧版的.proto
时,会引起严重的问题,包括数据损坏、隐私bug
等。避免给问题的途径之一就是指明你要删除的字段需要(或者会在JSON
序列化时会引起问题的名称)是reserved
的,这样将来用户在使用这些字段时protocol buffer
编译器就会告警。你可以指明你要保留的数字到可能的最大值(通过max
关键字)得范围。
enum Foo {
reserved 2, 15, 9 to 11, 40 to max;
reserved "FOO", "BAR";
}
复制代码
注意,不能在同一个reserved
语句中混用字段名称和字段序号。
使用其他消息类型
你也可以使用其它消息类型作为字段类型。
例如,假如你想在SearchResponse
消息中包含一个Result
消息,你可以在同一个.proto
文件中定义一个Result
消息类型,然后在SearchResponse
中声明一个Result
类型的字段。
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
复制代码
导入定义
在上面的例子中,Result
消息类型和SearchResponse
定义在同一个.proto
文件中,如果你要用来的字段类型已经在其它的.proto
文件中定义了呢?
你可以通过从其它.proto
文件中导入它们来使用这些定义。要使用其它.proto
的定义,你需要在你的文件头部导入声明:
import "myproject/other_protos.proto";
复制代码
默认情况下你只能使用直接导入的.proto
文件中的定义。然而, 有时候你需要移动一个.proto
文件到一个新的位置。现在,你可以在旧位置放置一个虚拟 .proto
文件,以使用命令 import public
将所有导入转发到新位置,而不是直接移动 .proto
文件并在一次更改中更新所有调用点。导入包含 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
标志设为项目的根目录,并且使用全路径导入。
使用proto2消息类型
可以在你的proto3
消息中导入并使用proto2
的消息类型,反之亦可。然而proto2
的枚举不能再proto3
中直接使用(可以在导入的proto2
的消息中使用)。
嵌套类型
你可以在一个消息类型中定义并使用其它的消息类型,就像下面的例子 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;
}
}
}
复制代码
更新消息类型
如果已存在的消息类型不再满足你的需求
例如,你想在消息格式中添加新的字段,但还想使用就格式生成的代码。别担心!在不破坏你现有代码的基础上更新消息类型很简单。只需要记住下面的规则:
- 不要修改已有字段的序号。
- 如果你新增了字段,任何使用旧格式序列化的消息仍能被新生成的代码解析。你应该记住这些元素的默认值,以便新代码可以正确地与旧代码生成的消息进行交互。类似地,由新代码创建的消息可以由旧代码解析:旧的二进制文件在解析时简单地忽略新字段。
- 字段可以被移除,只要它的序号不再被你更新的消息类型使用。你可以重命名字段,或者添加前缀”OBSOLETE_”,或者保留字段号,这样
.proto
的未来用户就不会意外地重用该号码。 int32
、uint32
、int64
、uint64
和bool
都是兼容的——这意味着你可以将一个字段从这些类型中的一种更改为另一种,而不会中断向前或向后兼容。如果从连线中解析出一个不适合相应类型的数字,那么你将获得与在c++
中将该数字强制转换为该类型相同的效果(例如,如果将64
位数字读取为int32
,那么它将被截断为32
位)。sint32
和sint64
是相互兼容的,但不与其它整型兼容。string
和bytes
兼容,bytes
与UTF-8
兼容。- 如果字节包含消息的编码版本,则嵌入的消息与
bytes
兼容。 fixed32
与sfixed32
、fixed64
和sfixed64
兼容。- 在传输格式中
enum
与int32
、uint32
、int64
、uint64
兼容(注意变量不兼容的部分将被截断)。然而需要留意的是在消息反序列化时,客户端代码会被区别对待:例如,尽管无法识别的proto3
中的enum
类型会被保存在消息中,但是在消息反序列化时,它是如何表示这取决于语言。int
字段总会保留它的值。修改new oneof
成员中的单个变量是安全且二进制兼容的。如果您确定没有代码一次设置多个字段,那么将多个字段移动到一个新的字段中可能是安全的。将任何字段移动到现有字段中都是不安全的。
未知字段
未知字段是protocol buffer
在序列化数据时无法解析的数据。例如,当旧的二进制代码在解析带有新字段的新二进制代码发送的数据时,这些新字段将成为旧二进制代码中的未知字段。
最初,在解析时proto3
总是丢弃未知字段,但在3.5
版本之后,重新引入了未知字段的保留来匹配proto2
的行为。在3.5
及之后的版本中,在解析时未知字段会被保留并将其包含的序列化的输出中。
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 Any& detail : status.details()) {
if (detail.Is<NetworkErrorDetails>()) {
NetworkErrorDetails network_error;
detail.UnpackTo(&network_error);
... processing network_error ...
}
}
复制代码
目前,用于处理 Any
类型的运行时库正在开发中。
如果您已经熟悉proto2
语法,则Any
可以保存任意 proto3
消息,类似于可以允许扩展的proto2
消息。
Oneof
如果有有一个包含多个字段的消息,在同一时间最多只能设置一个字段,那么你可以通过使用oneof
特性强制执行此行为并节省内存。
除所有字段共享同一个Oneof
内存和最多同时只能设置一个字段外,Oneof
字段与常规字段类似。设置oneof
字段中的任何成员都将自动清除其它成员。根据你所使用的的语言不同,你可以使用(必要时)特定的case()
或WhichOneof()
方法来检查Oneof
中的哪个变量被设置。
使用OneOf
要在你的.proto
文件中定义一个Oneof
字段,你可以在的oneof
关键字后跟上你的oneof
名称,就如下面的test_oneof
:
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
复制代码
之后你可以添加你的oneof
字段到oneof
定义中。除了不能使用repeated
字段,你可以使用任意字段。
在你生成的代码中,oneof
字段有着与常规字段一样的getters
和setters
。必要时,你也可以使用特定的方法来确定oneof
中的哪个值被设置。
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
字段使用反射APIs
。- 如果你设置
oneof
字段为默认值(比如设置int32
字段为0
),该字段的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()
来交换两个带有oneofs
的消息,每个消息会以另一个的oneof case
结束:在下面的例子中,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(msg2.has_name()); 复制代码
向后兼容问题
在新增或移除oneof
字段时要慎重。如果检测到oneof
的返回值为None/Not_SET
,可能意味着这个oneof
尚未设置或已在不同版本的oneof
中设置。无法区分这两者之间的不同,因为无法确定传输中的未知字段是否是给oneof
的成员。
Tag
重用问题
- 移入/移出字段到
oneof
:在消息序列化和解析后,你可能会丢失部分消息(有些字段被清理了)。但是,你可以安全地将单个字段移动到一个新的oneof
字段中,如果知道只设置了一个字段,则可以移动多个字段。 - 删除一个
oneof
字段后有添加:在消息序列化和解析后,可能会将你当前的设置清零。 - 切割/合并
oneof
:与移动常规字段问题相似。
Maps
如果你想创建一个关联映射作为你的数据定义的一部分,protocol buffers
提供了一个方便快捷的语法:
map<key_type, value_type> map_field = N;
复制代码
key_type
可以是任意的integral
或string
类型(即除了浮点型和bytes
外的所有标量类型)。注意enum
不是有效的key_type
。value_type
可以是除了其它Map
外的所有类型。
那么,假如你想创建一个项目映射,每个项目关联一个string
键,定义如下:
map<string, Project> projects = 3;
复制代码
Map
字段不可以是repeated
。- 映射值的网络格式排序和映射迭代排序是未定义的,所以在特定的排序中你不能依赖你的映射元素组成。
- 为
.proto
生成文本格式时,映射根据键排序。数字键按数字大小排序。 - 从网络解析/合并时,如果键有多个副本,那么使用最后遇到的键。当从文本格式中解析映射时,如果键存在副本,则可能解析失败。
- 如果你仅提供了
Map
字段的键而没有提供值,字段序列化时的行为因语言而异。在C++
、Java
和Python
中,值会被序列化为该类型的默认值,在其它语言中并不会被序列化。
向后兼容
map
语法等效于以下内容,因此不支持 map
的协议缓冲区实现仍然可以处理您的数据:
message MapFieldEntry {
optional key_type key = 1;
optional value_type value = 2;
}
repeated MapFieldEntry map_field = N;
复制代码
任何支持映射的协议缓冲区实现都必须生成和接受上述定义可以接受的数据。
packages
你可以在.proto
文件中添加package
说明符来避免协议消息类型键的名称冲突。
package foo.bar;
message Open { ... }
复制代码
之后在定义你的消息类型字段时,你可以使用package
说明符:
message Foo {
...
foo.bar.Open open = 1;
...
}
复制代码
package
说明符影响生成代码的方式依赖于你所选的语言:
- 在
C++
中,生成的类会被打包到C++
的命名空间中。例如:Open
位于foo::bar
命名空间中。 - 在
Java
中,package
作为Java
包使用,除非在.proto
文件中额外提供option java_package
。 - 在
Python
中,package
指令会被忽略,Python
模块是根据它们在文件系统中的位置来组织的。 - 在
Go
中,package
将被用作Go
的包名,除非在.proto
文件中额外提供option go_package
。 - 在
Ruby
中,生成的类会被打包嵌入到Ruby
的命名空间中,并转换为所需的Ruby
大小写样式(第一个字母大写;如果第一个字符不是字母,PB_
是前缀)。例如:Open
位于foo::bar
命名空间中。 - 在
C#
中,package
在被转换为PascalCase
后作为命名空间使用,除非在.proto
文件中额外提供option csharp_namespace
。
包和名称解析
Protocol buffer
语言中的类型名称解析类似于C++
:首先在最内层查找,之后是下一层,一次类推,每个包在其父包的“内部”。“.”开头(例如,.foo.bar.Baz
)意味着从最外层作用域开始查找。
Protocol buffer
编译器通过导入的.proto
文件来解析所有的类型名称。即使有着不同的作用域规则,各语言生成的代码也知道如何每种类型该如何使用。
定义服务
如果你现在RPC
(远程调用)系统中使用你的消息类型,你可以在.proto
文件中定义RPC
服务接口,之后protocol buffer
编译器会生成所选语言的服务接口代码和存根。比如,你要定义一个RPC
服务,它使用你的SearchRequest
并返回SearchResponse
,在.proto
文件中你可以这样定义:
service SearchService {
rpc Search (SearchRequest) returns (SearchResponse);
}
复制代码
使用protocol buffer
最直接的RPC
系统是gRPC
:由Google
开发的,与语言和平台无关的开源RPC
系统。gRPC
与protocol buffer
协同良好,它允许你使用特殊的protocol buffer
插件直接从.proto
文件中生成相关的RPC代码。
如果你不想使用gRPC
,你也可以在你自己的RPC
实现中使用protocol buffer
。
也有一些正在进行的第三方项目来为protocol buffer
开发RPC
实现。
JSON Mapping
Proto3
支持Json
编码规范,这使得在不同系统间共享数据变得更加方便。在下面的表中,将逐个类型地描述编码。
如果一个值在JSON
编码中丢失或为null
,在解析到protocol buffer
时它会被解释为合适的默认值。如果protocol buffer
中的字段有默认值,那么在Json
编码的数据中将默认省略该字段,以节省空间。在Json
编码输出中,实现可以提供带有默认字段的选项。
proto3 | Json | Json示例 | 备注 |
---|---|---|---|
message | object | {“fooBar”:v,”g”:null,_} | 生成Json对象。消息字段名称被映射为lowerCamelCase并成为Json对象的键。如果指定了json_name字段选项,则指定的值将被作为键使用。解析器既接受lowerCamelCase名称(或使用json_name指定的名称),也接受原生的proto字段名称。所有字段类型都可接受null,并被视为该类型的默认值。 |
enum | string | “FOO_BAR” | 使用proto中指定的enum值名称。解析器既接受枚举名称,也接受整数值。 |
map<K,V> | object | {“K”:v,_} | 所有的键都被转换成string。 |
repeated V | array | [v, …] | null被当做空列表[]。 |
bool | true,false | true,false | |
string | string | “Hello World!” | |
bytes | base64 string | “YWJjMTIzIT8kKiYoKSctPUB+” | Json值会变成使用添加padding的标准base64编码的string。标准的或url安全的base64编码,带/不带padding也都可以接受。 |
int32,fixed32,uint32 | number | 1,-10,0 | Json值会变成十进制的数字。数字或string都可被接受。 |
int64,fixed64,uint64 | string | “1”,”-10″ | Json值会变成十进制的string。数字或string都可被接受。 |
float,double | number | 1.1,-10.0,0,”NaN”,”Infinity” | Json值会变成数字或”NaN”、”Infinity”、”-Infinity”其中之一。数字或string都可被接受。指数表示法也被接受。 |
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-normalized后的,并使用0、3、6或9位小数。除“Z”以外的偏移量也可以接受。 |
Duration | string | “1.000340012s”,”1s” | 根据所需的精度,生成的输出总是包含0、3、6或9位小数,跟后缀”s“。只要符合纳秒精度和后缀“s”的要求,任何小数(也可以没有)都可以接受。 |
Struct | object | { … } | 任意的Json对象。 |
Wrapper types | various types | 2,”2″,”foo”,true,”true”,null,0,… | 包装器使用与包装的原始类型相同的JSON表示,但在数据转换和传输期间允许并保留null。 |
FieldMask | string | “f.fooBar,h” | |
ListValue | array | [foo,bar, …] | |
Value | value | 任意的Json值 | |
NullValue | null | Json null | |
Empty | object | {} | 任意的空Json对象。 |
JSON
选项
Proto3
的Json
实现可支持下列选项:
- 带默认值得空字段:默认情况下,在
proto3 JSON
输出中会省略具有默认值的字段。实现可以提供一个选项来覆盖此行为,并使用其默认值输出字段。 - 忽略未知类型:默认情况下,
Proto3 Json
解析器会驳回未知字段,但在解析时可以提供选项来忽略未知字段。 - 使用
proto
字段来代替lowerCamelCase
名称:默认情况下,proto3 Json
的输出应该将字段名转换为lowerCamelCase
并作为Json
名称使用。该实现可以通过提供选项来使用proto
字段作为Json
名称。Proto3 Json
解析器被设计为可同时接受转换后的lowerCamelCase
名称和proto
字段名称。 - 指明
enum
值作为整数而不是string
:默认情况下,在Json
输出中使用枚举值的名称。通过选项可指定使用数字代替枚举值。
options
.proto
文件中的各个声明可以用许多选项进行注释。选项不会改变声明的总体含义,但可能影响在特定上下文中处理它的方式。可用选项的完整列表在google/protobuf/description.proto
中定义。
有些选项是文件级别的,意味着它们应该写在开头位置,而不是在消息、枚举或服务定义中。有些选项是消息级别的,意味着它们应该写在消息定义中。有些选项是字段选项,意味着它们应该写在字段定义中。选项也可以写在枚举类型、枚举值、服务类型和服务方法中,然而,当前不存在对Any
有用的选项。
以下是一些最常用的选项:
-
java_package
(文件级):这个包你想用来生成Java
类。如果.proto
文件中没有额外给出java_package
选项,默认情况下使用proto
包(在.proto
文件中使用package
关键字指明的)。然而通常情况下proto
包并不适合Java
包,因为不希望proto
包以反向域名展开。如果不生成Java
代码,此项无效。option java_package = "com.example.foo"; 复制代码
-
java_outer_classname
(文件级):希望生成的最外层Java
类的类名(以及文件名)。如果在.proto
文件中没有指定显式的java_outer_classname
,那么将通过将.proto
文件名转换为驼峰写法(比如foo_bar.proto
变为FooBar.java
)来构造类名。如果不生成Java
代码,则此选项无效。option java_outer_classname = "Ponycopter"; 复制代码
-
java_multiple_files
(文件级):如果为false
,则只.java
为该.proto
文件生成一个文件,所有Java
类/枚举/等。为顶级消息、服务和枚举生成的消息将嵌套在外部类中。如果为true
,.java
将为每个Java
类/枚举/等生成单独的文件。为顶级消息、服务和枚举生成,并且为此.proto
文件生成的包装器Java
类将不包含任何嵌套类/枚举/等。这是一个布尔选项,默认为false
。如果不生成Java
代码,则此选项无效。option java_multiple_files = true; 复制代码
-
optimize_for
(文件选项):可以设置为SPEED、CODE_SIZE
、 或LITE_RUNTIME
。这会通过以下方式影响C++
和Java
代码生成器(以及可能的第三方生成器):- SPEED(默认):
Protocol buffer
编译器会为你的消息类型生成序列化、解析和其它常用操作的代码。此代码高度优化。 CODE_SIZE
:Protocol buffer
编译器会生成最小的类,其依赖共享、反射的代码来实现序列化、解析和其它操作。因此生成的代码比SPEED
小很多,但操作也会比较慢。Classes
仍会实现与SPEED
模式相同的公共API
。这种模式在包含大量.proto
文件且不是所有文件都需要快速生成的应用程序中最有用。LITE_RUNTIME
:Protocol buffer
编译器依赖“轻量的”运行时库(使用libprotobuf-lite
而不是libprotobuf
)。lite
运行时比完整的库小得多(大约小一个数量级),但是忽略了某些特性,比如描述符和反射。这对于在受限平台(如手机)上运行的应用程序尤其有用。编译器仍然会像在SPEED
模式下那样生成所有方法的快速实现。生成的类将仅用每种语言实现MessageLite
接口,该接口只提供完整Message
接口方法的一个子集。
option optimize_for = CODE_SIZE; 复制代码
- SPEED(默认):
-
cc_enable_arenas
(文件级):为C++
代码生成启用arena allocation
。 -
objc_class_prefix
(文件级):为.proto
文件生成的所有Objective-C
类设置前缀。没有默认值。你应该使用苹果推荐的前缀,即3-5
个大写字母。注意苹果保留所有的2
个字母的前缀。 -
deprecated
(文件级):如果设置为true
,则表示该字段已被废弃,新代码不应使用该字段。在大多数语言中,该选项并没有实际效果。在Java
中,会变成一个@Deprecated
注释。将来,其它语言的代码生成器可能会在字段访问器上生成弃用注释,这将使得编译器在尝试使用该字段时发出警告。如果该字段将不再使用且你也不希望有新的用户使用它,那么可以考虑使用保留语句替换字段声明。int32 old_field = 6 [deprecated=true] 复制代码
自定义option
Protocol buffer
也允许你定义并使用自定义的选项。这是大多数人用不到的高级功能。如果你真的想创建自定义选项,查看Proto2
。注意,用扩展来创建自定义选项,这是proto3
中唯一允许使用的自定义选项
编译生成
要从.proto
文件中包括你定义的消息类型的Java
、Python
、C++
、Go
、Ruby
、Objective-C
或C#
代码,你需要允许protocol buffer
编译器protoc
。如果你还没安装编译器,需要先安装。对于Go
,你还需要安装特定的生成插件。
Protocol
编译器使用如下:
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
选项指明,它们将按顺序检索。--proto_path
可简写为-I=IMPORT_PATH
。 -
你可以提供一个或多个输出命令:
--cpp_out
在DST_DIR
目录中生成C++
代码。详见C++ 生成代码引用。--java_out
在DST_DIR
目录中生成Java
代码。详见Java 生成代码引用。--python_out
在DST_DIR
目录中生成Python
代码。详见Python 生成代码引用。- —
go_out
在DST_DIR
目录中生成Go
代码。详见Go 生成代码引用。 - —
ruby_out
在DST_DIR
目录中生成Ruby
代码。详见Ruby 生成代码引用。 - —
objc_out
在DST_DIR
目录中生成Object-C
代码。详见Object-C 生成代码引用。 - —
csharp_out
在DST_DIR
目录中生成C#
代码。详见C# 生成代码引用。 - —
php_out
在DST_DIR
目录中生成PHP
代码。详见PHP 生成代码引用。
作为额外的便利,如果
DST_DIR
以.zip
或.jar
,编译器将生成指定名称的ZIP
格式的压缩包。.jar
输出还将根据Java JAR
规范的要求提供一个清单文件。注意如果输出文件已存在,那么它将被重写,编译器并不会生成一个新的副本。 -
你必须提供一个或者多个
.proto
文件作为输入。多个.proto
文件可以一次指定。虽然这些文件是相对于当前目录命名的,但是每个文件必须驻留在IMPORT_PATH
中的一个,以便编译器可以确定它的规范名称。