Google Protocol Buffer 的使用和原理
跳转到主要内容

- 登录 (或注册)
- 中文

- 技术主题
- 软件下载
- 社区
- 技术讲座

- developerWorks 中国
- Linux
- 文档库
Google Protocol Buffer 的使用和原理
?
刘 明, 软件工程师, 上海交大电子与通信系?
简介:?Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,很适合做数据存储或 RPC 数据交换格式。它可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。目前提供了 C++、Java、Python 三种语言的 API。
?标记本文!?
发布日期:?2010 年 11 月 18 日




简介
什么是 Google Protocol Buffer? 假如您在网上搜索,应该会得到类似这样的文字介绍:
Google Protocol Buffer( 简称 Protobuf) 是 Google 公司内部的混合语言数据标准,目前已经正在使用的有超过 48,162 种报文格式定义和超过 12,183 个 .proto 文件。他们用于 RPC 系统和持续数据存储系统。
Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。目前提供了 C++、Java、Python 三种语言的 API。
或许您和我一样,在第一次看完这些介绍后还是不明白 Protobuf 究竟是什么,那么我想一个简单的例子应该比较有助于理解它。
回页首
一个简单的例子
安装 Google Protocol Buffer
在网站
关于简单例子的描述
我打算使用 Protobuf 和 C++ 开发一个十分简单的例子程序。
该程序由两部分组成。第一部分被称为 Writer,第二部分叫做 Reader。
Writer 负责将一些结构化的数据写入一个磁盘文件,Reader 则负责从该磁盘文件中读取结构化数据并打印到屏幕上。
准备用于演示的结构化数据是 HelloWorld,它包含两个基本数据:
- ID,为一个整数类型的数据
- Str,这是一个字符串
书写 .proto 文件
首先我们需要编写一个 proto 文件,定义我们程序中需要处理的结构化数据,在 protobuf 的术语中,结构化数据被称为 Message。proto 文件非常类似 java 或者 C 语言的数据定义。代码清单 1 显示了例子应用中的 proto 文件内容。
清单 1. proto 文件编译 .proto 文件
写好 proto 文件之后就可以用 Protobuf 编译器将该文件编译成目标语言了。本例中我们将使用 C++。
假设您的 proto 文件存放在 $SRC_DIR 下面,您也想把生成的文件放在同一个目录下,则可以使用如下命令:
编写 writer 和 Reader
如前所述,Writer 将把一个结构化数据写入磁盘,以便其他人来读取。假如我们不使用 Protobuf,其实也有许多的选择。一个可能的方法是将数据转换为字符串,然后将字符串写入磁盘。转换为字符串的方法可以使用 sprintf(),这非常简单。数字 123 可以变成字符串”123”。
这样做似乎没有什么不妥,但是仔细考虑一下就会发现,这样的做法对写 Reader 的那个人的要求比较高,Reader 的作者必须了 Writer 的细节。比如”123”可以是单个数字 123,但也可以是三个数字 1,2 和 3,等等。这么说来,我们还必须让 Writer 定义一种分隔符一样的字符,以便 Reader 可以正确读取。但分隔符也许还会引起其他的什么问题。最后我们发现一个简单的 Helloworld 也需要写许多处理消息格式的代码。
如果使用 Protobuf,那么这些细节就可以不需要应用程序来考虑了。
使用 Protobuf,Writer 的工作很简单,需要处理的结构化数据由 .proto 文件描述,经过上一节中的编译过程后,该数据化结构对应了一个 C++ 的类,并定义在 lm.helloworld.pb.h 中。对于本例,类名为 lm::helloworld。
Writer 需要 include 该头文件,然后便可以使用这个类了。
现在,在 Writer 代码中,将要存入磁盘的结构化数据由一个 lm::helloworld 类的对象表示,它提供了一系列的 get/set 函数用来修改和读取结构化数据中的数据成员,或者叫 field。
当我们需要将该结构化数据保存到磁盘上时,类 lm::helloworld 已经提供相应的方法来把一个复杂的数据变成一个字节序列,我们可以将这个字节序列写入磁盘。
对于想要读取这个数据的程序来说,也只需要使用类 lm::helloworld 的相应反序列化方法来将这个字节序列重新转换会结构化数据。这同我们开始时那个“123”的想法类似,不过 Protobuf 想的远远比我们那个粗糙的字符串转换要全面,因此,我们不如放心将这类事情交给 Protobuf 吧。
程序清单 2 演示了 Writer 的主要代码,您一定会觉得很简单吧?
清单 2. Writer 的主要代码Msg1 是一个 helloworld 类的对象,set_id() 用来设置 id 的值。SerializeToOstream 将对象序列化后写入一个 fstream 流。
代码清单 3 列出了 reader 的主要代码。
清单 3. Reader同样,Reader 声明类 helloworld 的对象 msg1,然后利用 ParseFromIstream 从一个 fstream 流中读取信息并反序列化。此后,ListMsg 中采用 get 方法读取消息的内部信息,并进行打印输出操作。
运行结果
运行 Writer 和 Reader 的结果如下:
回页首
和其他类似技术的比较
看完这个简单的例子之后,希望您已经能理解 Protobuf 能做什么了,那么您可能会说,世上还有很多其他的类似技术啊,比如 XML,JSON,Thrift 等等。和他们相比,Protobuf 有什么不同呢?
简单说来 Protobuf 的主要优点就是:简单,快。
这有测试为证,项目 thrift-protobuf-compare 比较了这些类似的技术,图 1 显示了该项目的一项测试结果,Total Time.
图 1. 性能测试结果
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 的例子动态编译
一般情况下,使用 Protobuf 的人们都会先写好 .proto 文件,再用 Protobuf 编译器生成目标语言所需要的源代码文件。将这些生成的代码和应用程序一起编译。
可是在某且情况下,人们无法预先知道 .proto 文件,他们需要动态处理一些未知的 .proto 文件。比如一个通用的消息转发中间件,它不可能预知需要处理怎样的消息。这需要动态编译 .proto 文件,并使用其中的 Message。
Protobuf 提供了
编写新的 proto 编译器
随 Google Protocol Buffer 源代码一起发布的编译器 protoc 支持 3 种编程语言:C++,java 和 Python。但使用 Google Protocol Buffer 的 Compiler 包,您可以开发出支持其他语言的新的编译器。
类 CommandLineInterface 封装了 protoc 编译器的前端,包括命令行参数的解析,proto 文件的编译等功能。您所需要做的是实现类 CodeGenerator 的派生类,实现诸如代码生成等后端工作:
程序的大体框架如图所示:
图 4. XML 编译器框图
回页首Protobuf 的更多细节
人们一直在强调,同 XML 相比, Protobuf 的主要优点在于性能高。它以高效的二进制方式存储,比 XML 小 3 到 10 倍,快 20 到 100 倍。
对于这些 “小 3 到 10 倍”,“快 20 到 100 倍”的说法,严肃的程序员需要一个解释。因此在本文的最后,让我们稍微深入 Protobuf 的内部实现吧。
有两项技术保证了采用 Protobuf 的程序能获得相对于 XML 极大的性能提高。
第一点,我们可以考察 Protobuf 序列化后的信息内容。您可以看到 Protocol Buffer 信息的表示非常紧凑,这意味着消息的体积减少,自然需要更少的资源。比如网络上传输的字节数更少,需要的 IO 更少等,从而提高性能。
第二点我们需要理解 Protobuf 封解包的大致过程,从而理解为什么会比 XML 快很多。
Google Protocol Buffer 的 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 表示。大于 128 的数字,比如 300,会用两个字节来表示:1010 1100 0000 0010
下图演示了 Google Protocol Buffer 如何解析两个 bytes。注意到最终计算前将两个 byte 的位置相互交换过一次,这是因为 Google Protocol Buffer 字节序采用 little-endian 的方式。
图 6. Varint 编码
TypeMeaningUsed For0Varintint32, int64, uint32, uint64, sint32, sint64, bool, enum164-bitfixed64, sfixed64, double2Length-delimistring, bytes, embedded messages, packed repeated fields3Start groupGroups (deprecated)4End groupGroups (deprecated)532-bitfixed32, sfixed32, float
在我们的例子当中,field id 所采用的数据类型为 int32,因此对应的 wire type 为 0。细心的读者或许会看到在 Type 0 所能表示的数据类型中有 int32 和 sint32 这两个非常类似的数据类型。Google Protocol Buffer 区别它们的主要意图也是为了减少 encoding 后的字节数。
在计算机内,一个负数一般会被表示为一个很大的整数,因为计算机定义负数的符号位为数字的最高位。如果采用 Varint 表示一个负数,那么一定需要 5 个 byte。为此 Google Protocol Buffer 定义了 sint32 这种类型,采用 zigzag 编码。
Zigzag 编码用无符号数来表示有符号数字,正数和负数交错,这就是 zigzag 这个词的含义了。
如图所示:
图 8. ZigZag 编码
封解包的速度
首先我们来了解一下 XML 的封解包过程。XML 需要从文件中读取出字符串,再转换为 XML 文档对象结构模型。之后,再从 XML 文档对象结构模型中读取指定节点的字符串,最后再将这个字符串转换成指定类型的变量。这个过程非常复杂,其中将 XML 文件转换为文档对象结构模型的过程通常需要完成词法文法分析等大量消耗 CPU 的复杂计算。
反观 Protobuf,它只需要简单地将一个二进制序列,按照指定的格式读取到 C++ 对应的结构类型中就可以了。从上一节的描述可以看到消息的 decoding 过程也可以通过几个位移操作组成的表达式计算即可完成。速度非常快。
为了说明这并不是我拍脑袋随意想出来的说法,下面让我们简单分析一下 Protobuf 解包的代码流程吧。
以代码清单 3 中的 Reader 为例,该程序首先调用 msg1 的 ParseFromIstream 方法,这个方法解析从文件读入的二进制数据流,并将解析出来的数据赋予 helloworld 类的相应数据成员。
该过程可以用下图表示:
图 9. 解包流程图
回页首结束语
往往了解越多,人们就会越觉得自己无知。我惶恐地发现自己竟然写了一篇关于序列化的文章,文中必然有许多想当然而自以为是的东西,还希望各位能够去伪存真,更希望真的高手能不吝赐教,给我来信。谢谢。
参考资料
学习
- Google Protocol Buffer 的在线帮助关于作者
从事软件开发工作 10 年以上,爱好开源软件,目前从事数据库和数据仓库的开发工作。
建议
|受益匪浅啊…
由
很好的快速入门资料
由
附件在哪里?
由
我近期也在关注Protobuf方面的资料。
非常感谢作者能分享这么好的文章。由回页首
内容

- 简介
- 一个简单的例子
- 和其他类似技术的比较
- 高级应用话题
- Protobuf 的更多细节
- 结束语
- 参考资料
- 关于作者
- 建议
标签
使用搜索文本框在 My developerWorks 中查找包含该标签的所有内容。热门标签热门标签
- 1 (3)?
- 3 (3)?
- autoconf (3)?
- awk (3)?
- bash (4)?
- c (24)?
- eclipse (19)?
- file_systems (12)?
- ftp (5)?
- hadoop (16)?
- java_技术 (4)?
- kernel (7)?
- kvm (4)?
- linux (176)?
- linux_kernel (5)?
- linux_on_powe... (13)?
- linux_virtual... (12)?
- linux_入门 (11)?
- linux环境进程间通信 (7)?
- lvm (5)?
- network (4)?
- on_demand_busi... (7)?
- perl (17)?
- php (5)?
- php_(hypertex... (10)?
- posix (4)?
- python (34)?
- resource_virt... (17)?
- shell (7)?
- shells (9)?
- socket (10)?
- system_p (7)?
- system_x (4)?
- tools (5)?
- vim (5)?
- virtualization (5)?
- web_服务 (4)?
- 安全 (26)?
- 安装 (14)?
- 编程 (6)?
- 编码 (10)?
- 部分 (4)?
- 存储 (14)?
- 代码库 (12)?
- 调试 (10)?
- 多线程 (6)?
- 发行版 (6)?
- 防火墙 (4)?
- 访问控制 (4)?
- 负载均衡 (4)?
- 管理 (70)?
- 集群 (26)?
- 脚本编程 (13)?
- 开发工具 (34)?
- 开放源码 (13)?
- 内核 (85)?
- 配置 (19)?
- 迁移 (10)?
- 驱动程序 (6)?
- 全球化 (5)?
- 认证 (7)?
- 使用 (4)?
- 数据库和数据管理 (14)?
- 通用编程 (9)?
- 图形 (12)?
- 网络 (16)?
- 文档和文本管理 (6)?
- 文件系统 (4)?
- 系统脚本 (10)?
- 下几个文件操作命令的代码实现 (4)?
- 线程 (5)?
- 性能 (30)?
- 验证 (5)?
- 移动和嵌入式系统 (21)?
- 应用开发 (17)?
- 应用移植 (4)?
- 硬件平台 (51)?
- 用 (5)?
- 云计算 (11)?
- 桌面环境 (22)?
热门标签结束
我的标签要访问我的标签,请登录
查看热门标签
我的标签结束
更多更少
- 1 (3)?3 (3)?autoconf (3)?awk (3)?bash (4)?c (24)?
- eclipse (19)?file_systems (12)?ftp (5)?
- hadoop (16)?java_技术 (4)?kernel (7)?kvm (4)?linux (176)?linux_kernel (5)?
- linux_on_powe... (13)?
- linux_virtual... (12)?linux_入门 (11)?
- linux环境进程间通信 (7)?lvm (5)?
- network (4)?on_demand_busi... (7)?perl (17)?php (5)?php_(hypertex... (10)?posix (4)?
- python (34)?resource_virt... (17)?shell (7)?
- shells (9)?socket (10)?system_p (7)?
- system_x (4)?tools (5)?vim (5)?
- virtualization (5)?web_服务 (4)?安全 (26)?
- 安装 (14)?编程 (6)?编码 (10)?部分 (4)?存储 (14)?
- 代码库 (12)?调试 (10)?多线程 (6)?发行版 (6)?
- 防火墙 (4)?访问控制 (4)?负载均衡 (4)?管理 (70)?集群 (26)?脚本编程 (13)?开发工具 (34)?开放源码 (13)?内核 (85)?配置 (19)?
- 迁移 (10)?驱动程序 (6)?全球化 (5)?认证 (7)?
- 使用 (4)?数据库和数据管理 (14)?通用编程 (9)?图形 (12)?网络 (16)?文档和文本管理 (6)?文件系统 (4)?系统脚本 (10)?下几个文件操作命令的代码实现 (4)?线程 (5)?
- 性能 (30)?验证 (5)?移动和嵌入式系统 (21)?应用开发 (17)?应用移植 (4)?硬件平台 (51)?用 (5)?云计算 (11)?桌面环境 (22)?
查看方式云?|分享此页面
- 关注 developerWorks
技术主题
- AIX and UNIX
- Information
Management - Lotus
- Rational
- WebSphere
- Cloud computing
- Java technology
- Linux
- Open source
- SOA and web services
- Web development
- XML
更多...
查找软件
- IBM 产品
- 评估方式(下载,在线试用,Beta 版,云)
- 行业
技术讲座
社区
- 群组
- 博客
- Wiki
- 文件
- 使用条款与条件
- 报告滥用
更多...
关于 developerWorks
- 反馈意见
- 在线投稿
- 投稿指南
- 网站导航
- 请求转载内容
相关资源
- ISV 资源 (英语)
- IBM 教育学院教育培养计划
IBM
- 解决方案
- 软件
- 支持门户
- 产品文档
- 红皮书 (英语)
- 隐私条约
- 浏览辅助
http://www.ibm.com/developerworks/cn/linux/l-cn-gpb/index.html?ca=drs-
- Cloud computing
- Google Protocol Buffer 的在线帮助关于作者
Protobuf 的优点
回页首

回页首