return-1;
}
}
ListMsg(msg1);
…
}
同样,Reader声明类helloworld的对象msg1,然后利用ParseFromIstream从一个fstream流中读取信息并反序列化。
此后,ListMsg中采用get方法读取消息的内部信息,并进行打印输出操作。
运行结果
运行Writer和Reader的结果如下:
>writer
>reader
101
Hello
Reader读取文件log中的序列化信息并打印到屏幕上。
本文中所有的例子代码都可以在附件中下载。
您可以亲身体验一下。
这个例子本身并无意义,但只要您稍加修改就可以将它变成更加有用的程序。
比如将磁盘替换为网络socket,那么就可以实现基于网络的数据交换任务。
而存储和交换正是Protobuf最有效的应用领域。
和其他类似技术的比较
看完这个简单的例子之后,希望您已经能理解Protobuf能做什么了,那么您可能会说,世上还有很多其他的类似技术啊,比如XML,JSON,Thrift等等。
和他们相比,Protobuf有什么不同呢?
简单说来Protobuf的主要优点就是:
简单,快。
这有测试为证,项目thrift-protobuf-compare比较了这些类似的技术,图1显示了该项目的一项测试结果,TotalTime.
图1.性能测试结果
TotalTime指一个对象操作的整个时间,包括创建对象,将对象序列化为内存中的字节序列,然后再反序列化的整个过程。
从测试结果可以看到Protobuf的成绩很好,感兴趣的读者可以自行到网站
Protobuf的优点
Protobuf有如XML,不过它更小、更快、也更简单。
你可以定义自己的数据结构,然后使用代码生成器生成的代码来读写这个数据结构。
你甚至可以在无需重新部署程序的情况下更新数据结构。
只需使用Protobuf对数据结构进行一次描述,即可利用各种不同语言或从各种不同数据流中对你的结构化数据轻松读写。
它有一个非常棒的特性,即“向后”兼容性好,人们不必破坏已部署的、依靠“老”数据格式的程序就可以对数据结构进行升级。
这样您的程序就可以不必担心因为消息结构的改变而造成的大规模的代码重构或者迁移的问题。
因为添加新的消息中的field并不会引起已经发布的程序的任何改变。
Protobuf语义更清晰,无需类似XML解析器的东西(因为Protobuf编译器会将.proto文件编译生成对应的数据访问类以对Protobuf数据进行序列化、反序列化操作)。
使用Protobuf无需学习复杂的文档对象模型,Protobuf的编程模式比较友好,简单易学,同时它拥有良好的文档和示例,对于喜欢简单事物的人们而言,Protobuf比其他的技术更加有吸引力。
Protobuf的不足
Protbuf与XML相比也有不足之处。
它功能简单,无法用来表示复杂的概念。
XML已经成为多种行业标准的编写工具,Protobuf只是Google公司内部使用的工具,在通用性上还差很多。
由于文本并不适合用来描述数据结构,所以Protobuf也不适合用来对基于文本的标记文档(如HTML)建模。
另外,由于XML具有某种程度上的自解释性,它可以被人直接读取编辑,在这一点上Protobuf不行,它以二进制的方式存储,除非你有.proto定义,否则你没法直接读出Protobuf的任何内容【2】。
高级应用话题
更复杂的Message
到这里为止,我们只给出了一个简单的没有任何用处的例子。
在实际应用中,人们往往需要定义更加复杂的Message。
我们用“复杂”这个词,不仅仅是指从个数上说有更多的fields或者更多类型的fields,而是指更加复杂的数据结构:
嵌套Message
嵌套是一个神奇的概念,一旦拥有嵌套能力,消息的表达能力就会非常强大。
代码清单4给出一个嵌套Message的例子。
清单4.嵌套Message的例子
messagePerson{
requiredstringname=1;
requiredint32id=2;//UniqueIDnumberforthisperson.
optionalstringemail=3;
enumPhoneType{
MOBILE=0;
HOME=1;
WORK=2;
}
messagePhoneNumber{
requiredstringnumber=1;
optionalPhoneTypetype=2[default=HOME];
}
repeatedPhoneNumberphone=4;
}
在MessagePerson中,定义了嵌套消息PhoneNumber,并用来定义Person消息中的phone域。
这使得人们可以定义更加复杂的数据结构。
4.1.2ImportMessage
在一个.proto文件中,还可以用Import关键字引入在其他.proto文件中定义的消息,这可以称做ImportMessage,或者DependencyMessage。
比如下例:
清单5.代码
importcommon.header;
messageyouMsg{
requiredcommon.info_headerheader=1;
requiredstringyouPrivateData=2;
}
其中,common.info_header定义在common.header包内。
ImportMessage的用处主要在于提供了方便的代码管理机制,类似C语言中的头文件。
您可以将一些公用的Message定义在一个package中,然后在别的.proto文件中引入该package,进而使用其中的消息定义。
GoogleProtocolBuffer可以很好地支持嵌套Message和引入Message,从而让定义复杂的数据结构的工作变得非常轻松愉快。
动态编译
一般情况下,使用Protobuf的人们都会先写好.proto文件,再用Protobuf编译器生成目标语言所需要的源代码文件。
将这些生成的代码和应用程序一起编译。
可是在某且情况下,人们无法预先知道.proto文件,他们需要动态处理一些未知的.proto文件。
比如一个通用的消息转发中间件,它不可能预知需要处理怎样的消息。
这需要动态编译.proto文件,并使用其中的Message。
Protobuf提供了 google:
:
protobuf:
:
compiler包来完成动态编译的功能。
主要的类叫做importer,定义在importer.h中。
使用Importer非常简单,下图展示了与Import和其它几个重要的类的关系。
图2.Importer类
Import类对象中包含三个主要的对象,分别为处理错误的MultiFileErrorCollector类,定义.proto文件源目录的SourceTree类。
下面还是通过实例说明这些类的关系和使用吧。
对于给定的proto文件,比如lm.helloworld.proto,在程序中动态编译它只需要很少的一些代码。
如代码清单6所示。
清单6.代码
google:
:
protobuf:
:
compiler:
:
MultiFileErrorCollectorerrorCollector;
google:
:
protobuf:
:
compiler:
:
DiskSourceTreesourceTree;
google:
:
protobuf:
:
compiler:
:
Importerimporter(&sourceTree,&errorCollector);
sourceTree.MapPath("",protosrc);
importer.import(“lm.helloworld.proto”);
首先构造一个importer对象。
构造函数需要两个入口参数,一个是sourceTree对象,该对象指定了存放.proto文件的源目录。
第二个参数是一个errorcollector对象,该对象有一个AddError方法,用来处理解析.proto文件时遇到的语法错误。
之后,需要动态编译一个.proto文件时,只需调用importer对象的import方法。
非常简单。
那么我们如何使用动态编译后的Message呢?
我们需要首先了解几个其他的类
Packagegoogle:
:
protobuf:
:
compiler中提供了以下几个类,用来表示一个.proto文件中定义的message,以及Message中的field,如图所示。
图3.各个Compiler类之间的关系
类FileDescriptor表示一个编译后的.proto文件;类Descriptor对应该文件中的一个Message;类FieldDescriptor描述一个Message中的一个具体Field。
比如编译完lm.helloworld.proto之后,可以通过如下代码得到lm.helloworld.id的定义:
清单7.得到lm.helloworld.id的定义的代码
constprotobuf:
:
Descriptor*desc=
importer_.pool()->FindMessageTypeByName(“lm.helloworld”);
constprotobuf:
:
FieldDescriptor*field=
desc->pool()->FindFileByName(“id”);
通过Descriptor,FieldDescriptor的各种方法和属性,应用程序可以获得各种关于Message定义的信息。
比如通过field->name()得到field的名字。
这样,您就可以使用一个动态定义的消息了。
编写新的proto编译器
随GoogleProtocolBuffer源代码一起发布的编译器protoc支持3种编程语言:
C++,java和Python。
但使用GoogleProtocolBuffer的Compiler包,您可以开发出支持其他语言的新的编译器。
类CommandLineInterface封装了protoc编译器的前端,包括命令行参数的解析,proto文件的编译等功能。
您所需要做的是实现类CodeGenerator的派生类,实现诸如代码生成等后端工作:
程序的大体框架如图所示:
图4.XML编译器框图
在main()函数内,生成CommandLineInterface的对象cli,调用其RegisterGenerator()方法将新语言的后端代码生成器yourG对象注册给cli对象。
然后调用cli的Run()方法即可。
这样生成的编译器和protoc的使用方法相同,接受同样的命令行参数,cli将对用户输入的.proto进行词法语法等分析工作,最终生成一个语法树。
该树的结构如图所示。
图5.语法树
其根节点为一个FileDescriptor对象(请参考“动态编译”一节),并作为输入参数被传入yourG的Generator()方法。
在这个方法内,您可以遍历语法树,然后生成对应的您所需要的代码。
简单说来,要想实现一个新的compiler,您只需要写一个main函数,和一个实现了方法Generator()的派生类即可。
在本文的下载附件中,有一个参考例子,将.proto文件编译生成XML的compiler,可以作为参考。
Protobuf的更多细节
人们一直在强调,同XML相比,Protobuf的主要优点在于性能高。
它以高效的二进制方式存储,比XML小3到10倍,快20到100倍。
对于这些“小3到10倍”,“快20到100倍”的说法,严肃的程序员需要一个解释。
因此在本文的最后,让我们稍微深入Protobuf的内部实现吧。
有两项技术保证了采用Protobuf的程序能获得相对于XML极大的性能提高。
第一点,我们可以考察Protobuf序列化后的信息内容。
您可以看到ProtocolBuffer信息的表示非常紧凑,这意味着消息的体积减少,自然需要更少的资源。
比如网络上传输的字节数更少,需要的IO更少等,从而提高性能。
第二点我们需要理解Protobuf封解包的大致过程,从而理解为什么会比XML快很多。
GoogleProtocolBuffer的Encoding
Protobuf序列化后所生成的二进制消息非常紧凑,这得益于Protobuf采用的非常巧妙的Encoding方法。
考察消息结构之前,让我首先要介绍一个叫做Varint的术语。
Varint是一种紧凑的表示数字的方法。
它用一个或多个字节来表示一个数字,值越小的数字使用越少的字节数。
这能减少用来表示数字的字节数。
比如对于int32类型的数字,一般需要4个byte来表示。
但是采用Varint,对于很小的int32类型的数字,则可以用1个byte来表示。
当然凡事都有好的也有不好的一面,采用Varint表示法,大的数字则需要5个byte来表示。
从统计的角度来说,一般不会所有的消息中的数字都是大数,因此大多数情况下,采用Varint后,可以用更少的字节数来表示数字信息。
下面就详细介绍一下Varint。
Varint中的每个byte的最高位bit有特殊的含义,如果该位为1,表示后续的byte也是该数字的一部分,如果该位为0,则结束。
其他的7个bit都用来表示数字。
因此小于128的数字都可以用一个byte表示。
大