Protocol Buffers文档翻译:语言指南,Language Guide
这篇指南,说明的是,如何使用protocol buffer语言来组织妳的protocol buffer数据,包括:.proto文件语法;和,如何利用.proto文件来生成数据访问类。它涵盖了protocol buffers 语言的proto2 版本:欲了解较新的proto3语法,则参考Proto3语言指南。
这是一个参考指南——如果妳想找一个一步一步说明此文档中所描述的众多特性的示例的话,那么,请参考针对妳所选择的语言的教程。
首先,我们来看一个狠简单的示例。假设妳想要定义一个搜索请求消息格式,其中,每个搜索请求都包含一个查询字符串、结果中特定的页码、以及每页的结果数。那么,可以使用以下.proto文件。
message SearchRequest {
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3;
}
SearchRequest这个消息定义,指定了3个字段(名字/值对),每个字段都针对着妳想要在这种类型的消息中包含的一个数据。每个字段都有一个名字和一个类型。
在上面的示例中,所有的字段都是标量类型:两个整数(page_number和result_per_page)和一个字符串(query)。然而,妳也可以为字段指定复合类型,包括枚举和其它消息类型。
如妳所见,消息定义中的每个字段都有一个唯一的数字标记。这些标记被用来在消息的二进制格式中标识妳的各个字段,并且,一旦妳的消息类型投入使用,就不应当再改变这些标记了。注意,那些其值在 1 到15 范围的标记,需要占用一个字节来编码,这包括它的标识数字和字段的类型(妳可以在Protocol Buffer编码中找到更多相关说明)。其值在16到2047范围的标记,占用两个字节。因此,妳应当保留1到15的标记,用于最常用的消息元素。记住要留下一些空间,以便在日后加入某些新的常用元素。
妳可以使用的最小标记数字是1,最大标记数字是229 - 1,或者说536870911。另外,妳也不能使用19000到19999范围的数字(即为FieldDescriptor::kFirstReservedNumber到FieldDescriptor::kLastReservedNumber范围),它们是保留给Protocol Buffers实现自己来使用的——如果妳在自己的.proto文件中使用了其中某个保留数字,则protocol buffer编译器会给出警告。
妳可以将消息字段指定为以下的某一个:
•required:符合规范的消息中,必须拥有一个这个字段。
•optional:符合规范的消息中,可以拥有0个或1个这个字段(但不可以超过1个)。
•repeated:在符合规范的消息中,这个字段可以重复任意次(包括0次)。重复字段的值的顺序会被原样保留。
由于某些历史原因,对于由基本数字类型组合的重复字段,并未按照它们应该达到的高效率来编码。新写的代码,应当使用特殊的选项[packed=true]来产生一个更高效率的编码。例如:
repeated int32 samples = 4 [packed=true];
Required 是永久有效的 ,妳应当慎重地将字段标记为 required 。如果妳在某个时候想要不再输出或发送必需(required)字段,那么,在将它变更成一个可选(optional)字段时会遇到麻烦——旧的接收者会认为不带这个字段的消息是不完整的,因而可能会拒绝接收它们。妳应当考虑在程序内部根据实际需要来实现自定义的消息验证逻辑。Google的某个工程师已经得出结论,即,使用必需(required)字段, 坏处大于好处;它们倾向于只使用可选(optional)字段和重复(repeated)字段。然而,并非所有人都持这种看法。
可在单个.proto文件中定义多个消息类型。这在定义多个相关消息时狠有用——例如,假设妳想定义一个用于回复SearchResponse 消息类型的答复消息格式的话,就可以将它放置在同一个.proto中:
message SearchRequest {
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3;
}
message SearchResponse {
...
}
要向.proto文件中加入评论的话,请使用C/C++风格的//语法。
message SearchRequest {
required string query = 1;
optional int32 page_number = 2;// 我们预期获取到哪一页?
optional int32 result_per_page = 3;// 每页包括的搜索结果数。
}
当妳对妳的.proto文件运行protocol buffer 编译器时,编译器会生成与妳所选择的语言相对应的代码,妳将会需要这些代码来与妳所定义的消息类型打交道,包括获取及设置字段值、将妳的消息序列化到一个输出流中、以及从一个输入流中解析出妳的消息。
对于C++,编译器会针对每个.proto文件生成一个.h文件和一个.cc文件,并且,针对妳的文件中描述的每个消息类型都会生成一个类。
对于Java,编译器会针对每个消息类型生成一个.java文件,其中包含着对应的类,另外还有一个特殊的Builder类,用于创建消息类实例。
Python比较特殊 ——Python会生成一个模块,其中针对妳的.proto中的每个消息类型都有一个静态描述器,再在运行时刻与一个元类(metaclass)配合创建必要的Python 数据访问类。
妳可以在对应语言的教程中找到该语言APIs 的更多细节。欲知更多API细节,则参考相关的API手册。
一个标量消息字段可以是以下类型——下表列出的是,.proto文件中指定的类型,以及在自动生成的类中对应的类型:
.proto类型 |
说明 |
C++类型 |
Java类型 |
Python类型[2] |
double |
double |
double |
float |
|
float |
float |
float |
float |
|
int32 |
使用变长编码。对于负数的编码效率不高——如果某个字段可能具有负数值,则应当使用sint32。 |
int32 |
int |
int |
int64 |
使用变长编码。对于负数的编码效率不高——如果某个字段可能具有负数值,则应当使用sint64。 |
int64 |
long |
int/long[3] |
uint32 |
使用变长编码。 |
uint32 |
int[1] |
int/long[3] |
uint64 |
使用变长编码。 |
uint64 |
long[1] |
int/long[3] |
sint32 |
使用变长编码。带符号的整数值。相对于普通的int32,这种类型对于负数的编码效率更高。 |
int32 |
int |
int |
sint64 |
使用变长编码。带符号的整数值。相对于普通的int64,这种类型对于负数的编码效率更高。 |
int64 |
long |
int/long[3] |
fixed32 |
永远是4个字节。如果值通常比228 大,则比 uint32 的效率要高。 |
uint32 |
int[1] |
int |
fixed64 |
永远是8个字节。如果值通常比 256 大,则比 uint64 的效率要高。 |
uint64 |
long[1] |
int/long[3] |
sfixed32 |
永远是4个字节。 |
int32 |
int |
int |
sfixed64 |
永远是8个字节。 |
int64 |
long |
int/long[3] |
bool |
bool |
boolean |
bool |
|
string |
字符串中必须包含UTF-8编码或7位ASCII编码的文本内容。 |
string |
String |
str/unicode[4] |
bytes |
可以包含任意的字节序列。 |
string |
ByteString |
str |
在Protocol Buffer Encoding 中,可找到更多关于这些类型如何被序列化的信息。
[1] 在Java中,无符号32位和64位整数是使用对应的带符号类型来表示的,其最高位是储存在符号位中的。
[2] 在所有的情况下,针对一个字段设置其值,都会进行类型检查,以确保它是有效的。
[3] 64位或无符号32位整数,在解码时都是使用长整型(long)来表示的,但是,如果在设置该字段时传入的是一个整型(int),则其解码结果也是一个整型(int)。在所有情况下,其值都必定能够以在设置时所使用的类型容纳下来。参考[2]。
[4] Python字符串,在解码时是使用unicode 来表示的,但是,如果在设置值时传入的是一个ASCII 字符串,则其解码结果可能会是一个str(这一特性可能会发生改变)。
前面已经提到过,消息描述中的元素可以被标记为可选的(optional)。格式正确的消息,其中可能包含有可选元素,也可能不包含。在解析某条消息时,如果其中不包含可选元素,那么,在解析完毕的对象中,相应的字段会被设置为该字段的默认值。默认值可在消息描述中指定。例如,妳想给SearchRequest 消息的 result_per_page 值设置默认值为10。
optional int32 result_per_page = 3 [default = 10];
如果未给某个可选元素指定默认值,则,会使用一个与类型相关的默认值:对于字符串,默认值是空白字符串。对于逻辑值,默认值是假(false)。对于数字类型,默认值是0。对于枚举,默认值是该枚举的类型定义中的第一个值。
当妳定义消息类型时,妳可能希望其中的某个字段只能取某些预定义的值中的一个。例如,假设妳想针对每个SearchRequest 消息添加一个主体(corpus)字段,主体可以是UNIVERSAL、WEB、IMAGES、LOCAL、NEWS、PRODUCTS或VIDEO。要做到这一点狠简单,向妳的消息定义中添加一个枚举就行了——具有枚举类型的字段,它的值只能取预定义的常量集合中的某一个(如果妳试图提供一个超出范围的值,则,解析器会认为它是一个未知字段)。在下面的示例中,我们加入了一个名为Corpus 的枚举,列出了它的所有取值,以及一个类型为 Corpus 的字段:
message SearchRequest {
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3 [default = 10];
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
optional Corpus corpus = 4 [default = UNIVERSAL];
}
妳可以通过将相同的值赋予给不同的枚举常量来定义别名。要想实现这一点,首先要将allow_alias 选项设置为真(true),否则,protocol编译器在遇到别名时会输出一条错误信息。
enum EnumAllowingAlias {
option allow_alias = true;
UNKNOWN = 0;
STARTED = 1;
RUNNING = 1;
}
enum EnumNotAllowingAlias {
UNKNOWN = 0;
STARTED = 1;
// RUNNING = 1; // 如果 妳解除这一行的注释,将会在Google内部引发一条编译器错误,而外在表现是输出一条警告消息。
}
枚举常量的值,必须位于32 位整数的取值范围。由于枚举值在传递过程中使用的是变长编码,所以,负数值的效率较低,因而不建议使用。妳可以在一条消息定义中定义枚举,正如上面示例中那样,也可以定义在消息定义的外面——以这种形式定义的枚举,可在妳的.proto文件中的任意消息定义里使用。妳还可以将一个消息定义中声明的某个枚举类型用作另一个消息定义中某个字段的类型,其语法是MessageType.EnumType。
当妳针对一个使用了枚举的.proto文件运行编译器时,在生成的Java 或C++代码中,会有一个对应的枚举,而在生成的Python 代码中,会有一个特殊的EnumDescriptor 类,用来在运行时生成的类中创建一 组具有整数值的符号常量。
欲知更多关于如何在妳的应用程序中使用消息枚举的信息,则,参考相应语言的生成的代码指南。
妳可以将其它消息类型用作字段类型。例如,假设妳想在每个SearchResponse 消息中包含若干个Result 消息——要想实现这一点,妳可以在同一个.proto文件中定义一个Result 消息类型,然后,在SearchResponse 中将某个字段的类型指定为Result:
message SearchResponse {
repeated Result result = 1;
}
message Result {
required string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
在上面的示例中,Result这个消息类型是与SearchResponse 定义在同一个文件中的——如果妳想用作字段类型的消息类型已经被定义在另一个.proto文件中,怎么办呢?
妳可以通过导入来使用那些位于其它.proto文件中的定义。要想导入另一个.proto的定义,只需要在妳的文件顶部加入一条import 语句:
import "myproject/other_protos.proto";
默认情况下,妳只能使用那些被妳直接导入的.proto文件中的定义。然而,某些时候,妳可能需要将某个.proto文件移动到一个新的位置。这种情况下,妳无需费心地直接移动该.proto文件然后为了这次移动而修改所有的调用代码。妳有更好的解决方法,妳可以向原来的位置放置一个假的.proto文件,然后由它来利用import public 语句将所有的导入动作转发到新的位置。import public这种依赖关系可被任何一个导入了包含import public 语句的proto 文件的文件所利用。例如:
// new.proto
// 所有的定义都移动到了这里
// old.proto
// 这是被所有客户代码导入的那个proto。
import public "new.proto";
import "other.proto";
// client.proto
import "old.proto";
// 妳能够利用old.proto和new.proto中的定义,但是无法利用other.proto中的定义
protocol编译器会在一组目录中搜索导入文件,该目录集合是使用protocol编译器的命令行选项-I/--proto_path指定的。如果未指定该选项,则,它会在编译器被调用的目录中做搜索。通常来讲,妳应当将--proto_path选项设置为妳的项目的根目录,并在导入语句中使用完整的限定名。
可以在妳的proto2 消息中导入proto3 消息类型,反过来也可以。然而,proto2的枚举不可以在proto3 语法中使用。
妳可以在已有的消息类型中定义及使用其它的消息类型,如下所示——在这个示例中,Result消息是被定义在SearchResponse 消息内部的:
message SearchResponse {
message Result {
required string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
repeated Result result = 1;
}
如果妳想在其亲代消息类型外部重用这个消息类型的话,则,可使用Parent.Type 语法:
message SomeOtherMessage {
optional SearchResponse.Result result = 1;
}
只要妳喜欢,可以嵌套任意层消息:
message Outer { // 级别0
message MiddleAA { // 级别1
message Inner { // 级别2
required int64 ival = 1;
optional bool booly = 2;
}
}
message MiddleBB { // 级别1
message Inner { // 级别2
required int32 ival = 1;
optional bool booly = 2;
}
}
}
注意,这个特性已经被废弃,不应当在新创建的消息类型中再使用了——请使用嵌套消息类型作为替代。
分组,是另一种在消息定义中嵌套信息的方式。例如,以下这种方式,也可以表达出,一个SearchResponse 中包含着若干个Result:
message SearchResponse {
repeated group Result = 1 {
required string url = 2;
optional string title = 3;
repeated string snippets = 4;
}
}
分组,其实就是简单地将一个嵌套消息类型和一个字段组合成单个声明。在妳的代码中,可以这样看待这个消息:它拥有一个类型为 Result 的字段,名为result(后者已被转换成小写,以避免与前者冲突)。因此,这个示例与之前示例中的SearchResponse 等价,只是它们在传递过程中的格式不太一样。
如果已有的某个消息类型无法再满足妳的要求了——例如,妳想在该消息格式中再加入一个字段——但是,妳仍然希望使用那些使用旧格式创建的代码,没关系!更新消息类型而不打破已有代码的功能,是狠简单的。只需记住以下规则:
•不要修改已有字段的数字标记。
• 妳加入的任何新字段,都应当是可选的(optional)或重复的(repeated)。这就意味着,使用妳的“旧”消息格式生成的代码所序列化的任何消息,可被新生成的代码解析,因为它们不会缺少必需的(required)元素。妳应当为这些元素设置好有意义的默认值,使得新代码能够正确地处理由旧代码生成的消息。类似地,由新代码创建的消息也能够被旧代码解析:旧的程序在解析时会直接无视掉新的字段。然而,未知的字段不会被丢弃,当日后对消息重新进行序列化时,未知字段也会一并被序列化——这样,如果该消息再次被传递给一个新代码的话,新的字段仍然可被获取。
• 非必需的(Non-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兼容。
•optional与repeated兼容。将某个重复(repeated)字段的序列化数据作为输入,对于那些预期该字段为可选(optional)的客户代码来说,如果该字段的类型是基本类型,则会获取到最后一个输入值,如果该字段的类型是消息类型,则,会将所有输入元素融合到一起。
•改变默认值通常是没有问题的,只是妳需要记住,默认值是不会通过信道传递的。因此,如果一个程序接收到的某条消息中未设置特定的某个字段,则,该程序会看到在该程序对应版本的协议(protocol)中所设置的默认值。它 不会 看到发送者代码中定义的默认值。
扩展的作用是,让妳将某个消息中的一个字段数字范围声明为由第三方扩展使用。这样,其它人就可以在自己的.proto文件中利用这些数字标记来声明新的字段,而无需编辑原始的文件。看下面的示例:
message Foo {
// ...
extensions 100 to 199;
}
这就是说,在Foo 中,字段标记数字范围[100, 199]是保留给扩展使用的。这样,其它用户就可以在自己写的导入了妳的.proto文件的.proto文件中,向Foo 加入新字段,其标记数字处于妳指定的范围内——例如:
extend Foo {
optional int32 bar = 126;
}
这就是说,Foo现在有了一个可选的int32字段,名为bar。
当妳的用户的Foo消息被编码时,信道中所传递的格式,就与用户在Foo 中定义一个新字段的格式完全相同。然而,妳在代码中访问扩展字段的方式与访问常规字段的方式有一点点不同——妳所生成的数据访问代码中,带有特殊的访问函数,用于访问扩展字段。因此,举个例子,妳需要按照下面的代码来在C++中设置bar的值:
Foo foo;
foo.SetExtension(bar, 15);
类似地,Foo类中会定义以下模板访问函数:HasExtension()、ClearExtension()、GetExtension()、MutableExtension()和AddExtension()。它们的语义与相应的普通字段的访问函数的语义相同。欲知更多关于如何处理扩展的信息,则参考妳所选择的语言的生成代码指南。
注意,扩展可以是任意的字段类型,包括消息类型,但是,不可以是oneof或map。
妳可以在另一个类型的作用域内声明扩展:
message Baz {
extend Foo {
optional int32 bar = 126;
}
...
}
在这种情况下,用来访问该扩展的C++代码是:
Foo foo;
foo.SetExtension(Baz::bar, 15);
换句话说,唯一的效果就是,bar被定义在 Baz 的作用域内。
这是一个经常引起困惑的地方:在一个消息类型内部嵌套声明一个extend 块,并不表明,外围类型和被扩展的类型之间有什么关系。具体来说,上面的示例中,并不表明Baz是 Foo 的任何形式的子类。它唯一的意义就是,符号bar被声明于Baz 的作用域之内;它只是一个静态成员。
一种常用的模式是,将扩展定义在扩展的字段类型的作用域内——例如,以下是对于Foo 的一个扩展,其类型为Baz,该扩展被定义为 Baz 的一部分:
message Baz {
extend Foo {
optional Baz foo_ext = 127;
}
...
}
然而,并不要求说具有消息类型的扩展一定要定义在该类型内部。妳也可以这样写:
message Baz {
...
}
// 这甚至可以放置在另一个文件中。
extend Foo {
optional Baz foo_baz_ext = 127;
}
实际上,妳应当优先使用这种语法,以避免困惑。前面已经说过,嵌套语法,经常被那些并不熟悉扩展的用户误认为是子类继承。
有一点狠重要,不能让两个用户使用相同的数字标记来向同一个消息类型中加入不同的扩展——如果扩展被解释为错误的类型,可能会导致数据丢失。妳可能需要考虑为妳的项目定义一个扩展数字使用规定,以避免发生这种意外。
如果在妳的数字使用规定中,扩展可能会使用狠大的数字作为标记,则,妳可以指定,扩展的数字范围将一直延伸到最大的可能字段数字,具体做法就是使用max 关键字:
message Foo {
extensions 1000 to max;
}
max的值是229 - 1,或者说是536870911。
另外,在选择通常的标记数字时,妳的数字使用规定中也应当避免使用从19000 到19999(FieldDescriptor::kFirstReservedNumber到FieldDescriptor::kLastReservedNumber)的字段数字,因为,它们是被Protocol Buffers保留用作代码实现的。妳可以在扩展范围中包含这个范围,但是,protocol编译器不会允许妳定义出任何使用这些数字的实际扩展。
如果妳的某个消息类型中,有狠多个可选(optional)字段,并且,在同一时刻最多只有一个字段被用到,则,妳可以利用oneof 特性来强制启用这种行为,以节省内存。
Oneof字段类似于optional字段,不同之处是,oneof 中的所有字段共享同一砣内存,并且,同一时刻最多只有一个字段可被设置。设置oneof 中的任意成员,都会自动清空其它所有成员。妳可以使用特殊的case()或WhichOneof()方法来检查该oneof 中哪个值被设置了(如果有的话),具体方法名取决于妳所选择的语言。
要想在.proto中定义一个oneof,则,使用oneof关键字,后面跟上妳的oneof名字,在下面示例中是test_oneof:
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
然后,将妳的各个oneof组合字段添加到oneof定义中。妳可以添加任意类型的字段,但是不可以使用required、optional或repeated关键字。
在生成的代码中,对于oneof字段,其获取(getters)和设置(setters)函数与常规可选字段的函数相同。另外还有一个特殊的方法,用于检查,该oneof 中设置了哪个值(如果有的话)。在相应的API 参考中,可找到妳所选择语言的oneof API 的更多信息。
•设置某个oneof字段,会自动清除掉该oneof 中的其它成员。因此,如果妳设置了多个oneof字段,只有最后设置的字段会拥有值。
SampleMessage message;
message.set_name("name");
CHECK(message.has_name());
message.mutable_sub_message(); // 会清除 name字段 。
CHECK(!message.has_name());
•如果解析器从信道传来的数据中解析出了同一个oneof 的多个成员,则,只有最后出现的那个成员会进入解析后的结果消息中。
•oneof不支持扩展。
•oneof不可设置为重复的(repeated)。
•反射APIs对于oneof 字段有效。
•如果妳使用的是C++,请确保,妳的代码不会引起内存崩溃。以下示例代码会崩溃,因为,在调用set_name()方法时,sub_message 已经被删除了。
SampleMessage message;
SubMessage* sub_message = message.mutable_sub_message();
message.set_name("name"); // 会删除 sub_message
sub_message->set_... // 此处会崩溃
•仍然是C++要注意,如果妳交换(Swap())两个带有oneofs 的消息,则,它们最终会持有对方的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 版本的中的某个字段。无法区分这两种情况,因为,无法得知信道中传递过来的某个未知字段是否是该oneof 的一个成员。
标记重用问题
• 将可选( optional )字段移入或移出 oneof :当消息被序列化及解析之后,可能会丢失一些信息(某些字段会被清除)。
• 删除某个oneof字段 ,然后又将它添加回来 :当消息被序列化及解析之后,可能会清除妳当前设置的oneof 字段。
• 拆分 或合并 oneof :会遇到与移动常规可选字段相类似的问题。
如果妳想为妳的数据定义加入一个关联映射(map)的话,protocol buffers提供了一个便利的简洁语法:
map<key_type, value_type> map_field = N;
……其中,key_type可以是任意的整数或字符串(string)类型(也就是说,除了浮点数类型和bytes 之外的任意标量类型)。value_type可以是任意类型。
因此,举个例子,假设妳想要创建一个其值为项目的映射,其中,每个Project消息都与一个字符串键关联,则,可以这样定义:
map<string, Project> projects = 3;
所生成的映射API,当前对于proto2 支持的所有语言都可用。可在相关的API 参考中找到更多关于所选择语言中的映射API 的信息。
•映射中不支持扩展。
•映射不可设置为重复(repeated)、可选(optional)或必需(required)。
•信道格式中的顺序和映射迭代过程中各个值的顺序是未定义的,因此,不应当预期妳的映射条目具有特定顺序。
•在文字格式中,映射是按照键排序的。数字键是按照数字顺序排序的。
在信道格式中,map语法等价于以下语法,因此,在那些不支持映射的protocol buffers 实现中,仍然可以处理妳的数据:
message MapFieldEntry {
key_type key = 1;
value_type value = 2;
}
repeated MapFieldEntry map_field = N;
妳可以向.proto文件中加入一个可选的包标识符,以避免不同的protocol 消息类型之间发生命名冲突。
package foo.bar;
message Open { ... }
妳可以在消息类型中定义字段时使用包标识符:
message Foo {
...
required foo.bar.Open open = 1;
...
}
取决于妳所选择的语言,包标识符会以不同的方式对所生成的代码产生影响:
• 在C++中,生成的类是被包装在一个C++命名空间中。例如,Open会处于foo::bar 命名空间中。
• 在Java中,指定的包会被用作Java包,除非妳在.proto文件中显式地提供一个java_package 选项。
• 在Python中,package指令会被忽略,因为,Python模块是按照它们在文件系统中的位置来组织的。
protocol buffer 语言中的名字解析,是按照C++的方式来进行的:首先搜索最内层的作用域,然后是次内层作用域,如此循环,在此过程中每个包都被认为是其亲代包的“内层”。以'.'开头(例如,.foo.bar.Baz)则表示最从外层的作用域开始。
protocol buffer编译器通过解析那些被导入的.proto文件来解析所有的类型名字。针对各种语言的代码生成器知道如何引用该语言中的各个类型,即便它拥有不同的作用域规则也是如此。
如果妳想要在某个远程进程调用(RPC (Remote Procedure Call))系统中使用妳的消息类型,则,可在.proto文件中定义一个RPC 服务接口,protocol buffer编译器会按照妳所选择的语言生成服务接口代码及根(stubs)实现。因此,举个例子,假设妳要定义一个RPC服务,其中有一个方法,其输入为一个SearchRequest,输出为一个SearchResponse,那么,可在.proto中像这样定义:
service SearchService {
rpc Search (SearchRequest) returns (SearchResponse);
}
默认情况下,protocol编译器会相应地生成一个抽象接口,名为SearchService,以及一个对应的“根”("stub")实现。这个根,会将所有的调用都转发给一个RpcChannel,而后者又是一个抽象接口,妳必须根据妳自己的RPC 系统的情况来定义它。例如,在妳的RpcChannel 实现中,可能会将消息序列化,然后通过HTTP 发送给一个服务器。换句话说,所生成的根,提供了一个类型安全的接口,可用于发起基于protocol-buffer 的RPC 调用,却不会将妳捆绑在任何特定的RPC 实现上。因此,对于C++,最终的代码可能是这样的:
using google::protobuf;
protobuf::RpcChannel* channel;
protobuf::RpcController* controller;
SearchService* service;
SearchRequest request;
SearchResponse response;
void DoSearch() {
// 妳提供类 MyRpcChannel 和 MyRpcController ,它们实现了抽象接口
// protobuf::RpcChannel 和 protobuf::RpcController 。
channel = new MyRpcChannel("somehost.example.com:1234");
controller = new MyRpcController;
// protocol编译 器基于前面给出的定义来生成 SearchService 类。
service = new SearchService::Stub(channel);
// 设置 好请求中的属性。
request.set_query("protocol buffers");
// 执行RPC 。
service->Search(controller, request, response, protobuf::NewCallback(&Done));
}
void Done() {
delete service;
delete channel;
delete controller;
}
所有的服务类都实现了Service接口,这样,就提供了一种手段,可以在编译时刻不知道方法名和输入输出类型的情况下调用特定的方法。在服务器端,可用来实现一个RPC 服务器,并且向它注册服务。
using google::protobuf;
class ExampleSearchService : public SearchService {
public:
void Search(protobuf::RpcController* controller,
const SearchRequest* request,
SearchResponse* response,
protobuf::Closure* done) {
if (request->query() == "google") {
response->add_result()->set_url("http://www.google.com");
} else if (request->query() == "protocol buffers") {
response->add_result()->set_url("http://protobuf.googlecode.com");
}
done->Run();
}
};
int main() {
// 妳提供类 MyRpcServer 。 它不需要实现任何特定的接口;
// 这只是一个例子。
MyRpcServer server;
protobuf::Service* service = new ExampleSearchService;
server.ExportOnPort(1234, service);
server.Run();
delete service;
return 0;
}
如果妳不想插入自己已有的RPC系统,那么,妳可以使用gRPC:Google 内部开发的一个与语言无关、与平台无关的开源RPC 系统。gRPC与protocol buffers 完美配合,并且,妳可以使用一个特殊的protocol buffer 编译器插件来从.proto文件中直接生成相关的RPC 代码。然而,由于使用proto2 和proto3 生成的客户端、服务器端代码之间有潜在的兼容性问题,因此,我们建议妳使用proto3来定义gRPC服务。妳可在Proto3语言指南找到更多关于proto3 的语法说明。如果妳确实想要在proto2 中使用gRPC,那么,妳需要使用3.0.0 或更高版本的protocol buffers 编译器和库。
除了gRPC之外,还有若干正在开发的第三方项目,它们的目的都是为Protocol Buffers 开发RPC 实现。要想找到我们已知的项目的链接列表,则,参考第三方扩展维基页面。
在.proto文件中,可使用若干选项来对单个声明进行注解。选项不会改变该声明的整体意义,但是,在特定上下文环境下可能会影响到它被处理的方式。可用的选项的完整列表,位于google/protobuf/descriptor.proto 中。
某些选项是文件级别的选项,意味着,它们应当被写在顶级作用域,而不是写在任何消息、枚举或服务定义内部。某些选项是消息级别的选项,意味着,它们应当被写在消息定义内部。某些选项是字段级别的选项,意味着,它们应当被写在字段定义内部。选项也可针对枚举类型、枚举值、服务类型和服务方法而编写;然而,目前不存在针对这四种元素的有用的选项。
以下是最常见的有用的选项:
•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";
•optimize_for (文件选项): 可选值为SPEED、CODE_SIZE或LITE_RUNTIME。它会以以下方式影响C++和Java代码生成器(可能也会影响第三方生成器):
◦SPEED (默认值): protocol buffer编译器会针对妳的消息类型生成用于序列化、解析和进行其它常见操作的代码。所生成的代码是经过变态优化的。
◦CODE_SIZE: protocol buffer编译器会生成最小化的类,并且依赖共享的、基于反射技术的代码来实现序列化、解析和其它各种操作。因而,所生成的代码相对于SPEED 来说要小得多,但是其操作过程会慢得多。各个类,仍然会实现与SPEED 模式完全相同的公有API。这种模式适用于具有以下特点的应用程序:包含着狠多的.proto文件,而又并不要求它们全部都工作得狠快。
◦LITE_RUNTIME: protocol buffer编译器会生成只依赖于"lite"运行时库(libprotobuf-lite,而不是libprotobuf)的类。轻量级(lite)运行时,比完整库要小得多(大约要小一个数量级)但是省略了特定的特性,例如描述器和反射。这对于运行于资源有限平台的应用程序来说狠有用,例如手机。编译器仍然会像SPEED 模式一样为所有的方法生成快速的实现代码。所生成的类,只会在每种语言中实现MessageLite接口,该接口相对于完整的Message 接口只提供了一个方法子集。
option optimize_for = CODE_SIZE;
•cc_generic_services、java_generic_services、py_generic_services (文件选项): protocol buffer 编译器是否应当根据C++、Java 和Python 中的服务定义来生成抽象的服务代码。出于历史原因,这些选项的默认值为真(true)。然而,自从版本2.3.0 (2010年1月)开始,我们认为, 应当优先由RPC实现系统来提供代码生成插件,以生成与该系统关联度更高的代码,而不是依赖“抽象的”服务。
// 这个文件依赖着插件来生成服务代码。
option cc_generic_services = false;
option java_generic_services = false;
option py_generic_services = false;
•cc_enable_arenas (文件选项): 对于生成的C++代码,启用区域分配。
•message_set_wire_format (消息选项): 如果设置为真(true),则,该消息会使用一个不同的二进制格式,其目的是与Google 内部使用的一种叫做MessageSet 的旧格式相兼容。Google以外的用户应当无需使用这种选项。消息必须完全按照以下形式来声明:
message Foo {
option message_set_wire_format = true;
extensions 4 to max;
}
•packed (字段选项): 如果针对一个重复(repeated)字段或基本整数类型字段将这个选项设置为真(true),则,会使用一个更紧缩的编码方式。使用这个选项不会带来任何负作用。然而,请注意,对于2.3.0 之前的版本,那些在未预料到的情况下接收到紧缩(packed)数据的解析器,会忽略它。因此,无法在不打破信道数据兼容性的情况下将一个已有的字段改变成紧缩格式。在2.3.0及更高的版本中,这种改变是安全的,针对可紧缩字段的解析器,必定能够处理好这两种格式,但是,当妳需要与以旧版本protobuf 开发的旧程序打交道时,请小心行事。
repeated int32 samples = 4 [packed=true];
•deprecated (字段选项): 如果设置为真(true),其意义为,此字段已被废弃,不应当在新代码中使用。在大部分语言中,这个选项没有实际效果。在Java中,它会变成一个@Deprecated注解。日后,其它语言的代码生成器,可能也会针对该字段的访问函数生成废弃注解,这样,在编译那些尝试使用该字段的代码时,会产生警告。
optional int32 old_field = 6 [deprecated=true];
Protocol Buffers甚至允许妳定义及使用妳自己的选项。注意,这是一个高端特性,大部分人都用不上。由于选项是由 google/protobuf/descriptor.proto 中的消息来定义的(例如FileOptions或FieldOptions),所以,定义自己的选项,实际上狠简单,就是扩展那些消息。例如:
import "google/protobuf/descriptor.proto";
extend google.protobuf.MessageOptions {
optional string my_option = 51234;
}
message MyMessage {
option (my_option) = "Hello world!";
}
此处,我们通过MessageOptions 扩展来定义了一个新的消息级别的选项。日后,当我们使用该选项的时候,要使用括号将它包围起来,以表明它是一个扩展。我们可以在C++中使用以下的代码来读取的 my_option 值:
string value = MyMessage::descriptor()->options().GetExtension(my_option);
此处,MyMessage::descriptor()->options()返回的是MyMessage 中的那个MessageOptions 消息。要 从其中读取自定义的选项,其操作就跟读取任意其它扩展一样。
类似地,在Java中就是这样写:
String value = MyProtoFile.MyMessage.getDescriptor().getOptions()
.getExtension(MyProtoFile.myOption);
在Python中就是:
value = my_proto_file_pb2.MyMessage.DESCRIPTOR.GetOptions()
.Extensions[my_proto_file_pb2.my_option]
在Protocol Buffers 语言中,可针对所有类型的结构定义选项。以下示例中,使用了所有类型的选项:
import "google/protobuf/descriptor.proto";
extend google.protobuf.FileOptions {
optional string my_file_option = 50000;
}
extend google.protobuf.MessageOptions {
optional int32 my_message_option = 50001;
}
extend google.protobuf.FieldOptions {
optional float my_field_option = 50002;
}
extend google.protobuf.EnumOptions {
optional bool my_enum_option = 50003;
}
extend google.protobuf.EnumValueOptions {
optional uint32 my_enum_value_option = 50004;
}
extend google.protobuf.ServiceOptions {
optional MyEnum my_service_option = 50005;
}
extend google.protobuf.MethodOptions {
optional MyMessage my_method_option = 50006;
}
option (my_file_option) = "Hello world!";
message MyMessage {
option (my_message_option) = 1234;
optional int32 foo = 1 [(my_field_option) = 4.5];
optional string bar = 2;
}
enum MyEnum {
option (my_enum_option) = true;
FOO = 1 [(my_enum_value_option) = 321];
BAR = 2;
}
message RequestType {}
message ResponseType {}
service MyService {
option (my_service_option) = FOO;
rpc MyMethod(RequestType) returns(ResponseType) {
// 注意 : my_method_option 的类型是 MyMessage 。 我们可以使用单独的"option"代码行来为其中的每个字段设置值。
option (my_method_option).foo = 567;
option (my_method_option).bar = "Some string";
}
}
注意,如果妳想使用一个在别的包中定义的自定义选项,那么,妳必须在选项名前面加上包名前缀,这就像一般的类型名一样。例如:
// foo.proto
import "google/protobuf/descriptor.proto";
package foo;
extend google.protobuf.MessageOptions {
optional string my_option = 51234;
}
// bar.proto
import "foo.proto";
package bar;
message MyMessage {
option (foo.my_option) = "Hello world!";
}
最后一件事:由于自定义选项本质上是扩展,所以,必须像任何其它的字段或扩展一样赋予字段数字。在上面的示例中,我们使用的字段数字的范围是50000-99999。这个范围是保留用于让独立的组织内部使用的,所以,如果妳的开发的软件不会对外发布,那么可以自由地使用这个范围。如果妳想在公开发布的应用程序中使用自定义选项,那么,就有一个狠重要的注意事项,妳必须确保妳所使用的字段数字是全球唯一的。要想获取到全球唯一的字段数字,请向protobuf-global-extension-registry@google.com 发送一个请求。只需提供两个信息:妳的项目名字和妳的项目网站(如果有的话)。通常妳只需要一个扩展数字。妳可以仅使用一个扩展数字来声明多个选项,只需将它们放置到一个子消息中去就可以了:
message FooOptions {
optional int32 opt1 = 1;
optional string opt2 = 2;
}
extend google.protobuf.FieldOptions {
optional FooOptions foo_options = 1234;
}
// 用法:
message Bar {
optional int32 a = 1 [(foo_options).opt1 = 123, (foo_options).opt2 = "baz"];
// 或者 用一种聚合语法 (利用TextFormat) :
optional int32 b = 2 [(foo_options) = { opt1: 123 opt2: "baz" }];
}
另外,请注意,每种类型的选项(文件级别、消息级别、字段级别等等)都有它自己的数字空间,因此,举个例子,妳可以使用相同的数字来声明针对FieldOptions 和MessageOptions 的扩展。
要想生成针对.proto文件中定义的消息类型的Java、Python或C++代码,妳需要对该.proto文件运行protocol buffer 编译器protoc。如果妳仍未安装该编译器,那么,下载它,然后按照README 中的说 明来安装。
Protocol编译器应当按照如下方式运行:
protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR path/to/file.proto
•IMPORT_PATH ,指定的是,在解析导入指令时,要在其中搜索.proto文件的目录。如果省略,则会使用当前目录。可多次指定--proto_path选项,以提供多个导入目录;它们会被按照指定的顺序来搜索。可使用-I=IMPORT_PATH来作为--proto_path的缩写形式。
•妳可以指定一到多个输出指令:
◦--cpp_out ,在DST_DIR 中生成C++代码。参考C++生成的代码参考以了解更多信息。
◦--java_out ,在DST_DIR 中生成Java 代码。参考Java生成的代码参考以了解更多信息。
◦--python_out ,在DST_DIR 中生成Python 代码。参考Python 生成的代码参考以了解更多信息。
还有一个额外的便利特性,如果DST_DIR以.zip或.jar结尾,则,编译器会将输出内容放置到一个单个的ZIP格式压缩文件中,并给予指定的文件名。对于.jar的输出文件,也会按照Java JAR 规范给予一个清单文件。注意,如果输出的压缩文件已经存在,那么,会被覆盖掉;编译还未智能到能够向已有压缩包中加入文件的程度。
• 妳必须提供一个或多个.proto文件作为输入。可一次指定多个.proto文件。尽管那些文件都是以相对于当前目录的形式来指定的,但是,每个文件都必须位于某个IMPORT_PATH中,这样,编译器才能确定它们的规范名字。
通天狄仁杰
苍井空
八卦
界面原型设计
我同意
双屏干活才有效率
好天气
语音计分工具
谷歌地图
2011沙滩音乐节
Your opinionsHxLauncher: Launch Android applications by voice commands