Protobuf3语言指南

10次阅读

共计 5304 个字符,预计需要花费 14 分钟才能阅读完成。

定义一个消息类型
先来看一个非常简单的例子。假设你想定义一个“搜索请求”的消息格式,每一个请求含有一个查询字符串、你感兴趣的查询结果所在的页数,以及每一页多少条查询结果。可以采用如下的方式来定义消息类型的 .proto 文件了:
syntax = “proto3”;

message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}

文件的第一行指定了你正在使用 proto3 语法:如果你没有指定这个,编译器会使用 proto2。这个指定语法行必须是文件的非空非注释的第一个行。
SearchRequest 消息格式有 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 文件中使用这些预留标识号,编译时就会报警。同样你也不能使用早期保留的标识号。
指定字段规则
所指定的消息字段修饰符必须是如下之一:

singular:一个格式良好的消息应该有 0 个或者 1 个这种字段(但是不能超过 1 个)。
repeated:在一个格式良好的消息中,这种字段可以重复任意多次(包括 0 次)。重复的值的顺序会被保留。在 proto3 中,repeated 的标量域默认情况虾使用 packed。你可以了解更多的 pakced 属性在 Protocol Buffer 编码

添加更多消息类型
在一个.proto 文件中可以定义多个消息类型。在定义多个相关的消息的时候,这一点特别有用——例如,如果想定义与 SearchResponse 消息类型对应的回复消息格式的话,你可以将它添加到相同的.proto 文件中,如:
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}

message SearchResponse {

}
添加注释
向.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.
}
保留标识符(Reserved)
如果你通过删除或者注释所有域,以后的用户可以重用标识号当你重新更新类型的时候。如果你使用旧版本加载相同的.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 声明中同时声明域名字和标识号
从.proto 文件生成了什么?
当用 protocol buffer 编译器来运行.proto 文件时,编译器将生成所选择语言的代码,这些代码可以操作在.proto 文件中定义的消息类型,包括获取、设置字段值,将消息序列化到一个输出流中,以及从一个输入流中解析消息。

对 C ++ 来说,编译器会为每个.proto 文件生成一个.h 文件和一个.cc 文件,.proto 文件中的每一个消息有一个对应的类。
对 Java 来说,编译器为每一个消息类型生成了一个.java 文件,以及一个特殊的 Builder 类(该类是用来创建消息类接口的)。
对 Python 来说,有点不太一样——Python 编译器为.proto 文件中的每个消息类型生成一个含有静态描述符的模块,,该模块与一个元类(metaclass)在运行时(runtime)被用来创建所需的 Python 数据访问类。
对 go 来说,编译器会位每个消息类型生成了一个.pd.go 文件。
对于 Ruby 来说,编译器会为每个消息类型生成了一个.rb 文件。
javaNano 来说,编译器输出类似域 java 但是没有 Builder 类
对于 Objective- C 来说,编译器会为每个消息类型生成了一个 pbobjc.h 文件和 pbobjcm 文件,.proto 文件中的每一个消息有一个对应的类。
对于 C# 来说,编译器会为每个消息类型生成了一个.cs 文件,.proto 文件中的每一个消息有一个对应的类。

你可以从如下的文档链接中获取每种语言更多 API(proto3 版本的内容很快就公布)。API Reference
枚举
当需要定义一个消息类型的时候,可能想为一个字段指定某“预定义值序列”中的一个值。例如,假设要为每一个 SearchRequest 消息添加一个 corpus 字段,而 corpus 的值可能是 UNIVERSAL,WEB,IMAGES,LOCAL,NEWS,PRODUCTS 或 VIDEO 中的一个。其实可以很容易地实现这一点:通过向消息定义中添加一个枚举(enum)并且为每个可能的值定义一个常量就可以了。
在下面的例子中,在消息格式中添加了一个叫做 Corpus 的枚举类型——它含有所有可能的值 ——以及一个类型为 Corpus 的字段:
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
Corpus corpus = 4;
}
如你所见,Corpus 枚举的第一个常量映射为 0:每个枚举类型必须将其第一个类型映射为 0,这是因为:

必须有有一个 0 值,我们可以用这个 0 值作为默认值。
这个零值必须为第一个元素,为了兼容 proto2 语义,枚举类的第一个值总是默认值。

你可以通过将不同的枚举常量指定位相同的值。如果这样做你需要将 allow_alias 设定位 true,否则编译器会在别名的地方产生一个错误信息。
enum EnumAllowingAlias {
option allow_alias = true;
UNKNOWN = 0;
STARTED = 1;
RUNNING = 1;
}
enum EnumNotAllowingAlias {
UNKNOWN = 0;
STARTED = 1;
// RUNNING = 1; // Uncommenting this line will cause a compile error inside Google and a warning message outside.
}
枚举常量必须在 32 位整型值的范围内。因为 enum 值是使用可变编码方式的,对负数不够高效,因此不推荐在 enum 中使用负数。如上例所示,可以在 一个消息定义的内部或外部定义枚举——这些枚举可以在.proto 文件中的任何消息定义里重用。当然也可以在一个消息中声明一个枚举类型,而在另一个不同 的消息中使用它——采用 MessageType.EnumType 的语法格式。
当对一个使用了枚举的.proto 文件运行 protocol buffer 编译器的时候,生成的代码中将有一个对应的 enum(对 Java 或 C ++ 来说),或者一个特殊的 EnumDescriptor 类(对 Python 来说),它被用来在运行时生成的类中创建一系列的整型值符号常量(symbolic constants)。
在反序列化的过程中,无法识别的枚举值会被保存在消息中,虽然这种表示方式需要依据所使用语言而定。在那些支持开放枚举类型超出指定范围之外的语言中(例如 C ++ 和 Go),为识别的值会被表示成所支持的整型。在使用封闭枚举类型的语言中(Java),使用枚举中的一个类型来表示未识别的值,并且可以使用所支持整型来访问。在其他情况下,如果解析的消息被序列号,未识别的值将保持原样。
更新一个消息类型
如果一个已有的消息格式已无法满足新的需求——如,要在消息中添加一个额外的字段——但是同时旧版本写的代码仍然可用。不用担心!更新消息而不破坏已有代码是非常简单的。在更新时只要记住以下的规则即可。

不要更改任何已有的字段的数值标识。
如果你增加新的字段,使用旧格式的字段仍然可以被你新产生的代码所解析。你应该记住这些元素的默认值这样你的新代码就可以以适当的方式和旧代码产生的数据交互。相似的,通过新代码产生的消息也可以被旧代码解析:只不过新的字段会被忽视掉。注意,未被识别的字段会在反序列化的过程中丢弃掉,所以如果消息再被传递给新的代码,新的字段依然是不可用的(这和 proto2 中的行为是不同的,在 proto2 中未定义的域依然会随着消息被序列化)
非 required 的字段可以移除——只要它们的标识号在新的消息类型中不再使用(更好的做法可能是重命名那个字段,例如在字段前添加“OBSOLETE_”前缀,那样的话,使用的.proto 文件的用户将来就不会无意中重新使用了那些不该使用的标识号)。
int32, uint32, int64, uint64, 和 bool 是全部兼容的,这意味着可以将这些类型中的一个转换为另外一个,而不会破坏向前、向后的兼容性。如果解析出来的数字与对应的类型不相符,那么结果就像在 C ++ 中对它进行了强制类型转换一样(例如,如果把一个 64 位数字当作 int32 来 读取,那么它就会被截断为 32 位的数字)。
sint32 和 sint64 是互相兼容的,但是它们与其他整数类型不兼容。
string 和 bytes 是兼容的——只要 bytes 是有效的 UTF- 8 编码。
嵌套消息与 bytes 是兼容的——只要 bytes 包含该消息的一个编码过的版本。
fixed32 与 sfixed32 是兼容的,fixed64 与 sfixed64 是兼容的。
枚举类型与 int32,uint32,int64 和 uint64 相兼容(注意如果值不相兼容则会被截断),然而在客户端反序列化之后他们可能会有不同的处理方式,例如,未识别的 proto3 枚举类型会被保留在消息中,但是他的表示方式会依照语言而定。int 类型的字段总会保留他们的

Any
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
Oneof
如果你的消息中有很多可选字段,并且同时至多一个字段会被设置,你可以加强这个行为,使用 oneof 特性节省内存.
Oneof 字段就像可选字段,除了它们会共享内存,至多一个字段会被设置。设置其中一个字段会清除其它字段。你可以使用 case() 或者 WhichOneof() 方法检查哪个 oneof 字段被设置,看你使用什么语言了.
使用 Oneof
为了在.proto 定义 Oneof 字段,你需要在名字前面加上 oneof 关键字, 比如下面例子的 test_oneof:
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
然后你可以增加 oneof 字段到 oneof 定义中. 你可以增加任意类型的字段, 但是不能使用 repeated 关键字.

正文完
 0