这篇概论主要是介绍Protocol Buffers,并且告诉你,如果你要开始使用Protocol Buffers,需要做哪些事情。
1. 什么是Protocol Buffers
Protocol Buffers是一种易扩展、高效的、自动化的序列化结构化的数据的机制,比起XML来,它更小、更快、更简单。一旦你明确了你想要你的数据用什么结构, 你就可以很容易用某种生成的源代码来读写你的结构化数据,可以是各种各样的数据流,也可以用在各种各样的语言中。你甚至可以更新你的数据结构,而不用重新 部署你用“老”的数据格式编译的程序。
2. Protocol Buffers是怎样工作的?
你可以通过在.proto文件里面用Protocol Buffers消息类型进行定义,来让你准备序列化的信息如何结构化。每一个Protocol Buffers消息是信息的一条小的逻辑记录,里面包含一系列名称-值对。下面我们来看一个非常简单的.proto文件,这个文件定义了一个人的信息。
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 Buffers的消息类型(如上面的例子),这样你可以用分层的方式定义你的数据结构。并且你可以指定可选的域、必选的域和重复的域。
一旦你已经定义了你的Protocol Buffers消息,你可以对你的.proto文件运行你应用程序所使用语言的Protocol Buffers编译器来生成数据访问类。这个类会为每个域提供简单的访问方法(类似query()和set_query()),也会提供方法把整个结构序 列化成连续字节或者从连续字节解析成一个整个结构。举个例子,如果你选择了C++语言,在上面给出的例子上运行编译器,将会生成一个C++的Person 类,然后你可以在你的应用程序里面使用这个类,来填充、序列化和检索这个Person的Protocol Buffers的消息。你可以写出一些类似这样的代码:
C++代码

- 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);
然后,晚点,你可以读回你的消息:
C++代码

- 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作为你数据格式的通信协议时,你可以扩展你的协议,而不用担心破坏现有的代码。
3. 为什么不使用XML?
在序列化结构化数据方面,Protocol Buffers比起XML来,有很多优点:
- Protocol Buffers比XML更简单。
- Protocol Buffers比XML小3~10倍。
- Protocol Buffers比XML快20~100倍。
- Protocol Buffers比XML更明确。
- 在生成数据访问类方面,Protocol Buffers比XML更容易编程实现。
我们以构建一个person模型为例子,person里面包含一个name和一个email。如果采用XML方式,我们需要如下处理:
Xml代码

- <person>
- <name>John Doe</name>
- <email>jdoe@example.com</email>
- </person>
而相应的Protocol Buffers消息(文本格式)如下所示:
Proto代码

- # Textual representation of a protocol buffer.
- # This is *not* the binary format used on the wire.
- person {
- name: "John Doe"
- email: "jdoe@example.com"
- }
当这个消息被编码成Protocol Buffers二进制格式(上面的Protocol Buffers的文本格式只是为了方便调试或编辑的时候阅读)的时候,可能只有28个字节,解析只需要大概100~200纳秒。而XML版本的至少需要 69个字节(除去空格等),解析需要大概5~10微秒的时间。
同样,操作一个Protocol Buffers也更容易:
C++代码

- cout << "Name: " << person.name() << endl;
- cout << "E-mail: " << person.email() << endl;
反而使用XML你需要如下处理:
C++代码

- cout << "Name: "
- << person.getElementsByTagName("name")->item(0)->innerText()
- << endl;
- cout << "E-mail: "
- << person.getElementsByTagName("email")->item(0)->innerText()
- << endl;
然而,Protocol Buffers并不总是一个比XML更好的解决方案。比如,使用一个基于标记的文本文档进行建模,使用Protocol Buffers并不是一个很好的方案,因为在文本里面你不能很容易的进行隔行扫描结构。另外,XML的可读性和可编辑性要比Protocol Buffers的本地格式好很多。XML还有一些扩展——自我描述。Protocol Buffers只有在你已经对消息进行了定义(.proto文件)的前提下,才有意义。
4. Protocol Buffers简史
Protocol Buffers最初是由Google开发,用来处理索引服务器的请求/响应协议。在Protocol Buffers之前,采用了一种通过手工编组的格式来处理请求和响应,并且这种协议还支持多版本的,这样就导致了会出现一些丑陋的代码,如下所示:
C++代码

- if (version == 3) {
- ...
- } else if (version > 4) {
- if (version == 5) {
- ...
- }
- ...
- }
很明显,格式化的协议会让部署新版本协议的时候更加复杂,因为开发人员必须在按下一个开关开始使用新的协议之前,确认所有服务器(包括请求发起服务器和真正处理请求的服务器)能够理解新的协议。
Protocol Buffers是被设计用来解决以下两个方面的许多问题:
- 数据格式里面能够很容易引入一个新的域,中间服务器不需要观察数据格式就能够很简单的去解析它,并且不需要知道所有域就能够让数据通过。
- 数据格式里面有更多的自我描述,能够被各种各样的语言(C++、Java等)处理。
然而,用户仍旧需要亲自编写他们自己的解析代码。
随着Protocol Buffers的发展,它又有了一些其他的特征和用法:
- 自动生成序列化和反序列化代码,从而避免手工编写解析代码。
- 另外因为能够被用来进行短暂的远程过程调用(Remote Procedure Call,RPC)式的请求,所以人们开始使用Protocol Buffers作为一个便利的自我描述的数据格式,用来进行数据的持久化存储(例如,在Bigtable里面)。
- 服务器的RPC接口可以开始作为协议文件的一部分来进行声明了,协议编译器可以自动生成stub类,用户可以在服务器端用真实的接口实现来进行重载。
Protocol Buffers现在是Google的数据沟通语言,在写这篇文章的时候,在Google的代码里面大概用12183个.proto文件定义了大概 48162种不同的消息类型。他们被用于RPC系统和在各种各样的存储系统里面持久化存储数据这两个方面。