先来看一个非常简单的例子。假设你想定义一个“搜索请求”的消息格式,每一个请求含有一个查询字符串、你感兴趣的查询结果所在的页数,以及每一页多少条查询结果。可以采用如下的方式来定义消息类型的.proto文件了:
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
在上面的例子中,所有字段都是标量类型:两个整型(page_number和result_per_page),一个string类型(query)。当然,你也可以为字段指定其他的合成类型,包括枚举(enumerations)或其他消息类型。
正如你所见,在消息定义中,每个字段都有唯一的一个数字标识符。这些标识符是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变。注:[1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 [1,15]之内的标识号。切记:要为将来有可能添加的、频繁出现的标识号预留一些标识号。
最小的标识号可以从1开始,最大到2^29 - 1, or 536,870,911。不可以使用其中的[19000-19999]( (从FieldDescriptor::kFirstReservedNumber 到 FieldDescriptor::kLastReservedNumber))的标识号, Protobuf协议实现中对这些进行了预留。如果非要在.proto文件中使用这些预留标识号,编译时就会报警。同样你也不能使用早期保留的标识号。
所指定的消息字段修饰符必须是如下之一:
向.proto文件添加注释,可以使用C/C++/java风格的双斜杠(//) 语法格式,如:
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文件添加注释,可以使用C/C++/java风格的双斜杠(//) 语法格式,如:
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文件这会导致严重的问题,包括数据损坏、隐私错误等等。现在有一种确保不会发生这种情况的方法就是指定保留标识符(and/or names, which can also cause issues for JSON serialization不明白什么意思),protocol buffer的编译器会警告未来尝试使用这些域标识符的用户。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
注:不要在同一行reserved声明中同时声明域名字和标识号
当用protocol buffer编译器来运行.proto文件时,编译器将生成所选择语言的代码,这些代码可以操作在**.proto文件中定义的消息类型,包括获取**、设置字段值,将消息序列化到一个输出流中,以及从一个输入流中解析消息。
你可以从如下的文档链接中获取每种语言更多API。API Reference
一个标量消息字段可以含有一个如下的类型——该表格展示了定义于 .proto文件中的类型,以及与之对应的、在自动生成的访问类中定义的类型:
.proto Type | Notes | Python Type | Go Type |
---|---|---|---|
double | float | float64 | |
float | float | float32 | |
int32 | 使用变长编码,对于负值的效率很低,如果你的域有可能有负值,请使用sint64替代 | int | int32 |
uint32 | 使用变长编码 | int | uint32 |
uint64 | 使用变长编码 | int | uint64 |
sint32 | 使用变长编码,这些编码在负值时比int32高效的多 | int | int32 |
sint64 | 使用变长编码,有符号的整型值。编码时比通常的int64高效。 | int | int64 |
fixed32 | 总是4个字节,如果数值总是比总是比228大的话,这个类型会比uint32高效。 | int | uint32 |
fixed64 | 总是8个字节,如果数值总是比总是比256大的话,这个类型会比uint64高效。 | int | uint64 |
sfixed32 | 总是4个字节 | int | int32 |
sfixed64 | 总是8个字节 | int | int64 |
bool | bool | bool | |
string | 一个字符串必须是UTF-8编码或者7-bit ASCII编码的文本。 | str | string |
bytes | 可能包含任意顺序的字节数据。 | str | []byte |
你可以在文章Protocol Buffer 编码中,找到更多“序列化消息时各种类型如何编码”的信息。
当一个消息被解析的时候,如果被编码的信息不包含一个特定的singular元素,被解析的对象锁对应的域被设置位一个默认值,对于不同类型指定如下:
当需要定义一个消息类型的时候,可能想为一个字段指定某“预定义值序列”中的一个值。例如,假设要为每一个SearchRequest消息添加一个 corpus字段,而corpus的值可能是UNIVERSAL
,WEB
,IMAGES
,LOCAL
,NEWS
,PRODUCTS
或VIDEO
中的一个。 其实可以很容易地实现这一点:通过向消息定义中添加一个枚举(enum)并且为每个可能的值定义一个常量就可以了。
在下面的例子中,在消息格式中添加了一个叫做Corpus的枚举类型——它含有所有可能的值 ——以及一个类型为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
,这是因为:
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 compile error inside Google and a warning message outside.
ENAA_FINISHED = 2;
}
枚举常量必须在32位整型值
的范围内。因为enum值是使用可变编码方式的,对负数不够高效,因此不推荐在enum中使用负数。如上例所示,可以在 一个消息定义的内部或外部定义枚举——这些枚举可以在 .proto文件中的任何消息定义里重用。当然也可以在一个消息中声明一个枚举类型,而在另一个不同 的消息中使用它——采用MessageType.EnumType的语法格式。
当对一个使用了枚举的 .proto文件运行protocol buffer
编译器的时候,生成的代码中将有一个对应的enum(对Java或C++来说),或者一个特殊的EnumDescriptor类(对 Python来说),它被用来在运行时生成的类中创建一系列的整型值符号常量(symbolic constants)。
Caution:the generated code may be subject to language-specific limitations on the number of enumerators (low thousands for one language). Please review the limitations for the languages you plan to use.
在反序列化的过程中,无法识别的枚举值会被保存在消息中,虽然这种表示方式需要依据所使用语言而定。在那些支持开放枚举类型超出指定范围之外的语言中(例如C++和Go),为识别的值会被表示成所支持的整型。在使用封闭枚举类型的语言中(Java),使用枚举中的一个类型来表示未识别的值,并且可以使用所支持整型来访问。在其他情况下,如果解析的消息被序列号,未识别的值将保持原样。
关于如何在你的应用程序的消息中使用枚举的更多信息,请查看所选择的语言generated code guide
如果通过完全删除枚举条目来更新枚举类型,或者 注释掉它,未来的用户可以在制作他们的 自己对类型进行更新。如果他们以后加载旧的,这可能会导致严重的问题 相同的版本,包括数据损坏、隐私错误等 上。确保不会发生这种情况的一种方法是指定数字 的值(和/或名称,这也可能导致 JSON 序列化问题) 您删除的条目是。协议缓冲区编译器将抱怨 如果将来有任何用户尝试使用这些标识符。您可以指定您的 保留的数值范围使用关键字上升到最大可能值。.proto reserved 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文件中已经定义过了呢?
你可以通过导入(importing)其他.proto文件中的定义来使用它们。要导入其他.proto文件的定义,你需要在你的文件中添加一个导入声明,如:
import "myproject/other_protos.proto";
默认情况下你只能使用直接导入的 .proto文件中的定义. 然而, 有时候你需要移动一个 .proto文件到一个新的位置, 可以不直接移动 .proto文件, 只需放入一个伪 .proto 文件在老的位置, 然后使用import public转向新的位置。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_pathprotocal 编译器会在指定目录搜索要导入的文件。如果没有给出标志,编译器会搜索编译命令被调用的目录。通常你只要指定proto_path标志为你的工程根目录就好。并且指定好导入的正确名称就好。
你可以在其他消息类型中定义、使用消息类型,在下面的例子中,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;
}
}
}
如果一个已有的消息格式已无法满足新的需求——如,要在消息中添加一个额外的字段——但是同时旧版本写的代码仍然可用。不用担心!更新消息而不破坏已有代码是非常简单的。在更新时只要记住以下的规则即可。
required
的字段可以移除——只要它们的标识号在新的消息类型中不再使用(更好的做法可能是重命名那个字段,例如在字段前添加“OBSOLETE_”前缀,那样的话,使用的.proto文件的用户将来就不会无意中重新使用了那些不该使用的标识号)。int32
, uint32
, int64
, uint64
,和bool
是全部兼容的,这意味着可以将这些类型中的一个转换为另外一个,而不会破坏向前、 向后的兼容性。如果解析出来的数字与对应的类型不相符,那么结果就像在C++
中对它进行了强制类型转换一样(例如,如果把一个64位数字当作int32来 读取,那么它就会被截断为32位的数字)。sint32
和sint64
是互相兼容的,但是它们与其他整数类型不兼容。string
和bytes
是兼容的——只要bytes
是有效的UTF-8编码。bytes
包含该消息的一个编码过的版本。fixed32
与sfixed32
是兼容的,fixed64
与sfixed64
是兼容的。枚举类型
与int32
,uint32
,int64
和uint64
相兼容(注意如果值不相兼容则会被截断),然而在客户端反序列化之后他们可能会有不同的处理方式,例如,未识别的proto3枚举类型会被保留在消息中,但是他的表示方式会依照语言而定。int类型的字段总会保留他们的Any
类型消息允许你在没有指定他们的.proto
定义的情况下使用消息作为一个嵌套类型。一个Any
类型包括一个可以被序列化bytes
类型的任意消息,以及一个URL
作为一个全局标识符和解析消息类型。为了使用Any
类型,你需要导入import 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替换拓展
如果你的消息中有很多可选字段, 并且同时至多一个字段会被设置, 你可以加强这个行为,使用oneof
特性节省内存.
Oneof
字段就像可选字段, 除了它们会共享内存, 至多一个字段会被设置。 设置其中一个字段会清除其它字段。 你可以使用case()
或者WhichOneof()
方法检查哪个oneof
字段被设置, 看你使用什么语言了.
为了在.proto
定义Oneof
字段, 你需要在名字前面加上oneof
关键字, 比如下面例子的test_oneof:
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
然后你可以增加oneof
字段到 oneof
定义中. 你可以增加任意类型的字段, 但是不能使用repeated
关键字.
在产生的代码中, oneof
字段拥有同样的 getters
和setters
, 就像正常的可选字段一样. 也有一个特殊的方法来检查到底那个字段被设置. 你可以在相应的语言API指南中找到oneof API介绍.
oneof
会自动清楚其它oneof
字段的值. 所以设置多次后,只有最后一次设置的字段有值.SampleMessage message;
message.set_name("name");
CHECK(message.has_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.has_name());
oneof
中有多个成员,只有最会一个会被解析成消息。oneof
不支持repeated
.oneof
字段有效.C++
,需确保代码不会导致内存泄漏. 下面的代码会崩溃, 因为sub_message
已经通过set_name()
删除了SampleMessage message;
SubMessage* sub_message = message.mutable_sub_message();
message.set_name("name"); // Will delete sub_message
sub_message->set_... // Crashes here
C++
中,如果你使用Swap()
两个oneof
消息,每个消息,两个消息将拥有对方的值,例如在下面的例子中,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
字段。
Tage 重用问题:
oneof
:在消息被序列号或者解析后,你也许会失去一些信息(有些字段也许会被清除)oneof
字段oneof
:行为与移动常规字段相似。如果你希望创建一个关联映射,protocol buffer
提供了一种快捷的语法:
map<key_type, value_type> map_field = N;
其中key_type
可以是任意Integer
或者string类型
(所以,除了floating和bytes的任意标量类型都是可以的)value_type可以是任意类型。
例如,如果你希望创建一个project
的映射,每个Projecct
使用一个string
作为key
,你可以像下面这样定义:
map<string, Project> projects = 3;
Map
的字段可以是repeated
。Map
.proto
文件产生生成文本格式的时候,map
会按照key
的顺序排序,数值化的key
会按照数值排序。key
则后一个key
不会被使用,当从文本格式中解析map
时,如果存在重复的key
。生成map
的API现在对于所有proto3
支持的语言都可用了,你可以从API指南找到更多信息。
map语法序列化后等同于如下内容,因此即使是不支持map语法的protocol buffer实现也是可以处理你的数据的:
message MapFieldEntry {
key_type key = 1;
value_type value = 2;
}
repeated MapFieldEntry map_field = N;
任何支持映射的协议缓冲区实现都必须生成和 接受上述定义可以接受的数据。
当然可以为.proto文件新增一个可选的package声明符,用来防止不同的消息类型有命名冲突。如:
package foo.bar;
message Open { ... }
在其他的消息格式定义中可以使用包名+消息名的方式来定义域的类型,如:
message Foo {
...
foo.bar.Open open = 1;
...
}
包的声明符会根据使用语言的不同影响生成的代码。
C++
,产生的类会被包装在C++
的命名空间中,如上例中的Open
会被封装在 foo::bar
空间中; - 对于Java
,包声明符会变为java
的一个包,除非在.proto
文件中提供了一个明确有java_package
;Python
,这个包声明符是被忽略的,因为`Python模块是按照其在文件系统中的位置进行组织的。Go
,包可以被用做Go包名称,除非你显式的提供一个option go_package
在你的.proto
文件中。Ruby
,生成的类可以被包装在内置的Ruby
名称空间中,转换成Ruby
所需的大小写样式 (首字母大写;如果第一个符号不是一个字母,则使用PB_前缀),例如Open
会在Foo::Bar名称空间中。javaNano
包会使用Java
包,除非你在你的文件中显式的提供一个option java_package
。C#
包可以转换为PascalCase
后作为名称空间,除非你在你的文件中显式的提供一个option csharp_namespace
,例如,Open
会在Foo.Bar
名称空间中Protocol buffer
语言中类型名称的解析与C++
是一致的:首先从最内部开始查找,依次向外进行,每个包会被看作是其父类包的内部类。当然对于 (foo.bar.Baz)
这样以“.”分隔的意味着是从最外围开始的。
ProtocolBuffer
编译器会解析.proto
文件中定义的所有类型名。 对于不同语言的代码生成器会知道如何来指向每个具体的类型,即使它们使用了不同的规则。
如果想要将消息类型用在RPC(远程方法调用)系统中,可以在.proto
文件中定义一个RPC
服务接口,protocol buffer编译器将会根据所选择的不同语言生成服务接口代码及存根。如,想要定义一个RPC
服务并具有一个方法,该方法能够接收 SearchRequest
并返回一个SearchResponse
,此时可以在.proto
文件中进行如下定义:
service SearchService {
rpc Search(SearchRequest) returns (SearchResponse);
}
最直观的使用protocol buffe
r的RPC系统是gRPC一个由谷歌开发的语言和平台中的开源的PRC系统,gRPC在使用protocl buffer
时非常有效,如果使用特殊的protocol buffer
插件可以直接为您从.proto
文件中产生相关的RPC代码。
如果你不想使用gRPC
,也可以使用protocol buffer
用于自己的RPC实现,你可以从proto2语言指南中找到更多信息
还有一些第三方开发的PRC实现使用Protocol Buffer
。参考第三方插件wiki查看这些实现的列表。
Proto3
支持JSON
的编码规范,使他更容易在不同系统之间共享数据,在下表中逐个描述类型。
如果JSON编码的数据丢失或者其本身就是null
,这个数据会在解析成protocol buffer
的时候被表示成默认值。如果一个字段在protocol buffer
中表示为默认值,体会在转化成JSON
的时候编码的时候忽略掉以节省空间。具体实现可以提供在JSON编码中可选的默认值。