欢迎来到protocol buffer开发文档。protocol buffers是一个语言无关、平台无关、序列化结构数据可扩展的用来协议交互、数据存储等的解决方案。
本文档的目标受众是那些想要在他们的应用中使用protocol buffer的Java,C++或者Python开发者。这个总览将介绍protocol buffers并告诉你如何准备和开始——然后你可以去看学习指南或者深入研究protocol buffer编码。上述三种语言的API相关文档、还有写.proto文件的编程风格引导有相应的提供。
什么是protocol buffers?
Protocol Buffers是一个可变、高效、自动的将结构体数据序列化的机制——回想一下XML吧,但它又更小、更快速、更简单。你只要把数据按你的需要定义成结构体,就能使用特殊生成的代码,用各种语言从各种数据流中读写的数据结构。你甚至可以更新你的数据结构而不用重新部署那些用老的格式编译出来的程序。
如何工作?
你可以在.proto文件中定义protocol buffer消息类型来指定如何将你的序列化数据结构化。每个protocol buffer消息是一个小的信息的逻辑记录,包含了一系列的(名称—值)对。这是一个很基础的.proto文件示例,它定义了一个包含一个人信息的消息:message Person {
required string name = 1;
required int32 id = 2;
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
required string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phone = 4;
}
如你所见,消息的格式很简单——每个消息类型有一个或多个唯一的带数值字段,每个字段都是一个名称和一个数据类型,数据类型可以是各种数字(整数或浮点数),布尔值、字符串、原始字节,甚至是其他的protocol buffer消息类型(如示例所示)。你可以指定可选字段、必须字段,还有重复字段。在Protocol Buffer语言指引里面你可以找到更多关于如何写.proto文件的信息。
一旦你定义了所需要的消息,你就可以运行protocol buffer编译器,它会基于你写的.proto文件为你的应用程序的语言生成相应的类来操作数据。这里提供一个简单的操作类,它可以从原始字节里序列化或解析每个字段(像query()和set_query())和函数的结构¬¬——例如,对前面的示例,如果你选择用C++来运行编译器,那它会生成一个叫Person的类。然后你就可以在你的应用程序里使用这个类来填充、序列化和还原这个Person的protocol buffer消息。你的代码可以这样写:Person person;
person.set_name("John Doe");
person.set_id(1234);
person.set_email("[email protected]");
fstream output("myfile", ios::out | ios::binary);
person.SerializeToOstream(&output);
然后你可以这样来读回你的消息:fstream input("myfile", ios::in | ios::binary);
Person person;
person.ParseFromIstream(&input);
cout << "Name: " << person.name() << endl;
cout << "E-mail: " << person.email() << endl;
你可以在你的消息格式中添加新的字段而不用担心向后兼容问题;老的二进制文件在解析的时候会简单地忽略掉新增的字段。所以如果你使用protocol buffers来作为你交互协议的数据格式,你就可以扩展协议而无需担心破坏现有的代码。
在API 参考一节里,你可以找到完整的使用生成的protocol buffer代码的参考资料,并且可以在Protocol Buffer编码。
为什么不用XML就好了?
在序列化结构体数据上,Protocol buffer 相比XML有很多优势。Protocol buffer:
更加简单
小了3到10倍
快了20到100倍
更少歧义
生成的数据操作类更容易编程
例如,我们说你想为一个有姓名和邮件的人来建模,用XML的话,你需要这样写:John Doe
[email protected]
而相应的protocol buffer消息(用protocol buffer文本协议)是这样:# Textual representation of a protocol buffer.
# This is *not* the binary format used on the wire.
person {
name: "John Doe"
email: "[email protected]"
}
当这个消息编码成protocol buffer的二进制格式(上面说的文本格式只是一个方便用来debug和编辑可读表示),它可能只有28 字节大小且只需大约100-200纳秒来解析。XML的版本就算你去掉空格也要至少69 字节,并且要花大约5000-10000纳秒来解析。
同时,操作protocol buffer也更简单:cout << "Name: " << person.name() << endl;
cout << "E-mail: " << person.email() << endl;
而使用XML你必须得这样:cout << "Name: "
<< person.getElementsByTagName("name")->item(0)->innerText()
<< endl;
cout << "E-mail: "
<< person.getElementsByTagName("email")->item(0)->innerText()
<< endl;
然而,protocol buffer相比XML也不一定总是更好的解决方案,例如protocol buffer对有标记的文本(例如HTML)不是个建模的好方法,因为你无法容易地将文本和结构交错在一起。另外,XML具有良好的可读性和可编辑性;而protocol buffer,至少在自然格式上不是。XML在某种程度上可以自我描述。protocol buffer只有在你定义了消息(.proto文件)的时候才有意义。
听起来是个不错的解决方案!怎么开始呢?
下载安装包——它包含了Java、Python和C++的protocol buffer编译器的完整源代码及其相关的I/O和测试类。请按照README的指示来生成和安装你的编译器。
当你完成所有事情后,试着看你需要的语言的学习指南,它会逐步指引你用protocol buffer创建一个简单的应用程序。
一点历史:
protocol buffer最初在Google开发来处理一个索引服务器的收发请求协议。在protocol buffer之前,有个用来处理收发请求的格式,它支持组编和非组编的请求和回包,并且支持很多的版本。这造成了一些挺难看的代码,如:if (version == 3) {
...
} else if (version > 4) {
if (version == 5) {
...
}
...
}
由于开发者不得不在发起请求和真正处理请求的服务器之间的所有服务器在使用新的协议之前确保它们都能解析这个新的协议。
protocol buffer被设计来解决很多这样的问题:
新的字段可以容易地被引入,并且中间服务器没必要去细察数据,能简单地解析它、传输它但无需知道所有的字段。
格式更加具有自我描述性,可以兼容多种语言(C++,Java等)。
然而,使用者仍然需要手写自己的解析代码。
随着系统进化,它需要一些其它的特征和用法:
自动生成的序列化和还原序列化代码来避免需要手工解析
除了被用来处理短周期的PRC(远程程序调用)请求,人们开始使用protocol buffer作为简便的永久保存数据的一种自我描述格式。
PRC服务器接口开始被定义成协议文件的一部分,和协议编译器一起生成桩类,使用者可以重写它来实现真正的服务器接口。
protocol buffer现在是Google处理数据的通用语言,在写这篇介绍的时候,48162个不同的消息类型已经被定义在Google代码树的12183个.proto文件里面。它们被用在PRC系统中以及在各种各样的存储系统中来保存数据。
语言指引
定义消息类型
标识数值类型
可选类型和默认值
枚举
使用其他消息类型
内嵌类型
更新消息类型
扩展
包
定义服务
选项
生成你自己的类
本指引将描述如何使用protocol buffer语言来结构化你的protocol buffer数据,包括.proto文件语法以及从你的.proto文件生成数据操作类。
这是一个参考指引,在学习指南中你可以选择特定的语言,那里会有例子逐步介绍如何使用本文档中描述到的许多特性。
定义消息类型
首先让我们来看一下一个非常简单的例子。假设你想要定义一个搜索请求的消息格式,每个搜索请求会有一个查询语句,你想要的查询结果页数,以及每页的结果数量。这是一个用来定义这个消息类型的.proto文件:
message SearchRequest {
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3;
}
这个查询请求消息定义了三个字段,每个字段都有一个名称和类型。
指定字段类型
上面的例子中,所有字段都标识了类型:两个整数形(page_number和result_per_page)还有一个字符串(query)。然而你还可以制定组合类型的字段,包括枚举和其他消息类型。
分配标签
如你所见,消息定义中的每个字段都有一个独一无二的数值标签。这些标签被用来在消息二进制格式中区分你的字段,并且一旦你的消息类型开始使用,就不应该再改变了。注意标签的值在1到15之间将占据一个字节来编码,包括了这个区分数值和字段类型(在Protocol Buffer编码中可以找到更多关于这个的信息)。标签值在16到2047间的占用两个字节。所以你必须保留值在1到15的标签给那些出现得最频繁的消息元素使用。记得保留更多空间给将来可能添加进来的那些会频繁使用的元素。
你可以指定的最小的标签值是1,最大的是229-1,也就是536,870,991。不能使用19000-19999之间的标签值(FieldDescriptor::kFirstReservedNumber到FirldDescriptor::kLastReservedNumber)因为它们是protocol buffer实现中的保留值,如果你在.proto文件中使用这些保留值,protocol buffer编译器将会报错。
指定字段规则
你可以指定以下消息字段中的一种:
required:一个格式规范的消息必须恰好有一个这样的字段。
optional:一个格式规范的消息可以有0个或1个这样的字段(但不超过1个)。
repeated:一个格式规范的消息里这个字段可以重复任意次(包括0次)。这些重复值的顺序将被保留。
由于历史原因,基本数值类型的repeated字段没有被编码得如它们本可以达到的高效。新代码应该使用特别选项[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 {
...
}
添加注释
使用C/C++风格的//语法为你的.proto文件添加注释:message SearchRequest {
required string query = 1;
optional int32 page_number = 2;// Which page number do we want?
optional int32 result_per_page = 3;// Number of results to return per page.
}
你的.proto文件将生成些什么?
当你对一个.proto文件运行protocol buffer编译器的时候,编译器将根据你在文件中描述的消息类型生成你所选语言的代码,包括设定和读取字段值、把你的消息序列化成输出流、以及从输入流中解析你的消息。
对C++而言,编译器生为你的每个.proto文件生成成一个.h和.cpp文件,你所定义的每个消息类型对应一个类。
对Java而言,编译器生成一个.java文件,每个消息类型对应一个类,另外还有一个特定的Builder类来创建消息类实例。
python则有些不同,python的编译器为.proto文件中的每一个消息类型生成一个有静态描述符的模块,这个描述符和一个metaclass一起用来在运行时创建必要的python数据操作类。
你可以在你所选语言的学习指南中找到更多关于每个语言的API用法。更多更详细的API细节,请参见相关的API参考。
标识数值类型
消息标识字段可以是下面类型中的一种——这个表展示了所有在.proto文件中指定的类型及其在相应的自动生成类中的类型:.proto 类型 注意 C++ 类型 Java 类型
double double double
float float float
int32 使用可变长度编码。对负数编码的时候会相对低效,所以如果你的字段值有可能是负数,请使用sint32。 int32 int
int64 使用可变长编码。对负数编码的时候会相对低效,所以如果你的字段值有可能是负数,请使用sint64。 int64 long
uint32 使用可变长编码。 uint32 int[1]
uint64 使用可变长编码。 uint64 long[1]
sint32 使用可变长编码。带符号的int数值,它编码负数的时候比常规的int32s更加高效。 int32 int
sint64 使用可变长编码。带符号的int数值,它编码负数的时候比常规的int64s更加高效。 int64 long
fixed32 固定4个字节。若数值经常大于228时比uint32更加高效。 uint32 int[1]
fixed64 固定8个字节。若数值经常大于256时比uint64更加高效。 uint64 long[1]
sfixed32 固定4个字节。 int32 int
sfixed64 固定8个字节。 int64 long
bool bool boolean
string 字符串必须总包含utf8编码或7位的ASCII文本。 string String
bytes 可以包含任意顺序的字节。 string ByteString
你可以在Protocol Buffer编码中找到更多关于这些类型在你序列化消息时如何被编码的细节。
在Java中,无符号32位和64位整数用它们带符号的计数器部分来表示,最高位简单地被用来保存正负号。
可选字段和默认值
如上面提到的,消息中描述的元素可以被标识为optional。一个格式规范的消息可以包含或不包含可选字段。当消息被解析时,如果它没有包含可选元素,类中相应的字段将被设定成默认值。默认值可以在消息描述中指定。例如,假设你想要为SearchRequest的result_per_page字段设定默认值10:
optional int32 result_per_page = 3 [default = 10];
如果可选元素没有指定默认值,那将使用该类型的默认值:string类型会是个空字符串,bool类型会是false,数值类型会是0,枚举类型是枚举定义中的第一个值。
枚举
当你在定义消息类型的时候,你可能想要把其中的一个字段的值设为预定义好的值中的一个。例如,假设你想要在每个SearchRequest中添加一个corpus字段,这个corpus可以是普通事物、网站、图像、新闻、产品或者视频。你可以在消息定义中添加enum就能很简单地实现——一个enum类型的字段的值只能是规定的常数集合中的一个(如果你尝试提供一个不同值,解析器会把它当作未知字段)。接下来的例子我们将添加一个叫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];
}
枚举的值域必须在32位整形的范围内。因为枚举值在线路上使用varint编码,负值比较低效所以并不推荐。你可以在消息中定义枚举类型,就像上面的例子一样,或者在外面¬——这些枚举可以被你的.proto文件中的任意消息重用。你还可以使用一个消息中定义的枚举类型作为其他消息的字段类型,用MessageType.EnumType就可以了。
当你在一个使用了枚举类型的.proto文件上运行protocol buffer编译器的时候,生成的代码会有相应的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文件中定义了该怎么办呢?
使用import就可以使用其他.proto文件中的定义了。要导入其他.proto文件的定义,你需要在文件顶部添加import语句:
import "myproject/other_protos.proto";
协议编译器在一系列使用协议编译命令行-I/--proto_path标识指定的目录中搜索导入文件。如果没有给定标识,它将在编译器被调用的当前目录下搜索。通常你应该用—proto_path标识来设置你项目的根节点,并对所有的导入使用规范的名称。
内嵌类型
你可以定义和使用在其他消息类型里面的消息类型,例如下面的例子——这里的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 { // Level 0
message MiddleAA { // Level 1
message Inner { // Level 2
required int64 ival = 1;
optional bool booly = 2;
}
}
message MiddleBB { // Level 1
message Inner { // Level 2
required int32 ival = 1;
optional bool booly = 2;
}
}
}
群组
注意这个特性已经被弃用并且在创建新的消息类型的时候不应该使用——请使用内嵌消息类型取而代之。
群组是另一个在你的消息定义里面内嵌信息的方式。例如,另一个指定一个包含了多个Result的SearchResponse方式如下:message SearchResponse {
repeated group Result = 1 {
required string url = 2;
optional string title = 3;
repeated string snippets = 4;
}
}
群组简单组合了一个内嵌消息类型和一个字段来作为一个独立的声明。在你的代码里,你可以把它当作它有一个叫做result的Result类型字段(前者被转换成小写的以免和后者冲突)。因此,这个例子确实等价于上面的SearchResponse,除了消息有不同的线路格式。
更新消息类型
如果现有的消息类型已经不能满足你的需求——例如你想要在消息格式中加一个额外的字段——但你又还想用之前格式创建出来的代码,别急!在不破坏已有代码的前提下更新消息类型非常简单。只要记住下面的规则:
不要改变任何已有字段的数值标签。
你所新增的任何字段必须是optional或者repeated的。这以为这任何被使用老的消息格式的代码序列化的消息都能被新生成的代码解析,因为它们不会丢失任何的required元素。你得为这些元素设置合理的默认值,这样新代码能正确地和旧代码交互。相似的,新代码生成的消息也能被老代码解析:老的二进制解析的时候简单地忽略新的字段。然和,这些未知的字段也不会被丢弃,如果消息在之后又被序列化,未知的字段会跟着一起被序列化——所以如果消息被传递给新的代码,新的字段仍然可用。注意未知字段的保留目前仍不支持Python。
非required字段可以被删除,只要它的数值标签不再在你更新的消息类型里面使用(最好把字段也重命名了,例如添加“OBSOLETE_”前缀,这样将来其他使用你的.proto文件的人不会偶然地重用这个数值)。
一个非required字段可以被转换成扩展,反之亦然,只要类型和数值保持不变。
int32,uint32,int64,uint64和bool都是可兼容的——这意味着你可以把一个字段的类型从其中的任意一个转成其它的而不会破坏向前和向后兼容性。如果一个数值被一条不支持该类型的线路解析,那你会得到像你在C++中把它强制转换成那个类型的同样的效果。(例如,如果一个64位数值被当成一个32位的来读,那它会被截取出32位)。
sint32和sint64彼此兼容但不和其他整形类型兼容。
只要bytes是可用的utf8格式,string和bytes就能兼容。
只要bytes包含编码的消息版本,内嵌消息和bytes就能兼容。
fixed32和sfixed32兼容,fixed64和sfixed64兼容。
optional和repeated兼容。给出序列化的一个repeated字段作为输入,对于把这个字段当成optional的客户端,如果它是一个基本类型字段则取最后一个输入值,如果它是一个消息类型字段则合并所有的输入元素。
改变默认值通常是没问题的,只要你记住默认值不会通过线路传输。因此,如果一个程序收到一个某个字段没有设定值的消息,它会去查那个程序的该版本协议中定义的默认值。默认值不会出现在发送的代码里面。
扩展
扩展使你可以在消息中声明一定范围的字段数值给第三方扩展使用。其他人可以用这些数值标签在他们自己的.proto文件中声明新的字段而不必编辑原有的文件。我们来看个例子吧:message Foo {
// ...
extensions 100 to 199;
}
它的意思是Foo中,范围在[100, 199]间的字段值被保留来扩展。其他的用户可以在他们自己的.proto文件中import你的.proto然后为Foo增加新的字段,使用你指定的标签范围——例如:extend Foo {
optional int32 bar = 126;
}
这样Foo就有可一个optional int32的字段,叫做bar。
当Foo消息被编码的时候,线路格式和用户在Foo里定义了新的字段完全一样。然而,你在应用程序代码里面操作扩展字段的方法和正常的操作字段的方法有些不同——你生成的数据操作代码里面有特殊的扩展操作方法。所以,例如,在C++里面设置bar值的方法是这样的:Foo foo;
foo.SetExtension(bar, 15);
相似的,Foo类定义了模版操作函数HasExtension(),ClearExtension(),GetExtension(),MutableExtension()以及AddExtension()。所有这些都和一个普通字段一样有符合语义的相关生成的操作方法。更多关于使用扩展的信息,可以参见你所选语言的代码生成参考。
注意扩展可以是任意字段类型,包括消息类型。
内嵌扩展
你可以在任何类型的作用域内声明扩展:message Baz {
extend Foo {
optional int32 bar = 126;
}
...
}
因此,在C++代码中操作这个扩展是这样:Foo foo;
foo.SetExtension(Baz::bar, 15);
换句话说,唯一的结果是bar被定义在Baz的作用域里面。
这是一个常见的困惑来源:在消息类型里声明一个extend块并不意味着外在类型和扩展类型之间有任何的关系。尤其是,上面的例子不意味着Baz是Foo的子类。它只是意味着符号bar是在Baz的作用域里声明的,它只是一个简单的静态变量。
一个常见的模式是在扩展字段类型的作用域里定义扩展——例如,这是一个Baz类型的Foo扩展,它并定义成Baz的一部分:message Baz {
extend Foo {
optional Baz foo_ext = 127;
}
...
}
然而,并没有要求消息类型的扩展必须被定义在那个类型里面。你还可以这样做:message Baz {
...
}
// This can even be in a different file.
extend Foo {
optional Baz foo_baz_ext = 127;
}
事实上,这个语法对于避免困惑会更有帮助。如上所述,对于那些还不是很熟悉扩展的人来说,内嵌语法经常被误以为是子类。
选择扩展数值
确保两个用户不对相同的消息类型添加相同数值的标签非常重要——数据讹误会造成扩展偶然被解读成错误类型。你可能要考虑为你的项目定义一个扩展数值公约来避免这种事。
如果你的数值公约可能包含带较大数值标签的扩展,你可以使用max关键字来为扩展的范围指定最大可能的字段值。message Foo {
extensions 1000 to max;
}
max的值是229 – 1,也就是536,870,911。
包
你可以为.proto文件添加可选的package说明符来防止协议消息类型间的命名冲突。package foo.bar;
message Open { ... }
你可以用在你的消息类型定义字段的时候使用包说明符:message Foo {
...
required foo.bar.Open open = 1;
...
}
包说明符对生成代码的影响方式取决于你所选的语言:
在C++生成类被打包在C++的命名空间里。例如,Open会在命名空间foo::bar里面。
在Java中,包的使用方式跟Java的包一样,触发你显式地在你的.proto文件里提供一个optional java_package。
在Python里,包直接被忽略了,因为Python模块是按照它们在文件系统里的位置组织的。
包和命名方案
在protocol buffer里类型命名的解决方案跟C++相似:首先搜索最内层的作用域,然后次最内层,以此类推,每个包被认为是其父包的内层。用”.”开头(例如:.foo.bar.Baz)则意味着从最外层的作用域开始。
protocol buffer编译器通过解析导入的.proto文件来解决所有的类型命名。各种语言的代码生成器知道如何引用其他类型,即使是在不同的作用域规则中。
定义服务
如果想在PRC(远程程序调用)系统中使用消息类型,你可以在.proto文件里定义一个PRC服务接口,protocol buffer编译器会生成服务接口代码以及你所选语言的桩类。所以,例如你想定义一个带传入SearchRequest,返回SearchResponse的方法的PRC服务,你可以在你的.proto文件中这样定义:service SearchService {
rpc Search (SearchRequest) returns (SearchResponse);
}
协议编译器会生成抽象接口SearchService和相应的桩类实现。这个桩类会把所有请求提交到RpcChannel,这又是一个你得在你的PRC系统中定义的抽象接口。例如,你可能实现了一个能序列化消息并且把它通过HTTP发送到服务器的RpcChannel。换句话说,生成的积累为基于protocol buffer的PRC请求提供了一个类型安全的接口,而不会把你限制在某个特定的PRC实现中。所以,在C++中,你可能会最终写出这样的代码:using google::protobuf;
protobuf::RpcChannel* channel;
protobuf::RpcController* controller;
SearchService* service;
SearchRequest request;
SearchResponse response;
void DoSearch() {
// You provide classes MyRpcChannel and MyRpcController, which implement
// the abstract interfaces protobuf::RpcChannel and protobuf::RpcController.
channel = new MyRpcChannel("somehost.example.com:1234");
controller = new MyRpcController;
// The protocol compiler generates the SearchService class based on the
// definition given above.
service = new SearchService::Stub(channel);
// Set up the request.
request.set_query("protocol buffers");
// Execute the RPC.
service->Search(controller, request, response, protobuf::NewCallback(&Done));
}
void Done() {
delete service;
delete channel;
delete controller;
}
所有服务类都实现了Service接口,它提供了不需要知道函数名、输入输出类型就能在编译时调用指定函数的方式。在服务器这边,它可以用来实现PRC服务器然后用她来注册服务。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() {
// You provide class MyRpcServer. It does not have to implement any
// particular interface; this is just an example.
MyRpcServer server;
protobuf::Service* service = new ExampleSearchService;
server.ExportOnPort(1234, service);
server.Run();
delete service;
return 0;
}
有大量正在开发的第三方项目在实现Protocol Buffer的PRC。在第三方插件的wiki页面可以看到一些。
选项
在.proto文件中的独立声明可以标注上大量的optional。选项并不会改变声明的全局意义,但在某些特别的语境中会产生影响。完整的选项列表在google/protobuf/descriptor.proto中有定义。
某些选项是文件级别的选项,意味着它们应该被写在作用域的顶部,而不是在任何消息、枚举或者服务定义里面。某些选项是消息级别的选项,这意味着它们应该被写在消息定义里面。选项其实也可以写到字段、枚举类型、枚举值、服务类型和服务方法上——然而,现在还没有这样的有用的选项。
这里介绍几个最常用的选项:
java_package(文件选项):表明你想要在生成的Java类中使用的包。如果在.proto文件中没有显式地给出java_package选项,那默认的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文件但又不目盲追求速度的app中会非常有用。
LITE_RUNTIME:protocol buffer编译器会生成仅依赖“轻量级”运行时的库(libprotobuf-lite而不是libprotobuf)。这个轻量级的运行时比使用整个库更小(大概小一个数量级)但忽略某些像描述符和映射之类的特性。这个在那些运行在诸如手机之类的受限制平台上的app在运行时非常有用。编译器仍会生成运行快速的和SPEED模式下一样的所有函数。生成的类会在每种语言中只实现MessageLite接口——它提供了一个完整Message接口的部分函数。option optimize_for = CODE_SIZE;
cc_generic_services,java_generic_service,py_generic_service(文件选项):protocol buffer编译器分别根据C++、Java和Python中服务定义来决定是否生成抽象服务代码。由于历史原因,这个值默认是true。然而,在2.3.0版本(2010年2月)之后,为PRC实现提供代码生成插件来为特定的系统生成代码而不是依赖抽象服务被认为是更好的选择。// This file relies on plugins to generate service code.
option cc_generic_services = false;
option java_generic_services = false;
option py_generic_services = false;
message_set_wire_format(消息选项):如果被设置为true,消息使用不同的二进制格式从而和在Google内被称为MessageSet的老格式兼容。Google外的用户可能永远不会用选项。消息必须这样确切地声明:message Foo {
option message_set_wire_format = true;
extensions 4 to max;
}
packed(字段选项):如果在一个重复的基本整数类型字段中被设置成true,那将使用更加紧凑的编码。使用这个选项没有任何负面影响。然而,注意到在2.3.0版本之前,收到paked数据的解析器在非预期的情况下会忽略它。因此,没办法在不打破线路兼容性的前提下要把一个已有字段变成packed格式是不可能的。在2.3.0之后,这样的改变才是安全的,因为可打包字段的解析器可以接受两种格式,但在你不得处理使用老版本的protobuf的老程序时请小心:repeated int32 samples = 4 [packed=true];
deprecated(字段选项):如果设置成true,表明字段被弃用并且不应该被新的代码使用。在大多数语言里它是没有实际效果的。在Java中,它会变成一个@Deprecated标签。将来其他特定语言的代码生成器可能会生成这个字段的弃用标签,所以在编译试图使用这个字段的代码的时候会出现警告。optional int32 old_field = 6 [deprecated=true];
个性化选项
protocol buffer甚至允许你定义和使用你自己的选项。注意这是个大多数人不需要的高级特性。由于选项是被消息定义在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);
在写这篇文档的时候(2.3.0版),仍然不支持Python的自定义选项。
自定义选项可以被定义在各种各样的protocol buffer语言的构造中。这是一个使用各种选项的例子: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) {
// Note: my_method_option has type MyMessage. We can set each field
// within it using a separate "option" line.
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这个范围内。这个范围是为独立组织内部使用保留的,所以你可以在自己的程序中自由使用这个范围内的数值。如果你想要在公共应用中使用自定义选项,那么确保你的字段值是全局唯一的就非常重要了。可以向[email protected]发送请求来获取获取唯一值。只要提供你的项目名称(例如,Object-C插件)还有你的项目网站(如果有的话)。通常你只需要一个扩展值。你可以把它们放在一个桩消息中从而只用一个扩展值就能声明多个选项:message FooOptions {
optional int32 opt1 = 1;
optional string opt2 = 2;
}
extend google.protobuf.FieldOptions {
optional FooOptions foo_options = 1234;
}
// usage:
message Bar {
optional int32 a = 1 [(foo_options.opt1) = 123, (foo_options.opt2) = "baz"];
// alternative aggregate syntax (uses TextFormat):
optional int32 b = 2 [(foo_options) = { opt1: 123 opt2: "baz" }];
}
注意到每个选项类型(文件类型,消息类型,字段类型等等)都有自己的数值范围。例如你可以用同样的扩展值来定义文件选项和消息选项。
生成你自己的类
要生成Java、Python或者C++代码来使用.proto文件中定义的消息类型,你必须对.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指定在处理import指令时搜索.proto文件的目录。如果忽略它,则采用当前目录。多个import指令可以通过多次传递—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路径里面,这样编译器才能决定它的规范命名。
编码
- 一个简单消息
- Base 128 Varints
- 消息结果
- 更多的值类型
- 内嵌消息
- 可选和可重复类型
- 字段顺序
本文档描述protocol buffer消息的二进制线路格式。在应用程序中使用protocol buffer不需要知道这些,但知道不同的protocol buffer格式如何影响你编码的消息的大小是非常有用的。
一个简单消息
假设你有如下一个非常简单的消息定义:message Test1 {
required int32 a = 1;
}
在应用程序中,你创建一个Test1消息并把a设置为150,然和将它序列化到输出流中,如果你可以实验已编码的消息,你会看到3个字节:08 96 01
到目前位置,看起来很小而且是数值化的——但为什么呢?请看下文……Base 128 Varints
要了解简单的protocol buffer编码,首先你要了解varints。varints是一种用一个或多个字节来序列化整数的方法。数值更小,所占用的字节也更少。
varint中的每一个字节——除了最后一个字节——都有一个最高位(most significant bit),它表明后面还会有字节。每个字节的低7位被用来保存数值的补码,这些字节按照从低位到高位的顺序排序。
举个例子,对于数字1,它占1个字节,所以最高位不用设:0000 0001
而对于300就有点复杂了1010 1100 0000 0010
如何分辨出这是300呢?首先你把每个字节的最高位先忽略掉,因为它只是来表明后面还有没有字节的(如你所见,第一个字节的最高位是0所以因为varint中还有一个字节):1010 1100 0000 0010
→ 010 1100 000 0010
接下来把这两组7位的数值反转,varint对这些组是按照从低位到高位顺序保存起来的。然后你就可以把它们连接起来得到最后的值了:000 0010 010 1100
→ 000 0010 ++ 010 1100
→ 100101100
→ 256 + 32 + 8 + 4 = 300消息结构
如你所知,protocol buffer消息是一系列键-值对,消息的二进制版本只是用字段的数量作为键值——每个字段的名称和声明的类型只有在解码阶段的最后引用消息类型的定义才能决定(例如.proto文件)。
当消息被编码的时候,键和值都被连接到一起输出到字节流中。当消息被解码的时候,解析器需要能够跳过无法识别的字段,这是新字段被添加到消息中而不会破坏那些无法识别它们的老的程序的方法。最后,在线路格式中每个键值对的键其实是两个值——你的.proto文件中的字段值,加上一个线路类型,它提供了恰好足够的信息来找出下个值的长度。
可用的线路类型包括:类型 意义 表示
0 Varint int32, int64, uint32, uint64, sint32, sint64, bool, enum1 64-bit fixed64, sfixed64, double
2 Length-delimited string, bytes, embedded messages, packed repeated fields
3 Start group groups (deprecated)
4 End group groups (deprecated)
5 32-bit fixed32, sfixed32, float
消息流中的每个键的都是一个值为 (字段数<<3)|线路类型 的varint——换句话说,数值的最后三位保存线路类型。
现在我们再来看一下那个简单的例子。现在你知道数据流中的第一个数值总是一个varint键,并且它是08,或者(去掉最高位):000 1000
从最后的三位中得到线路类型(0),然后把它右移三位得到字段值(1)。由此可得标签是1,接下来的值是一个varint。再用从上一节中学到的varint解码知识,就开业知道接下来的两个字节保存的值是150。96 01 = 1001 0110 0000 0001
→ 000 0001 ++ 001 0110 (去掉最高位,然后把7个一组的数值倒过来)
→ 10010110
→ 2 + 4 + 16 + 128 = 150更多的值类型
有符号整数
如你在上一节所见,所有的与线路类型0相关的protocol buffer类型都被编码成varint。然而,在编码负数方面,有符号类型(sint32和sint64)和“标准的”整数类型(int32和int64)还是有很大差别的。如果你用int32或者int64来表示负数,结果的varint总是10个字节那么长——也就是实际上它被处理成一个非常大的无符号整数。如果你用了有符号类型,结果的varint会使用ZigZag编码,它会高效得多。
ZigZag编码将有符号整数映射到无符号整数中,这样绝对值小的数值(例如-1)也会有一个小的varint编码值,方法是“zig-zags”在正数和负数间交叉向前,这样-1被编码成1,1被编码成2,-2被编码成3,以此类推,如下表所示:原有的带符号整数 被编码为
0 0-1 1
1 2
-2 3
2147483647 4294967294
-2147483648 4294967295
换句话说,每个值n被如下编码:(n << 1) ^ (n >> 31)
或者对于sint32和64位类型:(n << 1) ^ (n >> 63)
注意到第二个移位——(n >>31)——是算术移位。所以换句话说,这个移位的结果要么是一个所有位都为0或者都是1(如果n是负数)的值。
解析的时候,sint32和sint64都会被解码成原来的带符号的版本。非varint数值
非varint数值类型比较简单——double和fixed64的线路类型为1,这意味着告诉编译器它是一块64位的定长数据。相似地float和fixed32线路类型为5,则是32位大小。这两种类型的值都用小端字节序保存。字符串
线路类型2(长度限定)意味着值是一个varint编码的长度接着指定长度字节的数据:message Test2 {
required string b = 2;
}
将b的值设置为“testing”结果会是:12 07
74 65 73 74 69 6e 67
红色的字节是“testing”的UTF8编码。这里的关键是0x12表示标签值为2,类型为2。varint长度值为7,如我们所见,它后面跟着7个字节——我们定义的字符串。内嵌消息
这是一个带内嵌消息定义的例子,用了上面例子中的Test1:message Test3 {
required Test1 c = 3;
}
这是它的编码,同样Test1的a字段被设置成了150:1a 03 08 96 01
如你所见,最后的三个字节跟上面的第一个例子一样(08 96 01),在它们之前是数值3——内嵌消息被当成了string(线路类型为2)。可选和重复元素
如果你的消息定义有repeated元素(没有[packed=true]选项的),那么编码出来的消息有0个或更多的键值对,它们的标签值一样。这些重复的值不一定要连续出现,中间可能穿插着其他字段。虽然和其他字段的顺序丢失了,但解析时元素的彼此间的顺序被保留下来。
如果你的元素里面有optional的,编码出来的消息可能有或没有一个带那个标签值的键值对。
通常来说,编码出来的消息不会有超过一个optional或者required字段的实例。然而,解析器必须处理这种情况。对于数值类型和字符串,如果同样的值出现多次,解析器会以最后出现的那个值为准。对于内嵌消息字段,解析器合并同一个字段的多个实例,比如通过Message::MergeFrom函数——它的功能是对所有单个字段,将后出现的实例覆盖先出现的实例,单个的内嵌消息会被合并,重复的字段被连接到一起。这些规则的效果是解析连接的两个编码过的消息产生的结果和你分别解析两个消息再合并结果一样,就是:MyMessage message;
message.ParseFromString(str1 + str2);
和MyMessage message, message2;
message.ParseFromString(str1);
message2.ParseFromString(str2);
message.MergeFrom(message2);
的效果是一样的。
这个属性有时会很有用,因为它允许你合并两个消息而不需要知道它们的类型。打包重复字段
2.1.0版本引入了打包重复字段,它的声明跟重复字段相似但是有特别的[packed=true]选项。这个函数像重复字段,但编码方式不一样。打包重复字段包含0个元素则不出现在编码过的消息里,否则,这个字段的所有元素被以线路类型2的方式打包进一个单一键值对中。每个元素被正常地以相同的方式编码,而不需要一个前置标签。
例如,假设你有这样一个消息类型:message Test4 {
repeated int32 d = 4 [packed=true];
}
现在你构建了一个Test4,提供的值是3,270,重复字段d为86942。那么编码的格式会是:22 // tag (field number 4, wire type 2)
06 // payload size (6 bytes)
03 // first element (varint 3)
8E 02 // second element (varint 270)
9E A7 05 // third element (varint 86942)
只有基本数值类型的重复字段(那些用varint,32位或者64位线路类型的类型)可以被声明为“packed”。
注意到尽管没有理由为一个打包重复字段编码超过一个键值对,编码器需要预备好接收多个键值对的策略。在这种情况下,负载是连续性的,每个键值对都必须包含所有元素。字段顺序
虽然你可以在.proto中以任意顺序使用字段值的,当消息被序列化的时候会根据识别出来的字段的值的顺序序列化为C++、Java或Python序列化代码。这允许编译代码使用那些依赖于有序的字段值的优化策略。然而,protocol buffer解析器必须能够解析以任意顺序排列的字段,因为并非所有消息都是通过简单的序列化一个类的方式来创建的——例如,有时候把两个消息简单地连接起来,以这种方式合并更有用。
如果一个消息有未识别字段,现有的Java和C++实现会在把已知字段序列化后,把它们以任意的顺序写进去。现有的Python实现不会追踪未识别字段。
http://blog.csdn.net/cugb1004101218/article/details/39022167