转自:http://www.usidcbbs.com/simple/?t5565.html
简介
什么是 Google Protocol Buffer? 假如您在网上搜索,应该会得到类似这样的文字介绍:Google Protocol Buffer( 简称 Protobuf) 是 Google 公司内部的混合语言数据标准,目前已经正在使用的有超过 48,162 种报文格式定义和超过 12,183 个 .proto 文件。他们用于 RPC 系统和持续数据存储系统。
[pre] tar -xzf protobuf-2.1.0.tar.gz cd protobuf-2.1.0 ./configure --prefix=$INSTALL_DIR make make check make install [/pre] |
[pre] package lm; message helloworld { required int32 id = 1; // ID required string str = 2; // str optional int32 opt = 3; //optional field } [/pre] |
[pre] packageName.MessageName.proto [/pre] |
[pre] protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto [/pre] |
[pre] #include "lm.helloworld.pb.h"… int main(void) { lm::helloworld msg1; msg1.set_id(101); msg1.set_str(“hello”); // Write the new address book back to disk. fstream output("./log", ios::out | ios::trunc | ios::binary); if (!msg1.SerializeToOstream(&output)) { cerr << "Failed to write msg." << endl; return -1; } return 0; } [/pre] |
[pre] #include "lm.helloworld.pb.h" … void ListMsg(const lm::helloworld & msg) { cout << msg.id() << endl; cout << msg.str() << endl; } int main(int argc, char* argv[]) { lm::helloworld msg1; { fstream input("./log", ios::in | ios::binary); if (!msg1.ParseFromIstream(&input)) { cerr << "Failed to parse address book." << endl; return -1; } } ListMsg(msg1); … } [/pre] |
[pre] >writer >reader 101 Hello [/pre] |
[pre] 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; } [/pre] |
[pre] import common.header; message youMsg{ required common.info_header header = 1; required string youPrivateData = 2; } [/pre] |
[pre] google::protobuf::compiler::MultiFileErrorCollector errorCollector; google::protobuf::compiler::DiskSourceTree sourceTree; google::protobuf::compiler::Importer importer(&sourceTree, &errorCollector); sourceTree.MapPath("", protosrc); importer.import(“lm.helloworld.proto”); [/pre] |
[pre] const protobuf::Descriptor *desc = importer_.pool()->FindMessageTypeByName(“lm.helloworld”); const protobuf::FieldDescriptor* field = desc->pool()->FindFileByName (“id”); [/pre] |
[pre] Test1.id = 10; Test1.str = “hello”;[/pre] |
[pre] (field_number << 3) | wire_type [/pre] |
Type | Meaning | Used For |
0 | Varint | int32, int64, uint32, uint64, sint32, sint64, bool, enum |
1 | 64-bit | fixed64, sfixed64, double |
2 | Length-delimi | string, bytes, embedded messages, packed repeated fields |
3 | Start group | Groups (deprecated) |
4 | End group | Groups (deprecated) |
5 | 32-bit | fixed32, sfixed32, float |
[pre] 08 65 12 06 48 65 6C 6C 6F 77 [/pre] |
[pre] 31 30 31 3C 2F 69 64 3E 3C 6E 61 6D 65 3E 68 65 6C 6C 6F 3C 2F 6E 61 6D 65 3E 3C 2F 68 65 6C 6C 6F 77 6F 72 6C 64 3E 一共 55 个字节,这些奇怪的数字需要稍微解释一下,其含义用 ASCII 表示如下: |
|
|
|
usidc5 | 2013-01-18 22:59 |
今天来介绍一下“Protocol Buffers ”(以下简称protobuf)这个玩意儿。本来俺在构思“生产者/消费者模式 ”系列的下一个帖子:关于生产者和消费者之间的数据传输格式。由于里面扯到了protobuf,想想干脆单独开一个帖子算了。 ★protobuf是啥玩意儿? 为了照顾从没听说过的同学,照例先来扫盲一把。 首先,protobuf是一个开源 项 目(官方站点在“这里 ”),而且是后台很硬的开源项目。网上现有的大部分(至少80%)开源项目,要么是某人单干、要么是几个闲杂人等合伙搞。而protobuf则不然,它是 鼎鼎大名的Google公司开发出来,并且在Google内部久经考验的一个东东。由此可见,它的作者绝非一般闲杂人等可比。 那这个听起来牛X的东东到底有啥用处捏?简单地说,这个东东干的事儿其实和XML 差不多,也就是把某种数据结构的信息,以某种格式保存起来。主要用于数据存储、传输协议格式等场合。有同学可能心理犯嘀咕了:放着好好的XML不用,干嘛重新发明轮子啊?!先别急,后面俺自然会有说道。 话说到了去年(大约是08年7月),Google突然大发慈悲,把这个好东西贡献给了开源社区。这下,像俺这种喜欢捡现成的家伙可就有福啦!貌似喜欢 捡现成的家伙还蛮多滴,再加上 Google的号召力,开源后不到一年,protobuf的人气就已经很旺了。所以俺为了与时俱进,就单独开个帖子来忽悠一把。 ★protobuf有啥特色? 扫盲完了之后,就该聊一下技术 方面的话题了。由于这玩意儿发布的时间较短(未满周岁),所以俺接触的时间也不长。今天在此是先学现卖,列位看官多多包涵 :-) ◇性能好/效率高 现在,俺就来说说Google公司为啥放着好端端的XML不用,非要另起炉灶,重新造轮子。一个根本的原因是XML性能不够好。 先说时间开销:XML格式化(序列化)的开销倒还好;但是XML解析(反序列化)的开销就不敢恭维啦。俺之前经常碰到一些时间性能很敏感的场合,由于不堪忍受XML解析的速度,弃之如敝履。 再来看空间开销:熟悉XML语法的同学应该知道,XML格式为了有较好的可读性,引入了一些冗余的文本信息。所以空间开销也不是太好(不过这点缺点,俺不常碰到)。 由于Google公司赖以吹嘘的就是它的海量数据和海量处理能力。对于几十万、上百万机器的集群,动不动就是PB级的数据量,哪怕性能稍微提高 0.1% 也是相当可观滴。所以Google自然无法容忍XML在性能上的明显缺点。再加上Google从来就不缺造轮子的牛人,所以protobuf也就应运而生 了。 Google对于性能的偏执,那可是出了名的。所以,俺对于Google搞出来protobuf是非常滴放心,性能上不敢说是最好,但肯定不会太差。 ◇代码 生成机制 除了性能好,代码生成机制是主要吸引俺的地方。为了说明这个代码生成机制,俺举个例子。 比如有个电子商务的系统(假设用C++实现),其中的模块A需要发送大量的订单信息给模块B,通讯的方式使用socket。 假设订单包括如下属性: -------------------------------- 时间:time(用整数表示) 客户id:userid(用整数表示) 交易金额:price(用浮点数表示) 交易的描述:desc(用字符串表示) -------------------------------- 如果使用protobuf实现,首先要写一个proto文件(不妨叫Order.proto),在该文件中添加一个名为"Order"的message结构,用来描述通讯协议中的结构化数据。该文件的内容大致如下: -------------------------------- message Order { required int32 time = 1; required int32 userid = 2; required float price = 3; optional string desc = 4; } -------------------------------- 然后,使用protobuf内置的编译器编译 该proto。由于本例子的模块是C++,你可以通过protobuf编译器的命令行参数(看“这里 ”),让它生成C++语言的“订单包装类”。(一般来说,一个message结构会生成一个包装类) 然后你使用类似下面的代码来序列化/解析该订单包装类: -------------------------------- // 发送方 Order order; order.set_time(XXXX); order.set_userid(123); order.set_price(100.0f); order.set_desc("a test order"); string sOrder; order.SerailzeToString(&sOrder); // 然后调用某种socket的通讯库把序列化之后的字符串发送出去 // ...... -------------------------------- // 接收方 string sOrder; // 先通过网络通讯库接收到数据,存放到某字符串sOrder // ...... Order order; if(order.ParseFromString(sOrder)) // 解析该字符串 { cout << "userid:" << order.userid() << endl << "desc:" << order.desc() << endl; } else { cerr << "parse error!" << endl; } -------------------------------- 有了这种代码生成机制,开发人员再也不用吭哧吭哧地编写那些协议解析的代码了(干这种活是典型的吃力不讨好)。 万一将来需求发生变更,要求给订单再增加一个“状态”的属性,那只需要在Order.proto文件中增加一行代码。对于发送方(模块A),只要增加一行设置状态的代码;对于接收方(模块B)只要增加一行读取状态的代码。哇塞,简直太轻松了! 另外,如果通讯双方使用不同的编程语言来实现,使用这种机制可以有效确保两边的模块对于协议的处理是一致的。 顺便跑题一下。 从某种意义上讲,可以把proto文件看成是描述通讯协议的规格说明书(或者叫接口规范)。这种伎俩其实老早就有了,搞过微软的COM编程或者接触过CORBA的同学,应该都能从中看到IDL(详细解释看“这里 ”)的影子。它们的思想是相通滴。 ◇支持“向后兼容”和“向前兼容” 还是拿刚才的例子来说事儿。为了叙述方便,俺把增加了“状态”属性的订单协议成为“新版本”;之前的叫“老版本”。 所谓的“向后兼容”(backward compatible),就是说,当模块B升级了之后,它能够正确识别模块A发出的老版本的协议。由于老版本没有“状态”这个属性,在扩充协议时,可以考 虑把“状态”属性设置成非必填 的,或者给“状态”属性设置一个缺省值(如何设置缺省值,参见“这里 ”)。 所谓的“向前兼容”(forward compatible),就是说,当模块A升级了之后,模块B能够正常识别模块A发出的新版本的协议。这时候,新增加的“状态”属性会被忽略。 “向后兼容”和“向前兼容”有啥用捏?俺举个例子:当你维护一个很庞大的分布式系统时,由于你无法同时 升级所有 模块,为了保证在升级过程中,整个系统能够尽可能不受影响,就需要尽量保证通讯协议的“向后兼容”或“向前兼容”。 ◇支持多种编程语言 俺开博以来点评 的几个开源项目(比如“Sqlite ”、“cURL ”),都是支持很多种 编程语言滴,这次的protobuf也不例外。在Google官方发布的源代码中包含了C++、Java 、Python三种语言(正好也是俺最常用的三种,真爽)。如果你平时用的就是这三种语言之一,那就好办了。 假如你想把protobuf用于其它语言,咋办捏?由于Google一呼百应的号召力,开源社区对protobuf响应踊跃,近期冒出很多其它编程语言的版本(比如ActionScript、C#、Lisp、Erlang、Perl、PHP 、Ruby等),有些语言还同时搞出了多个开源的项目。具体细节可以参见“这里 ”。 不过俺有义务提醒一下在座的各位同学。如果你考虑把protobuf用于上述这些语言,一定认真评估对应的开源库。因为这些开源库不是Google官方提供的、而且出来的时间还不长。所以,它们的质量、性能等方面可能还有欠缺。 ★protobuf有啥缺陷? 前几天刚刚在“光环效应 ”的帖子里强调了“要同时评估优点和缺点”。所以俺最后再来批判一下这玩意儿的缺点。 ◇应用 不够广 由于protobuf刚公布没多久,相比XML而言,protobuf还属于初出茅庐。因此,在知名度、应用广度等方面都远不如XML。由于这个原因,假如你设计的系统需要提供若干对外的接口给第三方系统调用,俺奉劝你暂时不要考虑protobuf格式。 ◇二进制格式导致可读性差 为了提高性能,protobuf采用了二进制格式进行编码。这直接导致了可读性差的问题(严格地说,是没有可读性)。虽然protobuf提供了TextFormat这个工具类(文档在“这里 ”),但终究无法彻底解决此问题。 可读性差的危害,俺再来举个例子。比如通讯双方如果出现问题,极易导致扯皮(都不承认自己有问题,都说是对方的错)。俺对付扯皮的一个简单方法 就是直接抓包并dump成log,能比较容易地看出错误在哪一方。但是protobuf的二进制格式,导致你抓包并直接dump出来的log难以看懂。 ◇缺乏自描述 一般来说,XML是自描述的,而protobuf格式则不是。给你一段二进制格式的协议内容,如果不配合相应的proto文件,那简直就像天书一般。 由于“缺乏自描述”,再加上“二进制格式导致可读性差”。所以在配置文件方面,protobuf是肯定无法取代XML的地位滴。 ★为什么俺会用上protobuf? 俺自从前段时间接触了protobuf之后,就着手把俺负责的产品中的部分 数据传输协议替换成protobuf。可能有同学会问,和protobuf类似的东东也有不少,为啥独独相中protobuf捏?由于今天写的篇幅已经蛮 长了,俺卖个关子,把这个话题留到“生产者/消费者模式[5]:如何选择传输协议及格式?”。俺会在后续的这个帖子里对比各种五花八门的协议格式,并谈谈 俺的浅见 |