Protobuf在Java中的简单使用
1. 在Java中使用protobuf需要jar包,下载protobuf-Java-2.5.0.jar包文件,添加到项目中。另外需要protoc.exe来编译proto文件。
2. 新建一个msg.proto文件:
package com.test.learn;
option java_package = "com.test.learn";
option java_outer_classname = "ProtoBufTest";
message msgInfo {
required int32 ID = 1;
required int64 GoodID = 2;
required string Url = 3;
required string Guid = 4;
required string Type = 5;
required int32 Order = 6;
}
3. 将msg.proto文件和protoc.exe拷贝到同一目录下,然后使用下面的命令将msg.proto文件编译成Java类文件,可在当前目录下看到生成的ProtoBufTest.java文件:
protoc --java_out=./ msg.proto
4. 在项目中导入上一步生成的ProtoBufTest.java文件,并将protobuf-Java-2.5.0.jar库添加到项目中,编写测试文件进行测试:
5. 测试代码:
package com.test;
import com.google.protobuf.InvalidProtocolBufferException;
import com.test.learn.ProtoBufTest;
public class MainFile {
public static voidmain(String[] args) {
ProtoBufTest.msgInfo.Builderbuilder = ProtoBufTest.msgInfo.newBuilder();
builder.setGoodID(100);
builder.setGuid("11111-22222-33333-44444");
builder.setOrder(0);
builder.setType("item");
builder.setID(10);
builder.setUrl("http://www.baidu.com");
ProtoBufTest.msgInfomsgInfo = builder.build();
byte[] result =msgInfo.toByteArray();
try {
ProtoBufTest.msgInfomsg = ProtoBufTest.msgInfo.parseFrom(result);
System.out.println(msg);
} catch(InvalidProtocolBufferExceptione) {
System.out.println(e.getMessage());
}
}
}
Protobuf语言指南
l 定义一个消息(message)类型
l 标量值类型
l Optional 的字段及默认值
l 枚举
l 使用其他消息类型
l 嵌套类型
l 更新一个消息类型
l 扩展
l 包(package)
l 定义服务(service)
l 选项(option)
l 生成访问类
本指南描述了怎样使用protocolbuffer语言来构造你的protocol buffer数据,包括.proto文件语法以及怎样生成.proto文件的数据访问类。
本文是一个参考指南——如果要查看如何使用本文中描述的多个特性的循序渐进的例子,请在http://code.google.com/intl/zh-CN/apis/protocolbuffers/docs/tutorials.html中查找需要的语言的教程。
l 定义一个消息类型
先来看一个非常简单的例子。假设你想定义一个“搜索请求”的消息格式,每一个请求含有一个查询字符串、你感兴趣的查询结果所在的页数,以及每一页多少条查询结果。可以采用如下的方式来定义消息类型的.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),一个string类型(query)。当然,你也可以为字段指定其他的合成类型,包括枚举(enumerations)或其他消息类型。
Ø 分配标识号
正如上述文件格式,在消息定义中,每个字段都有唯一的一个标识符。这些标识符是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变。注:[1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 [1,15]之内的标识号。切记:要为将来有可能添加的、频繁出现的标识号预留一些标识号。最小的标识号可以从1开始,最大到229 -1, or 536,870,911。不可以使用其中的[19000-19999]的标识号, Protobuf协议实现中对这些进行了预留。如果非要在.proto文件中使用这些预留标识号,编译时就会报警。
Ø 指定字段规则
所指定的消息字段修饰符必须是如下之一:
² required:一个格式良好的消息一定要含有1个这种字段。表示该值是必须要设置的;
² optional:消息格式中该字段可以有0个或1个值(不超过1个)。
² repeated:在一个格式良好的消息中,这种字段可以重复任意多次(包括0次)。重复的值的顺序会被保留。表示该值可以重复,相当于java中的List。
由于一些历史原因,基本数值类型的repeated的字段并没有被尽可能地高效编码。在新的代码中,用户应该使用特殊选项[packed=true]来保证更高效的编码。如:
repeated int32 samples = 4 [packed=true]; |
required是永久性的:在将一个字段标识为required的时候,应该特别小心。如果在某些情况下不想写入或者发送一个required的字段,将原始该字段修饰符更改为optional可能会遇到问题——旧版本的使用者会认为不含该字段的消息是不完整的,从而可能会无目的的拒绝解析。在这种情况下,你应该考虑编写特别针对于应用程序的、自定义的消息校验函数。Google的一些工程师得出了一个结论:使用required弊多于利;他们更愿意使用optional和repeated而不是required。当然,这个观点并不具有普遍性。
Ø 添加更多消息类型
在一个.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++/java风格的双斜杠(//) 语法格式,如:
message SearchRequest { required string query = 1; optional int32 page_number = 2;// 最终返回的页数 optional int32 result_per_page = 3;// 每页返回的结果数 } |
Ø 从.proto文件生成了什么?
当用protocolbuffer编译器来运行.proto文件时,编译器将生成所选择语言的代码,这些代码可以操作在.proto文件中定义的消息类型,包括获取、设置字段值,将消息序列化到一个输出流中,以及从一个输入流中解析消息。
² 对C++来说,编译器会为每个.proto文件生成一个.h文件和一个.cc文件,.proto文件中的每一个消息有一个对应的类。
² 对Java来说,编译器为每一个消息类型生成了一个.java文件,以及一个特殊的Builder类(该类是用来创建消息类接口的)。
² 对Python来说,有点不太一样——Python编译器为.proto文件中的每个消息类型生成一个含有静态描述符的模块,该模块与一个元类(metaclass)在运行时(runtime)被用来创建所需的Python数据访问类。
你可以从如下的文档链接中获取每种语言更多API。http://code.google.com/intl/zh-CN/apis/protocolbuffers/docs/reference/overview.html
l 标量数值类型
一个标量消息字段可以含有一个如下的类型——该表格展示了定义于.proto文件中的类型,以及与之对应的、在自动生成的访问类中定义的类型:
.proto类型 | Java 类型 | C++类型 | 备注 |
double | double | double |
|
float | float | float |
|
int32 | int | int32 | 使用可变长编码方式。编码负数时不够高效——如果你的字段可能含有负数,那么请使用sint32。 |
int64 | long | int64 | 使用可变长编码方式。编码负数时不够高效——如果你的字段可能含有负数,那么请使用sint64。 |
uint32 | int[1] | uint32 | Uses variable-length encoding. |
uint64 | long[1] | uint64 | Uses variable-length encoding. |
sint32 | int | int32 | 使用可变长编码方式。有符号的整型值。编码时比通常的int32高效。 |
sint64 | long | int64 | 使用可变长编码方式。有符号的整型值。编码时比通常的int64高效。 |
fixed32 | int[1] | uint32 | 总是4个字节。如果数值总是比总是比228大的话,这个类型会比uint32高效。 |
fixed64 | long[1] | uint64 | 总是8个字节。如果数值总是比总是比256大的话,这个类型会比uint64高效。 |
sfixed32 | int | int32 | 总是4个字节。 |
sfixed64 | long | int64 | 总是8个字节。 |
bool | boolean | bool |
|
string | String | string | 一个字符串必须是UTF-8编码或者7-bit ASCII编码的文本。 |
bytes | ByteString | string | 可能包含任意顺序的字节数据。 |
你可以在文章http://code.google.com/apis/protocolbuffers/docs/encoding.html 中,找到更多“序列化消息时各种类型如何编码”的信息。
l Optional的字段和默认值
如上所述,消息描述中的一个元素可以被标记为“可选的”(optional)。一个格式良好的消息可以包含0个或一个optional的元素。当解析消息时,如果它不包含optional的元素值,那么解析出来的对象中的对应字段就被置为默认值。默认值可以在消息描述文件中指定。例如,要为 SearchRequest消息的result_per_page字段指定默认值10,在定义消息格式时如下所示:
optional int32 result_per_page = 3 [default = 10]; |
如果没有为optional的元素指定默认值,就会使用与特定类型相关的默认值:对string来说,默认值是空字符串。对bool来说,默认值是false。对数值类型来说,默认值是0。对枚举来说,默认值是枚举类型定义中的第一个值。
l 枚举
当需要定义一个消息类型的时候,可能想为一个字段指定某“预定义值序列”中的一个值。例如,假设要为每一个SearchRequest消息添加一个 corpus字段,而corpus的值可能是UNIVERSAL,WEB,IMAGES,LOCAL,NEWS,PRODUCTS或VIDEO中的一个。其实可以很容易地实现这一点:通过向消息定义中添加一个枚举(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位整型值的范围内。因为enum值是使用可变编码方式的,对负数不够高效,因此不推荐在enum中使用负数。如上例所示,可以在一个消息定义的内部或外部定义枚举——这些枚举可以在.proto文件中的任何消息定义里重用。当然也可以在一个消息中声明一个枚举类型,而在另一个不同的消息中使用它——采用MessageType.EnumType的语法格式。当对一个使用了枚举的.proto文件运行protocol buffer编译器的时候,生成的代码中将有一个对应的enum(对Java或C++来说),或者一个特殊的EnumDescriptor类(对 Python来说),它被用来在运行时生成的类中创建一系列的整型值符号常量(symbolic constants)。
关于如何在你的应用程序的消息中使用枚举的更多信息,请查看所选择的语言http://code.google.com/intl/zh-CN/apis/protocolbuffers/docs/reference/overview.html。
l 使用其他消息类型
你可以将其他消息类型用作字段类型。例如,假设在每一个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文件中已经定义过了呢?
你可以通过导入(importing)其他.proto文件中的定义来使用它们。要导入其他.proto文件的定义,你需要在你的文件中添加一个导入声明,如:
import "myproject/other_protos.proto"; |
protocol编译器就会在一系列目录中查找需要被导入的文件,这些目录通过protocol编译器的命令行参数-I/–import_path指定。如果不提供参数,编译器就在其调用目录下查找。
l 嵌套类型
你可以在其他消息类型中定义、使用消息类型,在下面的例子中,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; } } } |
Ø 组
注:该特性已被弃用,在创建新的消息类型的时候,不应该再使用它——可以使用嵌套消息类型来代替它。
“组”是指在消息定义中嵌套信息的另一种方法。比如,在SearchResponse中包含若干Result的另一种方法是 :
message SearchResponse { repeated group Result = 1 { required string url = 2; optional string title = 3; repeated string snippets = 4; } } |
一个“组”只是简单地将一个嵌套消息类型和一个字段捆绑到一个单独的声明中。在代码中,可以把它看成是含有一个Result类型、名叫result的字段的消息(后面的名字被转换成了小写,所以它不会与前面的冲突)。
因此,除了数据传输格式不同之外,这个例子与上面的SearchResponse例子是完全等价的。
l 更新一个消息类型
如果一个已有的消息格式已无法满足新的需求——如,要在消息中添加一个额外的字段——但是同时旧版本写的代码仍然可用。不用担心!更新消息而不破坏已有代码是非常简单的。在更新时只要记住以下的规则即可。
² 不要更改任何已有的字段的数值标识。
² 所添加的任何字段都必须是optional或repeated的。这就意味着任何使用“旧”的消息格式的代码序列化的消息可以被新的代码所解析,因为它们不会丢掉任何required的元素。应该为这些元素设置合理的默认值,这样新的代码就能够正确地与老代码生成的消息交互了。类似地,新的代码创建的消息也能被老的代码解析:老的二进制程序在解析的时候只是简单地将新字段忽略。然而,未知的字段是没有被抛弃的。此后,如果消息被序列化,未知的字段会随之一起被序列化——所以,如果消息传到了新代码那里,则新的字段仍然可用。注意:对Python来说,对未知字段的保留策略是无效的。
² 非required的字段可以移除——只要它们的标识号在新的消息类型中不再使用(更好的做法可能是重命名那个字段,例如在字段前添加“OBSOLETE_”前缀,那样的话,使用的.proto文件的用户将来就不会无意中重新使用了那些不该使用的标识号)。
² 一个非required的字段可以转换为一个扩展,反之亦然——只要它的类型和标识号保持不变。
² int32, uint32, int64, uint64,和bool是全部兼容的,这意味着可以将这些类型中的一个转换为另外一个,而不会破坏向前、向后的兼容性。如果解析出来的数字与对应的类型不相符,那么结果就像在C++中对它进行了强制类型转换一样(例如,如果把一个64位数字当作int32来读取,那么它就会被截断为32位的数字)。
² sint32和sint64是互相兼容的,但是它们与其他整数类型不兼容。
² string和bytes是兼容的——只要bytes是有效的UTF-8编码。
² 嵌套消息与bytes是兼容的——只要bytes包含该消息的一个编码过的版本。
² fixed32与sfixed32是兼容的,fixed64与sfixed64是兼容的。
l 扩展
通过扩展,可以将一个范围内的字段标识号声明为可被第三方扩展所用。然后,其他人就可以在他们自己的.proto文件中为该消息类型声明新的字段,而不必去编辑原始文件了。看个具体例子:
message Foo { // … extensions 100 to 199; } |
这个例子表明:在消息Foo中,范围[100,199]之内的字段标识号被保留为扩展用。现在,其他人就可以在他们自己的.proto文件中添加新字段到Foo里了,但是添加的字段标识号要在指定的范围内——例如:
extend Foo { optional int32 bar = 126; } |
这个例子表明:消息Foo现在有一个名为bar的optional int32字段。
当用户的Foo消息被编码的时候,数据的传输格式与用户在Foo里定义新字段的效果是完全一样的。
然而,要在程序代码中访问扩展字段的方法与访问普通的字段稍有不同——生成的数据访问代码为扩展准备了特殊的访问函数来访问它。例如,下面是如何在C++中设置bar的值:
Foo foo; |
类似地,Foo类也定义了模板函数 HasExtension(),ClearExtension(),GetExtension(),MutableExtension(),以及 AddExtension()。这些函数的语义都与对应的普通字段的访问函数相符。要查看更多使用扩展的信息,请参考相应语言的代码生成指南。注:扩展可以是任何字段类型,包括消息类型。
l 嵌套的扩展
可以在另一个类型的范围内声明扩展,如:
message Baz { extend Foo { optional int32 bar = 126; } … } |
在此例中,访问此扩展的C++代码如下:
Foo foo; foo.SetExtension(Baz::bar, 15); |
一个通常的设计模式就是:在扩展的字段类型的范围内定义该扩展——例如,下面是一个Foo的扩展(该扩展是Baz类型的),其中,扩展被定义为了Baz的一部分:
message Baz { extend Foo { optional Baz foo_ext = 127; } … } |
然而,并没有强制要求一个消息类型的扩展一定要定义在那个消息中。也可以这样做:
message Baz { … }
extend Foo { optional Baz foo_baz_ext = 127; }
|
事实上,这种语法格式更能防止引起混淆。正如上面所提到的,嵌套的语法通常被错误地认为有子类化的关系——尤其是对那些还不熟悉扩展的用户来说。
Ø 选择可扩展的标符号
在同一个消息类型中一定要确保两个用户不会扩展新增相同的标识号,否则可能会导致数据的不一致。可以通过为新项目定义一个可扩展标识号规则来防止该情况的发生。
如果标识号需要很大的数量时,可以将该可扩展标符号的范围扩大至max,其中max是229 - 1, 或536,870,911。如下所示:
message Foo { extensions 1000 to max; } |
通常情况下在选择标符号时,标识号产生的规则中应该避开[19000-19999]之间的数字,因为这些已经被Protocol Buffers实现中预留了。
l 包(Package)
当然可以为.proto文件新增一个可选的package声明符,用来防止不同的消息类型有命名冲突。如:
package foo.bar; message Open { ... } |
在其他的消息格式定义中可以使用包名+消息名的方式来定义域的类型,如:
message Foo { ... required foo.bar.Open open = 1; ... } |
包的声明符会根据使用语言的不同影响生成的代码。对于C++,产生的类会被包装在C++的命名空间中,如上例中的Open会被封装在 foo::bar空间中;对于Java,包声明符会变为java的一个包,除非在.proto文件中提供了一个明确有java_package;对于 Python,这个包声明符是被忽略的,因为Python模块是按照其在文件系统中的位置进行组织的。
Ø 包及名称的解析
Protocol buffer语言中类型名称的解析与C++是一致的:首先从最内部开始查找,依次向外进行,每个包会被看作是其父类包的内部类。当然对于(foo.bar.Baz)这样以“.”分隔的意味着是从最外围开始的。ProtocolBuffer编译器会解析.proto文件中定义的所有类型名。对于不同语言的代码生成器会知道如何来指向每个具体的类型,即使它们使用了不同的规则。
l 定义服务(Service)
如果想要将消息类型用在RPC(远程方法调用)系统中,可以在.proto文件中定义一个RPC服务接口,protocol buffer编译器将会根据所选择的不同语言生成服务接口代码及存根。如,想要定义一个RPC服务并具有一个方法,该方法能够接收 SearchRequest并返回一个SearchResponse,此时可以在.proto文件中进行如下定义:
service SearchService { rpc Search (SearchRequest) returns (SearchResponse); } |
protocol编译器将产生一个抽象接口SearchService以及一个相应的存根实现。存根将所有的调用指向RpcChannel,它是一个抽象接口,必须在RPC系统中对该接口进行实现。如,可以实现RpcChannel以完成序列化消息并通过HTTP方式来发送到一个服务器。换句话说,产生的存根提供了一个类型安全的接口用来完成基于protocolbuffer的RPC调用,而不是将你限定在一个特定的RPC的实现中。C++中的代码如下所示:
using google::protobuf; protobuf::RpcChannel* channel; void DoSearch() { // The protocol compiler generates the SearchService class based on the service = new SearchService::Stub(channel); // Execute the RPC. void Done() { |
所有service类都必须实现Service接口,它提供了一种用来调用具体方法的方式,即在编译期不需要知道方法名及它的输入、输出类型。在服务器端,通过服务注册它可以被用来实现一个RPC Server。
using google::protobuf; |
l 选项(Options)
在定义.proto文件时能够标注一系列的options。Options并不改变整个文件声明的含义,但却能够影响特定环境下处理方式。完整的可用选项可以在google/protobuf/descriptor.proto找到。
一些选项是文件级别的,意味着它可以作用于最外范围,不包含在任何消息内部、enum或服务定义中。一些选项是消息级别的,意味着它可以用在消息定义的内部。当然有些选项可以作用在域、enum类型、enum值、服务类型及服务方法中。到目前为止,并没有一种有效的选项能作用于所有的类型。
如下就是一些常用的选择:
² java_package (file option): 这个选项表明生成java类所在的包。如果在.proto文件中没有明确的声明java_package,就采用默认的包名。当然了,默认方式产生的 java包名并不是最好的方式,按照应用名称倒序方式进行排序的。如果不需要产生java代码,则该选项将不起任何作用。如:
option java_package = "com.example.foo"; |
² java_outer_classname (file option): 该选项表明想要生成Java类的名称。如果在.proto文件中没有明确的java_outer_classname定义,生成的class名称将会根据.proto文件的名称采用驼峰式的命名方式进行生成。如(foo_bar.proto生成的java类名为FooBar.java),如果不生成java代码,则该选项不起任何作用。如:
option java_outer_classname = "Ponycopter"; |
² optimize_for (fileoption): 可以被设置为 SPEED, CODE_SIZE,or LITE_RUNTIME。这些值将通过如下的方式影响C++及java代码的生成:
· SPEED (default):protocol buffer编译器将通过在消息类型上执行序列化、语法分析及其他通用的操作。这种代码是最优的。
· CODE_SIZE: protocolbuffer编译器将会产生最少量的类,通过共享或基于反射的代码来实现序列化、语法分析及各种其它操作。采用该方式产生的代码将比SPEED要少得多,但是操作要相对慢些。当然实现的类及其对外的API与SPEED模式都是一样的。这种方式经常用在一些包含大量的.proto文件而且并不盲目追求速度的应用中。
· LITE_RUNTIME:protocol buffer编译器依赖于运行时核心类库来生成代码(即采用libprotobuf-lite 替代libprotobuf)。这种核心类库由于忽略了一些描述符及反射,要比全类库小得多。这种模式经常在移动手机平台应用多一些。编译器采用该模式产生的方法实现与SPEED模式不相上下,产生的类通过实现 MessageLite接口,但它仅仅是Messager接口的一个子集。
option optimize_for = CODE_SIZE; |
² cc_generic_services, java_generic_services, py_generic_services (file options): 在C++、java、python中protocol buffer编译器是否应该基于服务定义产生抽象服务代码。由于历史遗留问题,该值默认是true。但是自2.3.0版本以来,它被认为通过提供代码生成器插件来对RPC实现更可取,而不是依赖于“抽象”服务。
// 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 (message option):如果该值被设置为true,该消息将使用一种不同的二进制格式来与Google内部的MessageSet的老格式相兼容。对于Google外部的用户来说,该选项将不会被用到。如下所示:
message Foo { option message_set_wire_format = true; extensions 4 to max; } |
² packed (field option): 如果该选项在一个整型基本类型上被设置为真,则采用更紧凑的编码方式。当然使用该值并不会对数值造成任何损失。在2.3.0版本之前,解析器将会忽略那些非期望的包装值。因此,它不可能在不破坏现有框架的兼容性上而改变压缩格式。在2.3.0之后,这种改变将是安全的,解析器能够接受上述两种格式,但是在处理protobuf老版本程序时,还是要多留意一下。
repeated int32 samples = 4 [packed=true]; |
² deprecated (field option): 如果该选项被设置为true,表明该字段已经被弃用了,在新代码中不建议使用。在多数语言中,这并没有实际的含义。在java中,它将会变成一个 @Deprecated注释。也许在将来,其它基于语言声明的代码在生成时也会如此使用,当使用该字段时,编译器将自动报警。如:
optional int32 old_field = 6 [deprecated=true]; |
Ø 自定义选项
ProtocolBuffers允许自定义并使用选项。该功能应该属于一个高级特性,对于大部分人是用不到的。由于options是定在 google/protobuf/descriptor.proto中的,因此你可以在该文件中进行扩展,定义自己的选项。如:
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); |
在Java代码中的读取方式如下:
String value = MyProtoFile.MyMessage.getDescriptor().getOptions().getExtension(MyProtoFile.myOption); |
正如上面的读取方式,定制选项对于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]已经被占用,该范围内的值已经被内部所使用,当然了你可以在内部应用中随意使用。如果你想在一些公共应用中进行自定义选项,你必须确保它是全局唯一的。可以通过protobuf-global-extension-registry@google.com来获取全局唯一标识号。
l 生成访问类
可以通过定义好的.proto文件来生成Java、Python、C++代码,需要基于.proto文件运行protocol buffer编译器protoc。运行的命令如下所示:
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是它的简化形式。
· 当然也可以提供一个或多个输出路径:
o --cpp_out 在目标目录DST_DIR中产生C++代码,可以在 http://code.google.com/intl/zh-CN/apis/protocolbuffers/docs/reference/cpp-generated.html中查看更多。
o --java_out 在目标目录DST_DIR中产生Java代码,可以在 http://code.google.com/intl/zh-CN/apis/protocolbuffers/docs/reference/java-generated.html中查看更多。
o --python_out 在目标目录 DST_DIR 中产生Python代码,可以在http://code.google.com/intl/zh-CN/apis/protocolbuffers/docs/reference/python-generated.html中查看更多。
作为一种额外的使得,如果DST_DIR 是以.zip或.jar结尾的,编译器将输出结果打包成一个zip格式的归档文件。.jar将会输出一个 Java JAR声明必须的manifest文件。注:如果该输出归档文件已经存在,它将会被重写,编译器并没有做到足够的智能来为已经存在的归档文件添加新的文件。
· 你必须提供一个或多个.proto文件作为输入。多个.proto文件能够一次全部声明。虽然这些文件是相对于当前目录来命名的,每个文件必须在一个IMPORT_PATH中,只有如此编译器才可以决定它的标准名称。
from:http://www.open-open.com/home/space.php?uid=37924&do=blog&id=5873
=========================================================
ProtoBuf开发者指南:http://gashero.yeax.com/?p=108
官方:http://code.google.com/p/protobuf/
语言指南
http://code.google.com/intl/zh-CN/apis/protocolbuffers/docs/proto.html
风格
http://code.google.com/intl/zh-CN/apis/protocolbuffers/docs/style.html
一种自动反射消息类型的Google Protobuf 网络传输方案
陈硕(giantchen_AT_gmail)
Blog.youkuaiyun.com/Solstice t.sina.com.cn/giantchen
这篇文章要解决的问题是:在接收到 protobuf 数据之后,如何自动创建具体的 Protobuf Message 对象,再做的反序列化。“自动”的意思是:当程序中新增一个 protobuf Message 类型时,这部分代码不需要修改,不需要自己去注册消息类型。其实,GoogleProtobuf 本身具有很强的反射(reflection)功能,可以根据 type name 创建具体类型的 Message 对象,我们直接利用即可。
本文假定读者了解 Google Protocol Buffers 是什么,这不是一篇 protobuf 入门教程。
本文以 C++ 语言举例,其他语言估计有类似的解法,欢迎补充。
本文的示例代码在: https://github.com/chenshuo/recipes/tree/master/protobuf
网络编程中使用 protobuf 的两个问题
Google Protocol Buffers (Protobuf)是一款非常优秀的库,它定义了一种紧凑的可扩展二进制消息格式,特别适合网络数据传输。它为多种语言提供 binding,大大方便了分布式程序的开发,让系统不再局限于用某一种语言来编写。
在网络编程中使用 protobuf 需要解决两个问题:
· 长度,protobuf 打包的数据没有自带长度信息或终结符,需要由应用程序自己在发生和接收的时候做正确的切分;
· 类型,protobuf 打包的数据没有自带类型信息,需要由发送方把类型信息传给给接收方,接收方创建具体的 Protobuf Message 对象,再做的反序列化。
第一个很好解决,通常的做法是在每个消息前面加个固定长度的 length header,例如我在 《Muduo 网络编程示例之二:Boost.Asio 的聊天服务器》 中实现的LengthHeaderCodec,代码见http://code.google.com/p/muduo/source/browse/trunk/examples/asio/chat/codec.h
第二个问题其实也很好解决,Protobuf 对此有内建的支持。但是奇怪的是,从网上简单搜索的情况看,我发现了很多山寨的做法。
山寨做法
以下均为在 protobuf data 之前加上 header,header 中包含 int length 和类型信息。类型信息的山寨做法主要有两种:
· 在 header 中放 int typeId,接收方用 switch-case 来选择对应的消息类型和处理函数;
· 在 header 中放 string typeName,接收方用 look-up table 来选择对应的消息类型和处理函数。
这两种做法都有问题。
第一种做法要求保持 typeId 的唯一性,它和 protobuf message type 一一对应。如果 protobuf message 的使用范围不广,比如接收方和发送方都是自己维护的程序,那么 typeId 的唯一性不难保证,用版本管理工具即可。如果 protobuf message 的使用范围很大,比如全公司都在用,而且不同部门开发的分布式程序可能相互通信,那么就需要一个公司内部的全局机构来分配 typeId,每次增加新 message type 都要去注册一下,比较麻烦。
第二种做法稍好一点。typeName 的唯一性比较好办,因为可以加上 package name(也就是用 message 的 fullyqualified type name),各个部门事先分好 namespace,不会冲突与重复。但是每次新增消息类型的时候都要去手工修改 look-up table 的初始化代码,比较麻烦。
其实,不需要自己重新发明轮子,protobuf 本身已经自带了解决方案。
根据 type name 反射自动创建 Message 对象
Google Protobuf 本身具有很强的反射(reflection)功能,可以根据 type name 创建具体类型的 Message 对象。但是奇怪的是,其官方教程里没有明确提及这个用法,我估计还有很多人不知道这个用法,所以觉得值得写这篇 blog 谈一谈。
以下是陈硕绘制的 Protobuf class diagram,点击查看原图。
我估计大家通常关心和使用的是图的左半部分:MessageLite、Message、Generated Message Types(Person, AddressBook) 等,而较少注意到图的右半部分:Descriptor, DescriptorPool, MessageFactory。
上图中,其关键作用的是 Descriptor class,每个具体 Message Type 对应一个 Descriptor 对象。尽管我们没有直接调用它的函数,但是Descriptor在“根据 type name 创建具体类型的 Message 对象”中扮演了重要的角色,起了桥梁作用。上图的红色箭头描述了根据 typename 创建具体 Message对象的过程,后文会详细介绍。
原理简述
Protobuf Message class 采用了 prototype pattern,Message class 定义了 New() 虚函数,用以返回本对象的一份新实例,类型与本对象的真实类型相同。也就是说,拿到 Message* 指针,不用知道它的具体类型,就能创建和它类型一样的具体 Message Type 的对象。
每个具体 MessageType 都有一个 defaultinstance,可以通过ConcreteMessage::default_instance() 获得,也可以通过 MessageFactory::GetPrototype(const Descriptor*) 来获得。所以,现在问题转变为 1. 如何拿到 MessageFactory;2. 如何拿到 Descriptor*。
当然,ConcreteMessage::descriptor()返回了我们想要的 Descriptor*,但是,在不知道 ConcreteMessage 的时候,如何调用它的静态成员函数呢?这似乎是个鸡与蛋的问题。
我们的英雄是 DescriptorPool,它可以根据 type name 查到Descriptor*,只要找到合适的DescriptorPool,再调用DescriptorPool::FindMessageTypeByName(const string& type_name) 即可。眼前一亮?
在最终解决问题之前,先简单测试一下,看看我上面说的对不对。
简单测试
本文用于举例的 proto 文件:query.proto,见 https://github.com/chenshuo/recipes/blob/master/protobuf/query.proto
package muduo;
message Query {
required int64 id = 1;
required string questioner = 2;
repeated string question = 3;
}
message Answer {
required int64 id = 1;
required string questioner = 2;
required string answerer = 3;
repeated string solution = 4;
}
message Empty {
optional int32 id = 1;
}
其中的 Query.questioner 和 Answer.answerer 是我在前一篇文章这提到的《分布式系统中的进程标识》。
以下代码验证 ConcreteMessage::default_instance()、ConcreteMessage::descriptor()、 MessageFactory::GetPrototype()、DescriptorPool::FindMessageTypeByName() 之间的不变式 (invariant):
https://github.com/chenshuo/recipes/blob/master/protobuf/descriptor_test.cc#L15
typedef muduo::Query T;
std::string type_name = T::descriptor()->full_name();
cout << type_name << endl;
const Descriptor* descriptor = DescriptorPool::generated_pool()->FindMessageTypeByName(type_name);
assert(descriptor == T::descriptor());
cout << "FindMessageTypeByName() = " << descriptor << endl;
cout << "T::descriptor() = " << T::descriptor() << endl;
cout << endl;
const Message* prototype = MessageFactory::generated_factory()->GetPrototype(descriptor);
assert(prototype == &T::default_instance());
cout << "GetPrototype() = " << prototype << endl;
cout << "T::default_instance() = " << &T::default_instance() << endl;
cout << endl;
T* new_obj = dynamic_cast<T*>(prototype->New());
assert(new_obj != NULL);
assert(new_obj != prototype);
assert(typeid(*new_obj) == typeid(T::default_instance()));
cout << "prototype->New() = " << new_obj << endl;
cout << endl;
delete new_obj;
根据 type name 自动创建 Message 的关键代码
好了,万事具备,开始行动:
1. 用 DescriptorPool::generated_pool() 找到一个 DescriptorPool 对象,它包含了程序编译的时候所链接的全部 protobuf Message types。
2. 用 DescriptorPool::FindMessageTypeByName() 根据 type name 查找 Descriptor。
3. 再用 MessageFactory::generated_factory() 找到 MessageFactory 对象,它能创建程序编译的时候所链接的全部 protobuf Message types。
4. 然后,用 MessageFactory::GetPrototype() 找到具体 Message Type 的 default instance。
5. 最后,用 prototype->New() 创建对象。
示例代码见 https://github.com/chenshuo/recipes/blob/master/protobuf/codec.h#L69
Message* createMessage(const std::string& typeName)
{
Message* message = NULL;
const Descriptor* descriptor = DescriptorPool::generated_pool()->FindMessageTypeByName(typeName);
if (descriptor)
{
const Message* prototype = MessageFactory::generated_factory()->GetPrototype(descriptor);
if (prototype)
{
message = prototype->New();
}
}
return message;
}
调用方式:https://github.com/chenshuo/recipes/blob/master/protobuf/descriptor_test.cc#L49
Message* newQuery = createMessage("muduo.Query");
assert(newQuery != NULL);
assert(typeid(*newQuery) == typeid(muduo::Query::default_instance()));
cout << "createMessage(\"muduo.Query\") = " << newQuery << endl;
古之人不余欺也 :-)
注意,createMessage()返回的是动态创建的对象的指针,调用方有责任释放它,不然就会内存泄露。在 muduo 里,我用shared_ptr<Message> 来自动管理 Message 对象的生命期。
线程安全性
Google 的文档说,我们用到的那几个 MessageFactory 和DescriptorPool 都是线程安全的,Message::New()也是线程安全的。并且它们都是 const member function。
关键问题解决了,那么剩下工作就是设计一种包含长度和消息类型的 protobuf 传输格式。
Protobuf 传输格式
陈硕设计了一个简单的格式,包含 protobuf data 和它对应的长度与类型信息,消息的末尾还有一个 check sum。格式如下图,图中方块的宽度是 32-bit。
用 Cstruct 伪代码描述:
struct ProtobufTransportFormat __attribute__ ((__packed__))
{
int32_t len;
int32_t nameLen;
char typeName[nameLen];
char protobufData[len-nameLen-8];
int32_t checkSum; // adler32 of nameLen, typeName and protobufData
};
注意,这个格式不要求 32-bit 对齐,我们的 decoder 会自动处理非对齐的消息。
例子
用这个格式打包一个 muduo.Query 对象的结果是:
设计决策
以下是我在设计这个传输格式时的考虑:
· signed int。消息中的长度字段只使用了 signed 32-bit int,而没有使用 unsigned int,这是为了移植性,因为 Java 语言没有 unsigned 类型。另外 Protobuf 一般用于打包小于 1M 的数据,unsigned int 也没用。
· check sum。虽然 TCP 是可靠传输协议,虽然 Ethernet 有 CRC-32 校验,但是网络传输必须要考虑数据损坏的情况,对于关键的网络应用,check sum 是必不可少的。对于 protobuf 这种紧凑的二进制格式而言,肉眼看不出数据有没有问题,需要用 check sum。
· adler32 算法。我没有选用常见的 CRC-32,而是选用 adler32,因为它计算量小、速度比较快,强度和 CRC-32差不多。另外,zlib 和 java.unit.zip 都直接支持这个算法,不用我们自己实现。
· type name 以 '\0' 结束。这是为了方便 troubleshooting,比如通过 tcpdump 抓下来的包可以用肉眼很容易看出 type name,而不用根据 nameLen 去一个个数字节。同时,为了方便接收方处理,加入了 nameLen,节省 strlen(),空间换时间。
· 没有版本号。Protobuf Message 的一个突出优点是用 optional fields 来避免协议的版本号(凡是在 protobuf Message 里放版本号的人都没有理解 protobuf 的设计),让通信双方的程序能各自升级,便于系统演化。如果我设计的这个传输格式又把版本号加进去,那就画蛇添足了。具体请见本人《分布式系统的工程化开发方法》第 57 页:消息格式的选择。
示例代码
为了简单起见,采用 std::string 来作为打包的产物,仅为示例。
打包 encode 的代码:https://github.com/chenshuo/recipes/blob/master/protobuf/codec.h#L35
解包 decode 的代码:https://github.com/chenshuo/recipes/blob/master/protobuf/codec.h#L99
测试代码: https://github.com/chenshuo/recipes/blob/master/protobuf/codec_test.cc
如果以上代码编译通过,但是在运行时出现“cannot open shared object file”错误,一般可以用 sudo ldconfig 解决,前提是 libprotobuf.so 位于/usr/local/lib,且/etc/ld.so.conf 列出了这个目录。
$ make all # 如果你安装了 boost,可以 make whole
$ ./codec_test
./codec_test: error while loading shared libraries: libprotobuf.so.6: cannot openshared object file: No such file or directory
$ sudo ldconfig
与 muduo 集成
muduo 网络库将会集成对本文所述传输格式的支持(预计 0.1.9 版本),我会另外写一篇短文介绍 Protobuf Message<=> muduo::net::Buffer 的相互转化,使用 muduo::net::Buffer 来打包比上面 std::string 的代码还简单,它是专门为 non-blocking 网络库设计的 buffer class。
此外,我们可以写一个 codec 来自动完成转换,就行 asio/char/codec.h 那样。这样客户代码直接收到的就是 Message 对象,发送的时候也直接发送 Message 对象,而不需要和 Buffer 对象打交道。
消息的分发 (dispatching)
目前我们已经解决了消息的自动创建,在网络编程中,还有一个常见任务是把不同类型的 Message 分发给不同的处理函数,这同样可以借助 Descriptor 来完成。我在 muduo 里实现了 ProtobufDispatcherLite 和 ProtobufDispatcher 两个分发器,用户可以自己注册针对不同消息类型的处理函数。预计将会在 0.1.9 版本发布,您可以先睹为快:
初级版,用户需要自己做 down casting: https://github.com/chenshuo/recipes/blob/master/protobuf/dispatcher_lite.cc
高级版,使用模板技巧,节省用户打字: https://github.com/chenshuo/recipes/blob/master/protobuf/dispatcher.cc
基于 muduo 的 Protobuf RPC?
Google Protobuf 还支持 RPC,可惜它只提供了一个框架,没有开源网络相关的代码,muduo 正好可以填补这一空白。我目前还没有决定是不是让 muduo 也支持以 protobuf message 为消息格式的 RPC,muduo 还有很多事情要做,我也有很多博客文章打算写,RPC 这件事情以后再说吧。
注:RemoteProcedure Call (RPC) 有广义和狭义两种意思。狭义的讲,一般特指 ONCRPC,就是用来实现 NFS 的那个东西;广义的讲,“以函数调用之名,行网络通信之实”都可以叫 RPC,比如 Java RMI,.Net Remoting,Apache Thrift,libeventRPC,XML-RPC 等等。
ProtoBuf开发者指南 – 非官方不完整版
ProtoBuf开发者指南
1 概览
欢迎来到protocolbuffer的开发者指南文档,一种语言无关、平台无关、扩展性好的用于通信协议、数据存储的结构化数据串行化方法。
本文档面向希望使用protocolbuffer的Java、C++或Python开发者。这个概览介绍了protocol buffer,并告诉你如何开始,你随后可以跟随编程指导(http://code.google.com/apis/protocolbuffers/docs/tutorials.html )深入了解protocol buffer编码方式(http://code.google.com/apis/protocolbuffers/docs/encoding.html )。API参考文档(http://code.google.com/apis/protocolbuffers/docs/reference/overview.html)同样也是提供了这三种编程语言的版本,不够协议语言(http://code.google.com/apis/protocolbuffers/docs/proto.html )和样式( http://code.google.com/apis/protocolbuffers/docs/style.html )指导都是编写 .proto 文件。
1.1 什么是protocol buffer
ProtocolBuffer是用于结构化数据串行化的灵活、高效、自动的方法,有如XML,不过它更小、更快、也更简单。你可以定义自己的数据结构,然后使用代码生成器生成的代码来读写这个数据结构。你甚至可以在无需重新部署程序的情况下更新数据结构。
1.2 他们如何工作
你首先需要在一个.proto 文件中定义你需要做串行化的数据结构信息。每个ProtocolBuffer信息是一小段逻辑记录,包含一系列的键值对。这里有个非常简单的 .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;
}
有如你所见,消息格式很简单,每个消息类型拥有一个或多个特定的数字字段,每个字段拥有一个名字和一个值类型。值类型可以是数字(整数或浮点)、布尔型、字符串、原始字节或者其他ProtocolBuffer类型,还允许数据结构的分级。你可以指定可选字段,必选字段和重复字段。你可以在(http://code.google.com/apis/protocolbuffers/docs/proto.html )找到更多关于如何编写 .proto 文件的信息。
一旦你定义了自己的报文格式(message),你就可以运行ProtocolBuffer编译器,将你的 .proto 文件编译成特定语言的类。这些类提供了简单的方法访问每个字段(像是query() 和set_query() ),像是访问类的方法一样将结构串行化或反串行化。例如你可以选择C++语言,运行编译如上的协议文件生成类叫做 Person 。随后你就可以在应用中使用这个类来串行化的读取报文信息。你可以这么写代码:
Person person;
person.set_name("John Doe");
person.set_id(1234);
person.set_email("jdoe@example.com");
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;
你可以在不影响向后兼容的情况下随意给数据结构增加字段,旧有的数据会忽略新的字段。所以如果使用ProtocolBuffer作为通信协议,你可以无须担心破坏现有代码的情况下扩展协议。
你可以在API参考(http://code.google.com/apis/protocolbuffers/docs/reference/overview.html)中找到完整的参考,而关于ProtocolBuffer的报文格式编码则可以在(http://code.google.com/apis/protocolbuffers/docs/encoding.html )中找到。
1.3 为什么不用XML?
ProtocolBuffer拥有多项比XML更高级的串行化结构数据的特性,ProtocolBuffer:
· 更简单
· 小3-10倍
· 快20-100倍
· 更少的歧义
· 可以方便的生成数据存取类
例如,让我们看看如何在XML中建模Person的name和email字段:
<person>
<name>John Doe</name>
<email>jdoe@example.com</email>
</person>
对应的ProtocolBuffer报文则如下:
#ProtocolBuffer的文本表示
#这不是正常时使用的二进制数据
person {
name: "John Doe"
email: "jdoe@example.com"
}
当这个报文编码到ProtocolBuffer的二进制格式(http://code.google.com/apis/protocolbuffers/docs/encoding.html )时(上面的文本仅用于调试和编辑),它只需要28字节和100-200ns的解析时间。而XML的版本需要69字节(除去空白)和5000-10000ns的解析时间。
当然,操作ProtocolBuffer也很简单:
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()
<< end;
当然,ProtocolBuffer并不是在任何时候都比XML更合适,例如ProtocolBuffer无法对一个基于标记文本的文档建模,因为你根本没法方便的在文本中插入结构。另外,XML是便于人类阅读和编辑的,而ProtocolBuffer则不是。还有XML是自解释的,而 ProtocolBuffer仅在你拥有报文格式定义的 .proto 文件时才有意义。
1.4 听起来像是为我的解决方案,如何开始?
下载包( http://code.google.com/p/protobuf/downloads/ ),包含了Java、Python、C++的ProtocolBuffer编译器,用于生成你需要的IO类。构建和安装你的编译器,跟随README的指令就可以做到。
一旦你安装好了,就可以跟着编程指导(http://code.google.com/apis/protocolbuffers/docs/tutorials.html )来选择语言-随后就是使用ProtocolBuffer创建一个简单的应用了。
1.5 一点历史
ProtocolBuffer最初是在Google开发的,用以解决索引服务器的请求、响应协议。在使用ProtocolBuffer之前,有一种格式用以处理请求和响应数据的编码和解码,并且支持多种版本的协议。而这最终导致了丑陋的代码,有如:
if (version==3) {
...
}else if (version>4) {
if (version==5) {
...
}
...
}
通信协议因此变得越来越复杂,因为开发者必须确保,发出请求的人和接受请求的人必须同时兼容,并且在一方开始使用新协议时,另外一方也要可以接受。
ProtocolBuffer设计用于解决这一类问题:
· 很方便引入新字段,而中间服务器可以忽略这些字段,直接传递过去而无需理解所有的字段。
· 格式可以自描述,并且可以在多种语言中使用(C++、Java等)
然而用户仍然需要手写解析代码。
随着系统的演化,他需要一些其他的功能:
· 自动生成编码和解码代码,而无需自己编写解析器。
· 除了用于简短的RPC(Remote Procedure Call)请求,人们使用ProtocolBuffer来做数据存储格式(例如BitTable)。
· RPC服务器接口可以作为 .proto 文件来描述,而通过ProtocolBuffer的编译器生成存根(stub)类供用户实现服务器接口。
ProtocolBuffer现在已经是Google的混合语言数据标准了,现在已经正在使用的有超过48,162种报文格式定义和超过12,183个.proto 文件。他们用于RPC系统和持续数据存储系统。
2 语言指导
本指导描述了如何使用ProtocolBuffer语言来定义结构化数据类型,包括 .proto 文件的语法和如何生成存取类。
这是一份指导手册,一步步的例子使用文档中的多种功能,查看入门指导(http://code.google.com/apis/protocolbuffers/docs/tutorials.html )选择你的语言。
2.1 定义一个消息类型
@waiting …
2.2 值类型
@waiting …
2.3 可选字段与缺省值
@waiting …
2.4 枚举
@waiting …
2.5 使用其他消息类型
@waiting …
2.6 嵌套类型
@waiting …
2.7 更新一个数据类型
@waiting …
2.8 扩展
@waiting …
2.9 包
@waiting …
2.10 定义服务
@waiting …
2.11 选项
@waiting …
2.12 生成你的类
@waiting …
3 代码风格指导
本文档提供了.proto 文件的代码风格指导。按照惯例,你将会,你将会生成一些便于阅读和一致的ProtocolBuffer定义文件。
3.1 消息与字段名
使用骆驼风格的大小写命名,即单词首字母大写,来做消息名。使用GNU的全部小写,使用下划线分隔的方式定义字段名:
message SongServerRequest {
required string song_name=1;
}
使用这种命名方式得到的名字如下:
C++:
const string& song_name() {...}
void set_song_name(const string& x) {...}
Java:
public String getSongName() {...}
public Builder setSongName(String v) {...}
3.2 枚举
使用骆驼风格做枚举名,而用全部大写做值的名字:
enum Foo {
FIRST_VALUE=1;
SECOND_VALUE=2;
}
每个枚举值最后以分号结尾,而不是逗号。
3.3 服务
如果你的.proto 文件定义了RPC服务,你可以使用骆驼风格:
service FooService {
rpc GetSomething(FooRequest) returns (FooResponse);
}
4 编码
本文档描述了ProtocolBuffer的串行化二进制数据格式定义。你如果仅仅是在应用中使用ProtocolBuffer,并不需要知道这些,但是这些会对你定义高效的格式有所帮助。
4.1 一个简单的消息
@waiting …
4.2 基于128的Varints
@waiting …
4.3 消息结构
@waiting …
4.4 更多的值类型
@waiting …
4.5 内嵌消息
@waiting …
4.6 可选的和重复的元素
@waiting …
4.7 字段顺序
@waiting …
5 ProtocolBuffer基础:C++
@waiting …
6 ProtocolBuffer基础:Java
@waiting …
7 ProtocolBuffer基础:Python
本指南给Python程序员一个快速使用的ProtocolBuffer的指导。通过一些简单的例子来在应用中使用ProtocolBuffer,它向你展示了如何:
· 定义 .proto 消息格式文件
· 使用ProtocolBuffer编译器
· 使用Python的ProtocolBuffer编程接口来读写消息
这并不是一个在Python中使用ProtocolBuffer的完整指导。更多细节请参考手册信息,查看语言指导(http://code.google.com/apis/protocolbuffers/docs/proto.html ),Python API(http://code.google.com/apis/protocolbuffers/docs/reference/python/index.html),和编码手册(http://code.google.com/apis/protocolbuffers/docs/encoding.html )。
7.1 为什么使用ProtocolBuffer?
下面的例子”地址本”应用用于读写人的联系信息。每个人有name、ID、email,和联系人电话号码。
如何串行化和读取结构化数据呢?有如下几种问题:
· 使用Python的pickle,这是语言内置的缺省方法,不过没法演化,也无法让其他语言支持。
· 你可以发明一种数据编码方法,例如4个整数”12:3-23:67″,这是简单而灵活的方法,不过你需要自己写解析器代码,且只适用于简单的数据。
· 串行化数据到XML。这种方法因为可读性和多种语言的兼容函数库而显得比较吸引人,不过这也不是最好的方法,因为XML浪费空间是臭名昭著的,编码解码也很浪费时间。而XML DOM树也是很复杂的。
ProtocolBuffer提供了灵活、高效、自动化的方法来解决这些问题。通过ProtocolBuffer,只需要写一个 .proto 数据结构描述文件,就可以编译到几种语言的自动编码解码类。生成的类提供了setter和getter方法来控制读写细节。最重要的是ProtocolBuffer支持后期扩展协议,而又确保旧格式可以兼容。
7.2 哪里可以找到例子代码
源码发行包中已经包含了,在”example”文件夹。
7.3 定义你的协议格式
想要创建你的地址本应用,需要开始于一个 .proto 文件。定义一个 .proto 文件很简单:添加一个消息到数据结构,然后指定一个和一个类型到每一个字段,如下是本次例子使用的 addressbook.proto
package tutorial;
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;
}
message AddressBook {
repeated Person person=1;
}
有如你所见的,语法类似于C++或Java。让我们分块理解他们。
@waiting …
7.4 编译你的ProtocolBuffer
现在已经拥有了.proto 文件,下一步就是编译生成相关的访问类。运行编译器protoc 编译你的 .proto 文件。
1. 如果还没安装编译器则下载并按照README的安装。
2. 运行编译器,指定源目录和目标目录,定位你的 .proto 文件到源目录,然后执行:
protoc -I=$SRC_DIR --python_out=$DST_DIR addressbook.proto
因为需要使用Python类,所以 --python_out 选项指定了特定的输出语言。
这个步骤会生成 addressbook_pb2.py 到目标目录。
7.5 ProtocolBuffer API
不像生成的C++和Java代码,Python生成的类并不会直接为你生成存取数据的代码。而是(有如你在 addressbook_pb2.py 中见到的)生成消息描述、枚举、和字段,还有一些神秘的空类,每个对应一个消息类型:
class Person(message.Message):
__metaclass__=reflection.GeneratedProtocolMessageType
class PhoneNumber(message.Message):
__metaclass__=reflection.GeneratedProtocolMessageType
DESCRIPTION=_PERSON_PHONENUMBER
DESCRIPTOR=_PERSON
class AddressBook(message.Message):
__metaclass__=reflection.GeneratedProtocolMessageType
DESCRIPTOR=_ADDRESSBOOK
这里每个类最重要的一行是__metaclass__=reflection.GeneratedProtocolMessageType 。通过Python的元类机制工作,你可以把他们看做是生成类的模板。在载入时,GeneratedProtocolMessageType 元类使用特定的描述符创建Python方法。随后你就可以使用完整的功能了。
最后就是你可以使用Person 类来操作相关字段了。例如你可以写:
import addressbook_pb2
person=addressbook_pb2.Person()
person.id=1234
person.name="John Doe"
person.email="jdoe@example.com"
phone=person.phone.add()
phone.number="555-4321"
phone.type=addressbook_pb2.Person.HOME
需要注意的是这些赋值属性并不是简单的增加新字段到Python对象,如果你尝试给一个 .proto 文件中没有定义的字段赋值,就会抛出 AttributeError 异常,如果赋值类型错误会抛出 TypeError 。在给一个字段赋值之前读取会返回缺省值:
person.no_such_field=1 #raise AttributeError
person.id="1234" #raise TypeError
更多相关信息参考(http://code.google.com/apis/protocolbuffers/docs/reference/python-generated.html )。
7.5.1 枚举
枚举在元类中定义为一些符号常量对应的数字。例如常量 addressbook_pb2.Person.WORK 拥有值2。
7.5.2 标准消息方法
每个消息类包含一些其他方法允许你检查和控制整个消息,包括:
· IsInitialized() :检查是否所有必须(required)字段都已经被赋值了。
· __str__() :返回人类可读的消息表示,便于调试。
· CopyFrom(other_msg) :使用另外一个消息的值来覆盖本消息。
· Clear() :清除所有元素的值,回到初识状态。
这些方法是通过接口Message 实现的,更多消息参考(http://code.google.com/apis/protocolbuffers/docs/reference/python/google.protobuf.message.Message-class.html )。
7.5.3 解析与串行化
最后,每个ProtocolBuffer类有些方法用于读写消息的二进制数据(http://code.google.com/apis/protocolbuffers/docs/encoding.html )。包括:
· SerializeToString() :串行化,并返回字符串。注意是二进制格式而非文本。
· ParseFromString(data) :解析数据。
他们是成对使用的,提供二进制数据的串行化和解析。另外参考消息API参考(http://code.google.com/apis/protocolbuffers/docs/reference/python/google.protobuf.message.Message-class.html )了解更多信息。
Note
ProtocolBuffer与面向对象设计
ProtocolBuffer类只是用于存取数据的,类似于C++中的结构体,他们并没有在面向对象方面做很好的设计。如果你想要给这些类添加更多的行为,最好的方法是包装(wrap)。包装同样适合于复用别人写好的 .proto 文件。这种情况下,你可以把ProtocolBuffer生成类包装的很适合于你的应用,并隐藏一些数据和方法,暴露有用的函数等等。 你不可以通过继承来给自动生成的类添加行为。 这会破坏他们的内部工作机制。
7.6 写消息
现在开始尝试使用ProtocolBuffer的类。第一件事是让地址本应用可以记录联系人的细节信息。想要做这些需要先创建联系人实例,然后写入到输出流。
这里的程序从文件读取地址本,添加新的联系人信息,然后写回新的地址本到文件。
#! /usr/bin/python
import addressbook_pb2
import sys
#这个函数使用用户输入填充联系人信息
def PromptForAddress(person):
person.id=int(raw_input("Enter person ID number: "))
person.name=raw_input("Enter name: ")
email=raw_input("Enter email address (blank for none): ")
if email!="":
person.email=email
while True:
number=raw_input("Enter a phone number (or leave blank to finish): ")
if number=="":
break
phone_number=person.phone.add()
phone_number.number=number
type=raw_input("Is this a mobile, home, or work phone? ")
if type=="mobile":
phone_number.type=addressbook_pb2.Person.MOBILE
elif type=="home":
phone_number.type=addressbook_pb2.Person.HOME
elif type=="work":
phone_number.type=addressbook_pb2.Person.WORK
else:
print "Unknown phone type; leaving as default value."
#主函数,从文件读取地址本,添加新的联系人,然后写回到文件
if len(sys.argv)!=2:
print "Usage:",sys.argv[0],"ADDRESS_BOOK_FILE"
sys.exit(-1)
address_book=addressbook_pb2.AddressBook()
#读取已经存在的地址本
try:
f=open(sys.argv[1],"fb")
address_book.ParseFromString(f.read())
f.close()
except OSError:
print sys.argv[1]+": Count open file. Creating a new one."
#添加地址
PromptFromAddress(address_book.person.add())
#写入到文件
f=open(sys.argv[1],"wb")
f.write(address_book.SerializeToString())
f.close()
7.7 读消息
当然,一个无法读取的地址本是没什么用处的,这个例子读取刚才创建的文件并打印所有信息:
#! /usr/bin/python
import addressbook_pb2
import sys
#遍历地址本中所有的人并打印出来
def ListPeople(address_book):
for person in address_book.person:
print "Person ID:",person.id
print " Name:",person.name
if person.HasField("email"):
print " E-mail:",person.email
for phone_number in person.phone:
if phone_number.type==addressbook_pb2.Person.MOBILE:
print " Mobile phone #:",
elif phone_number.type==addressbook_pb2.Person.HOME:
print " Home phone #:",
elif phone_number.type==addressbook_pb2.Person.WORK:
print " Work phone #:",
print phone_number.number
#主函数,从文件读取地址本
if len(sys.argv)!=2:
print "Usage:",sys.argv[0],"ADDRESS_BOOK_FILE"
sys.exit(-1)
address_book=addressbook_pb2.AddressBook()
#读取整个地址本文件
f=open(sys.argv[1],"rb")
address_book.ParseFromString(f.read())
f.close()
ListPeople(address_book)
7.8 扩展ProtocolBuffer
在你发不了代码以后,可能会想要改进ProtocolBuffer的定义。如果你想新的数据结构向后兼容,而你的旧数据可以向前兼容,那么你就找对了东西了,不过有些规则需要遵守。在新版本的ProtocolBuffer中:
· 必须不可以改变已经存在的标签的数字。
· 必须不可以增加或删除必须(required)字段。
· 可以删除可选(optional)或重复(repeated)字段。
· 可以添加新的可选或重复字段,但是必须使用新的标签数字,必须是之前的字段所没有用过的。
这些规则也有例外(http://code.google.com/apis/protocolbuffers/docs/proto.html#updating),不过很少使用。
如果你遵从这些规则,旧代码会很容易的读取新的消息,并简单的忽略新的字段。而对旧的被删除的可选字段也会简单的使用他们的缺省值,被删除的重复字段会自动为空。新的代码也会透明的读取旧的消息。然而,需要注意的是新的可选消息不会在旧的消息中显示,所以你需要使用 has_ 严格的检查他们是否存在,或者在 .proto 文件中提供一个缺省值。如果没有缺省值,就会有一个类型相关的默认缺省值:对于字符串就是空字符串;对于布尔型则是false;对于数字类型默认为0。同时要注意的是如果你添加了新的重复字段,你的新代码不会告诉你这个字段为空(新代码)也不会,也不会(旧代码)包含 has_ 标志。
7.9 高级使用
ProtocolBuffer不仅仅提供了数据结构的存取和串行化。查看Python API参考(http://code.google.com/apis/protocolbuffers/docs/reference/python/index.html)了解更多功能。
一个核心功能是通过消息类的映射(reflection)提供的。你可以通过它遍历消息的所有字段,和管理他们的值。关于映射的一个很有用的地方是转换到其他编码,如XML或JSON。一个使用映射的更高级的功能是寻找同类型两个消息的差异,或者开发出排序、正则表达式等功能。使用你的创造力,还可以用ProtocolBuffer实现比你以前想象的更多的问题。
映射是通过消息接口提供的。
8 参考概览
@waiting …
9 C++代码生成
@waiting …
10 C++API
@waiting …
11 Java代码生成
@waiting …
12 JavaAPI
@waiting …
13 Python代码生成
本页提供了Python生成类的相关细节。你可以在阅读本文档之前查看语言指导。
Python的ProtocolBuffer实现与C++和Java的略有不同,编译器只输出构建代码的描述符来生成类,而由Python的元类来执行工作。本文档描述了元类开始生效以后的东西。
13.1 编译器的使用
ProtocolBuffer通过编译器的 --python_out= 选项来生成Python的相关类。这个参数实际上是指定输出的Python类放在哪个目录下。编译器会为每个.proto 文件生成一个对应的 .py 文件。输出文件名与输入文件名相关,不过有两处修改:
· 扩展名 .proto 改为 .py 。
· 路径名的修改。
如果你按照如下调用编译器:
protoc --proto_path=src --python_out=build/gen src/foo.proto src/bar/baz.proto
编译器会自动读取两个.proto 文件然后产生两个输出文件。在需要时编译器会自动创建目录,不过 --python_out 指定的目录不会自动创建。
需要注意的是,如果.proto 文件名或路径包含有无法在Python中使用的模块名(如连字符),就会被自动转换为下划线。所以文件 foo-bar.proto 会变成foo_bar_pb2.py 。
Note
在每个文件后缀的 _pb2.py 中的2代表ProtocolBuffer版本2。版本1仅在Google内部使用,但是你仍然可以在以前发布的一些代码中找到它。自动版本2开始,ProtocolBuffer开始使用完全不同的接口了,从此Python也没有编译时类型检查了,我们加上这个版本号来标志Python文件名。
13.2 包
Python代码生成根本不在乎包的名字。因为Python使用目录名来做包名。
13.3 消息
先看看一个简单的消息声明:
message Foo {}
ProtocolBuffer编译器会生成类Foo,它是 google.protobuf.Message 的子类。这个实体类,不含有虚拟方法。不像C++和Java,Python生成类对优化选项不感冒;实际上Python的生成代码已经为代码大小做了优化。
你不能继承Foo的子类。生成类被设计不可以被继承,否则会被打破一些设计。另外,继承本类也是不好的设计。
Python的消息类没有特定的公共成员,而是定义接口,极其嵌套的字段、消息和枚举类型。
一个消息可以在另外一个消息中声明,例如 message Foo { message Bar {}} 。在这种情况下,Bar类定义为Foo的一个静态成员,所以你可以通过 Foo.Bar 来引用。
13.4 字段
对于消息类型中的每一个字段,都有对应的同名成员。
13.4.1 简单字段
如果你有一个简单字段(包括可选的和重复的),也就是非消息字段,你可以通过简单字段的方式来管理,例如foo字段的类型是int32,你可以:
message.foo=123
print message.foo
注意设置foo的值,如果类型错误会抛出TypeError。
如果foo在赋值之前就读取,就会使用缺省值。想要检查是否已经赋值,可以用HasField() ,而清除该字段的值用ClearField() 。例如:
assert not message.HasField("foo")
message.foo=123
assert message.HasField("foo")
message.ClearField("foo")
assert not message.HasField("foo")
13.4.2 简单消息字段
消息类型工作方式略有不同。你无法为一个嵌入消息字段赋值。而是直接操作这个消息的成员。因为实例化上层消息时,其包含的子消息同时也实例化了,例如定义:
message Foo {
optional Bar bar=1;
}
message bar {
optional int32 i=1;
}
你不可以这么做,因为不能做消息类型字段的赋值:
foo=Foo()
foo.bar=Bar() #WRONG!
而是可以直接对消息类型字段的成员赋值:
foo=Foo()
assert not foo.HasField("bar")
foo.bar.i=1
assert foo.HasField("bar")
注意简单的读取消息类型字段的未赋值成员只不过是打印其缺省值:
foo=Foo()
assert not foo.HasField("bar")
print foo.bar.i #打印i的缺省值
assert not foo.HasField("bar")
13.4.3 重复字段
重复字段表现的像是Python的序列类型。如果是嵌入的消息,你无法为字段直接赋值,但是你可以管理。例如给定的定义:
message Foo {
repeated int32 nums=1;
}
你就可以这么做:
foo=Foo()
foo.nums.append(15)
foo.nums.append(32)
assert len(foo.nums)==2
assert foo.nums[0]==15
assert foo.nums[1]==32
for i in foo.nums:
print i
foo.nums[1]=56
assert foo.nums[1]==56
作为一种简单字段,清除该字段必须使用 ClearField() 。
13.4.4 重复消息字段
重复消息字段工作方式与重复字段很像,除了 add() 方法用于返回新的对象以外。例如如下定义:
message Foo {
repeated Bar bar=1;
}
message Bar {
optional int32 i=1;
}
你可以这么做:
foo=Foo()
bar=foo.bars.add()
bar.i=15
bar=foo.bars.add()
bar.i=32
assert len(foo.bars)==2
assert foo.bars[0].i==15
assert foo.bars[1].i==32
for bar in foo.bars:
print bar.i
foo.bars[1].i=56
assert foo.bars[1].i==56
13.4.5 枚举类型
@waiting …
13.4.6 扩展
@waiting …
13.5 服务
13.5.1 接口
一个简单的接口定义:
service Foo {
rpc Bar(FooRequest) returns(FooResponse);
}
ProtocolBuffer的编译器会生成类 Foo 来展示这个服务。 Foo 将会拥有每个服务定义的方法。在这种情况下 Bar 方法的定义是:
def Bar(self,rpc_controller,request,done)
参数等效于Service.CallMethod() ,除了隐含的 method_descriptor 参数。
这些生成的方法被定义为可以被子类重载。缺省实现只是简单的调用 controller.SetFailed() 而抛出错误信息告之尚未实现。然后调用done回调。在实现你自己的服务时,你必须继承生成类,然后重载各个接口方法。
Foo继承了 Service 接口。ProtocolBuffer编译器会自动声响相关的实现方法:
· GetDescriptor :返回服务的 ServiceDescriptor 。
· CallMethod :检测需要调用哪个方法,并且直接调用。
· GetRequestClass 和 GetResponseClass :返回指定方法的请求和响应类。
13.5.2 存根(Stub)
ProtocolBuffer编译器也会为每个服务接口提供一个存根实现,用于客户端发送请求到服务器。对于Foo服务,存根实现是 Foo_Stub 。
Foo_Stub 是Foo的子类,他的构造器是一个 RpcChannel 。存根会实现调用每个服务方法的 CallMethod() 。
ProtocolBuffer哭并不包含RPC实现。然而,它包含了你构造服务类的所有工具,不过选择RPC实现则随你喜欢。你只需要提供 RpcChannel 和RpcController 的实现即可。
14 PythonAPI
@waiting …
15 其他语言
@waiting …
Protobuf动态解析那些事儿 - jacksu@tencent
需求背景
在接收到 protobuf 数据之后,如何自动创建具体的 Protobuf Message 对象,再做反序列化。“自动”的意思主要有两个方面:(1)当程序中新增一个 protobuf Message 类型时,这部分代码不需要修改,不需要自己去注册消息类型,不需要重启进程,只需要提供protobuf文件;(2)当protobuf Message修改后,这部分代码不需要修改,不需要自己去注册消息类型,不需要重启进程只需要提供修改后protobuf文件。
技术介绍
Protobuf的入门可以参考 Google Protocol Buffer 的在线帮助 网页 或者IBM developerwor上的文章 《Google Protocol Buffer 的使用和原理》 。
protobuf的动态解析在google protobuf buffer官网并没有什么介绍。通过google出的一些参考文档可以知道,其实,Google Protobuf 本身具有很强的反射(reflection)功能,可以根据 type name 创建具体类型的 Message 对象,我们直接利用即可,应该就可以满足上面的需求。
实现可以参考淘宝的文章 《玩转Protocol Buffers 》 ,里面对protobuf的动态解析的原理做了详细的介绍,在此我介绍一下Protobuf class diagram。
大家通常关心和使用的是图的左半部分:MessageLite、Message、Generated Message Types (Person, AddressBook) 等,而较少注意到图的右半部分:Descriptor, DescriptorPool, MessageFactory。
上图中,其关键作用的是 Descriptor class,每个具体 Message Type 对应一个 Descriptor 对象。尽管我们没有直接调用它的函数,但是Descriptor在“根据 type name 创建具体类型的 Message 对象”中扮演了重要的角色,起了桥梁作用。上图的红色箭头描述了根据 type name 创建具体 Message 对象的过程。
实现
先直接上代码,这个代码来自于 《玩转Protocol Buffers 》 :
#include <google/protobuf/descriptor.h> #include <google/protobuf/descriptor.pb.h> #include <google/protobuf/dynamic_message.h> #include <google/protobuf/compiler/importer.h> using namespace google::protobuf; using namespace google::protobuf::compiler; int main(int argc,const char *argv[]) DiskSourceTree sourceTree; //look up .proto file in current directory sourceTree.MapPath("","./"); Importer importer(&sourceTree, NULL); //runtime compile foo.proto importer.Import("foo.proto"); const Descriptor *descriptor = importer.pool()-> FindMessageTypeByName("Pair"); cout << descriptor->DebugString(); // build a dynamic message by "Pair" proto DynamicMessageFactory factory; const Message *message = factory.GetPrototype(descriptor); // create a real instance of "Pair" Message *pair = message->New(); // write the "Pair" instance by reflection const Reflection *reflection = pair->GetReflection(); const FieldDescriptor *field = NULL; field = descriptor->FindFieldByName("key"); reflection->SetString(pair, field,"my key"); field = descriptor->FindFieldByName("value"); reflection->SetUInt32(pair, field, 1111); cout << pair->DebugString(); |
那我们就来看看上面的代码
1 )把本地地址映射为虚拟地址
DiskSourceTree sourceTree;
//look up .proto file in current directory
sourceTree.MapPath("","./");
2 )构造 DescriptorPool
Importer importer(&sourceTree, NULL);
//runtime compile foo.proto
importer.Import("foo.proto");
3 )获取 Descriptor
const Descriptor *descriptor = importer.pool()->FindMessageTypeByName("Pair");
4 )通过 Descriptor获取Message
const Message *message = factory.GetPrototype(descriptor);
5 ) 根据类型信息使用 DynamicMessage new 出这个类型的一个空对象
Message *pair = message->New();
6 ) 通过 Message 的 reflection 操作 message 的各个字段
const Reflection *reflection = pair->GetReflection();
const FieldDescriptor *field = NULL;
field = descriptor->FindFieldByName("key");
reflection->SetString(pair, field,"my key");
field = descriptor->FindFieldByName("value");
reflection->SetUInt32(pair, field, 1111);
直接copy上面代码看起来我们上面的需求就满足了,只是唯一的缺点就是每次来个包加载一次配置文件,当时觉得性能应该和读取磁盘的性能差不多,但是经过测试性能极差,一个进程每秒尽可以处理1000多个包,经过分析性能瓶颈不在磁盘,而在频繁调用malloc和free上。
看来我们得重新考虑实现,初步的实现想法:只有protobuf描述文件更新时再重新加载,没有更新来包只需要使用加载好的解析就可以。这个方案看起来挺好的,性能应该不错,经过测试,性能确实可以,每秒可以处理3万左右的包,但是实现中遇到了困难。要更新原来的Message,必须更新Importer和Factory,那么要更新这些东西,就涉及到了资源的释放。经过研究这些资源的释放顺序特别重要,下面就介绍一下protobuf相关资源释放策略。
动态的Message是我们用DynamicMessageFactory构造出来的,因此销毁Message必须用同一个DynamicMessageFactory。 动态更新.proto文件时,我们销毁老的并使用新的DynamicMessageFactory,在销毁DynamicMessageFactory之前,必须先删除所有经过它构造的Message。
原理:DynamicMessageFactory里面包含DynamicMessage的共享信息,析构DynamicMessage时需要用到。生存期必须保持Descriptor>DynamicMessageFactory>DynamicMessage。
释放顺序必须是:释放所有DynamicMessage,释放DynamicMessageFactory,释放Importer。
总结
资源释放前,必须要了解资源的构造原理,通过构造原理反推释放顺序,这样就少走弯路、甚至不走。
参考文献
Google Protocol Buffer 的在线帮助 网页
一种自动反射消息类型的 Google Protobuf 网络传输方案
《Google Protocol Buffer 的使用和原理》
protobuf在网络编程中的应用思考
分类: 基础框架库2010-07-21 17:40 54306人阅读 评论(19) 收藏 举报
protobuf简介
protobuf是google提供的一个开源序列化框架,类似于XML,JSON这样的数据表示语言,其最大的特点是基于二进制,因此比传统的XML表示高效短小得多。虽然是二进制数据格式,但并没有因此变得复杂,开发人员通过按照一定的语法定义结构化的消息格式,然后送给命令行工具,工具将自动生成相关的类,可以支持java、c++、python等语言环境。通过将这些类包含在项目中,可以很轻松的调用相关方法来完成业务消息的序列化与反序列化工作。
protobuf在google中是一个比较核心的基础库,作为分布式运算涉及到大量的不同业务消息的传递,如何高效简洁的表示、操作这些业务消息在google这样的大规模应用中是至关重要的。而protobuf这样的库正好是在效率、数据大小、易用性之间取得了很好的平衡。
更多信息可参考官方文档
例子介绍
先下载protobuf-2.3.0.zip源代码库,下载后解压,选择vsprojects目录下的protobuf.sln解决方案打开,编译整个方案顺利成功。其中有一些测试工程,库相关的工程是libprotobuf、libprotobuf-lite、libprotoc和protoc。其中protoc是命令行工具。在example目录下有一个地址薄消息的例子,业务消息的定义文件后缀为.proto,其中的addressbook.proto内容为:
package tutorial;
option java_package = "com.example.tutorial";
option java_outer_classname = "AddressBookProtos";
message Person {
required string name = 1;
required int32 id = 2; // Unique ID number for this person.
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;
}
// Our address book file is just one of these.
message AddressBook {
repeated Person person = 1;
}
该定义文件,定义了地址薄消息的结构,顶层消息为AddressBook,其中包含多个Person消息,Person消息中又包含多个PhoneNumber消息。里面还定义了一个PhoneType的枚举类型。
类型前面有required表示必须,optional表示可选,repeated表示重复,这些定义都是一目了然的,无须多说。关于消息定义的详细语法可参考官方文档。
现在用命令行工具来生成业务消息类,切换到protoc.exe所在的debug目录,在命令行敲入:
protoc.exe--proto_path=../../examples --cpp_out=../../examples ../../examples/addressbook.proto
该命令中--proto_path参数表示.proto消息定义文件路径,--cpp_out表示输出c++类的路径,后面接着是addressbook.proto消息定义文件。该命令会读取addressbook.proto文件并生成对应的c++类头文件和实现文件。执行完后在examples目录生存了addressbook.pb.h和addressbook.pb.cpp。
现在新建两个空控制台工程,第一个不妨叫AddPerson,然后把examples目录下的add_person.cc、addressbook.pb.h和addressbook.pb.cpp加入到该工程,另一个工程不妨叫ListPerson,将examples目录下的list_people.cc、addressbook.pb.h和addressbook.pb.cpp加入到该工程,在两个工程的项目属性中附加头文件路径../src。两个工程的项目依赖都选择libprotobuf工程(库)。
给AddPerson工程添加一个命令行参数比如叫addressbook.dat用于将地址薄信息序列化写入该文件,然后编译运行AddPerson工程,根据提示输入地址薄信息:
输入完成后,将序列化到addressbook.dat文件中。
在ListPerson工程的命令行参数中加读取文件参数../AddPerson/addressbook.dat,然后在运行ListPerson工程,可在list_people.cc的最后设个断点,避免命令行窗口运行完后关闭看不到结果:
写入地址薄的操作,关键操作就是调用address_book.SerializeToOstream进行序列化到文件流。
而读取操作中就是address_book.ParseFromIstream从文件流反序列化,这都是框架自动生成的类中的方法。
其他操作都是业务消息的字段set/get之类的对象级操作,很明了。更详细的API参考官方文档有详细说明。
在TCP网络编程中的考虑
从上面的例子可以看出protobuf这样的库是很方便高效的,那么自然的想到在网络编程中用来做业务消息的序列化、反序列化支持。在基于UDP协议的网络应用中,由于UDP本身是有边界,那么用protobuf来处理业务消息就很方便。但在TCP应用中,由于TCP协议没有消息边界,这就需要有一种机制来确定业务消息边界。在TCP网络编程中这是必须面对的问题。
注意上面的address_book.ParseFromIstream调用,如果流参数的内容多一个字节或者少一个字节,该方法都会返回失败(虽然某些字段可能正确得到结果了),也就是说送给反序列化的数据参数除了格式正确还必须有正确的大小。因此在tcp网络编程中,要反序列化业务消息,就要先知道业务数据的大小。而且在实际应用中可能在一个发送操作中,发送多个业务消息,而且每个业务消息的大小、类型都不一样。而且可能发送很大的数据流,比如文件。
显然消息边界的确认问题和protobuf库无关,还得自己搞定。在官方文档中也提到,protobuf并不太适合来作大数据的处理,当业务消息超过1M时,就应该考虑是否应该用另外的替代方案。当然对于大数据,你也可以分割为多个小块用protobuf做小块消息封装进行传递。但对很多应用这样的作法显得比较多余,比如发送一个大的文件,一般是在接收方从协议栈收到多少数据就写多少数据到磁盘,这是一种边接收边处理的流模式,这种模式基本上和每次收到的数据量没有关系。这种模式下再采用分割成小消息进行反序列化就显得多此一举了。
由于每个业务消息的大小和处理方式都可能不一样,那么就需要独立抽象出一个边界消息来区分不同的业务消息,而且这个边界消息的格式和大小必须固定。对于网络编程熟手,可能早已经想到了这样的消息,我们可以结合protobuf库来定义一个边界消息,不妨叫BoundMsg:
message BoundMsg
{
required int32 msg_type = 1;
required int32 msg_size = 2;
}
可以根据需要扩充一些字段,但最基本的这两个字段就够用了。我们只需要知道业务消息的类型和大小即可。这个消息大小是固定的8字节,专门用来确定数据流的边界。有了这样的边界消息,在接收端处理任何业务消息就很灵活方便了,下面是接收端处理的简单伪代码示例:
if(net_read(buf,8))
{
boundMsg.ParseFromIstream(buf);
switch(boundMsg.msg_type)
{
case BO_1:
if(net_read(bo1Buf,boundMsg.msg_size))
{
bo1.ParseFromIstream(bo1Buf);
....
}
break;
case BO_2:
if(net_read(bo2Buf,boundMsg.msg_size))
{
bo2.ParseFromIstream(bo2Buf);
....
}
break;
case FILE_DATA:
count = 0;
while(count < boundMsg.msg_size)
{
piece_size = net_read(fileBuf,1024);
write_file(filename,fileBuf,piece_size);
count = count + piece_size;
}
break;
}
}
注意上面如果FILE_DATA消息后,还紧接其他业务消息的话,需要小心,即count累计出的值可能大于
boundMsg.msg_size的值,那么多出来的实际上应该是下一个边界消息数据了。为了避免处理的复杂性,上面所有的循环网络读取操作(上面BO_1,BO_2都可能需要循环读取,为了简化没有写成循环)的缓冲区位置和大小参数应该动态调整,即每次读取时传递的都是还期望读取的数据大小,对于文件的话,可能特殊点,因为边读取边写入,就没有必要事先要分配一个文件大小的缓冲区来存放数据了。对于文件分配一个小缓冲区来读,注意确认下边界即可。
上面是我的一点考虑,不妥之处还请大家讨论交流。想想借助于ACE、MINA这样的网络编程框架,然后结合protobuf这样的序列化框架,网络编程中技术基础设施层面的东西就给我们解决得差不多了,我们可以真正只关注于业务的实现。