第10章 Protocol Buffer

注:本教程使用 Proto3 版本

一、什么是Protobuf?

  全称Protocol buffers,是google的一种数据交换的格式,它独立于语言,独立于平台。

  作用:作为一种效率和兼容性都很优秀的二进制数据传输格式,可以用于诸如网络传输配置文件数据存储(序列化与反序列化)等诸多领域。

  优点:相比较XML和JSON格式,protobuf更小、更快、更便捷。你只需要将要被序列化的数据结构定义一次(译注:使用.proto文件定义),便可以使用特别生成的源代码(译注:使用protobuf提供的生成工具)轻松的使用不同的数据流完成对这些结构数据的读写操作,即使你使用不同的语言(译注:protobuf的跨语言支持特性)。你甚至可以更新你的数据结构的定义(译注:就是更新.proto文件内容)而不会破坏依赖“老”格式编译出来的程序。

  

二、ProtoBuf的版本

  PB具有三个版本:

  一、Google官方版本:    谷歌官方开发、比较晦涩,主库名字:Google.ProtoBuf.dll

  二、.Net社区版本:    .Net社区爱好者开发,写法上比较符合.net上的语法习惯,主库名字:protobuf-net.dll

  三、.Net社区版本(二):    据说是由谷歌的.net员工为.net开发,在官方没有出来csharp的时候开发,到发博文时还在维护,主库名字:Google.ProtocolBuffers.dll

  至于选用那个版本,跨平台的需求不大的话,可以用版本二、大的话可以选用一或者三。

  

三、protocol buffers的工作流程

首先,你需要通过在.proto文件中定义protocol buffermessage类型来指定你想要序列化的数据结构,每一个protocol buffer message是一个逻辑上的信息记录,它包含一系列的键值对。这里展示一个最基本的.ptoto文件的例子,它定义了一个包含Person信息的message

  1. syntax = "proto3";
  2. package shenjun;
  3. message Person
  4. {
  5. string name = 1;
  6. int32 id = 2;
  7. string email = 3;
  8. enum PhoneType
  9. {
  10. HOME = 0;
  11. MOBILE = 1;
  12. WORK = 2;
  13. }
  14. message PhoneNumber
  15. {
  16. string number = 1;
  17. PhoneType type = 2;
  18. }
  19. repeated PhoneNumber phone = 4;
  20. }

正如你所看见的那样,message的格式非常简单—每一个message类型都有一个或多个带有唯一编号的字段,每一个字段有一个字段名和一个字段类型,字段类型可以是数值类型(比如整形或浮点型)、booleans(布尔类型)、strings(字符串类型)、raw bytes、甚至(正如上面的例子)还可以是其他的protocol buffer message类型,这允许你可以分层次的组织你的数据结构。

运行编译器编译上述的例子将生成一个名为Person的类,在你的应用程序中你可以使用这个类来填充、序列化和反序列化Person protocol buffer messages。之后你可能会写下如下类似的代码(译注:序列化):

  1. Person p = new Person();
  2. p.Name = "shenjun";
  3. p.Id = 1;
  4. p.Email = "380921128@qq.com";
  5. p.Phone.Add(new Person.Types.PhoneNumber
  6. {
  7. Number = "12345678901",
  8. Type = Person.Types.PhoneType.Mobile
  9. });
  10. p.Phone.Add(new Person.Types.PhoneNumber
  11. {
  12. Number = "123456",
  13. Type = Person.Types.PhoneType.Home
  14. });
  15. byte[] buff = p.ToByteArray();

之后,你可以将你的message读回(译注:反序列化):

  1. IMessage IMperson = new Person();
  2. Person person = (Person)IMperson.Descriptor.Parser.ParseFrom(buff);

你可以向你的message中添加新的字段而不会破坏前向兼容性;在解析时旧的二进制文件会简单的忽略掉新字段,所以,如果你的通信协议中使用protocol buffers作为数据交换格式,那么你可以扩展你的协议而不用担心会打乱现有的代码。

四、为什么不使用XML?

相对于XML,protocol buffers在序列化结构数据时拥有许多先进的特性:

  1. 1、更简单
  2. 2、序列化后字节占用空间比XML3-10
  3. 3、序列化的时间效率比XML20-100
  4. 4、具有更少的歧义性
  5. 5、自动生成数据访问类方便应用程序的使用

举个例子,如果你想描述一个具有nameemailperson数据结构,在XML中,你需要这样做:

  1. <person>
  2. <name>John Doe</name>
  3. <email>jdoe@example.com</email>
  4. </person>

然而,在protocol buffersmessage中(protocol buffers的文本格式)你需要这样做:

  1. # Textual representation of a protocol buffer.
  2. # This is *not* the binary format used on the wire.
  3. person {
  4. name: "John Doe"
  5. email: "jdoe@example.com"
  6. }

当这个message被编码成protocol buffer的二进制格式(上述的文本格式只是为了方便阅读、调试和编辑),它将可能占用28个字节长度并且仅需要100-200纳秒的解析时间。相比,XML版本的则至少需要占用69字节的空间(这是在移除XML中的空格、换行之后),同时,将耗费大约5000-10000纳秒的解析时间。

  除此之外,手动操作protocol buffer更为方便,例如如下C++代码:

  1. cout << "Name: " << person.name() << endl;
  2. cout << "E-mail: " << person.email() << endl;

  然而如果你使用XML,那么你将需要这样做:

  1. cout << "Name: "<< person.getElementsByTagName("name")->item(0)->innerText()<< endl;
  2. cout << "E-mail: "<< person.getElementsByTagName("email")->item(0)->innerText()<< endl;

事物总有两面性,和XML相比protocol buffers并不总是更好的选择,例如,protocol buffers并不适合用来描述一个基于文本的标记型文档(比如HTML),因为你无法轻易的交错文本的结构。另外,XML具有很好的可读性和可编辑性;而protocol buffers,至少在它们的原生形式上并不具备这个特点。XML同时也是可扩展、自描述的。而一个protocol buffer只有在具有message 定义(在.proto文件中定义)时才会有意义。

五、如何开始使用protocol buffers?

首先,可以在这里下载安装包或者源码包

  https://developers.google.com/protocol-buffers/docs/downloads#release-packages

  这包含了针对JAVA、Python、C#和C++编译器的完整源码,同时包含了你所需要的I/O和测试类。为了完成编译和安装,请参照README文件。

  一旦你完成了编译和安装,那么就可以开始使用protocol buffers了。

六、proto3介绍

我们最新的版本version 3 alpha release引进了一个新的语言版本—Protocol Buffers version 3 (称之为proto3),它在我们现存的语言版本(proto2)上引进了一些新特性。proto3简化了protocol buffer language,这使其可以更便于使用和支持更多的编程语言:我们现在的alpha release版本可以让你能产生JAVAC++PythonJavaNanoRubyObjective-CC#版本的protocol buffer code,不过可能有时会有一些局限性。另外,你可以使用最新的Go protoc插件来产生Go语言版本的proto3 code,这可以从golang/protobuf Github repository获取。

我们现在只推荐你使用proto3:

  1、如果你想尝试在我们新支持的语言中使用protocol buffers

  2、如果你想尝试我们最新开源的RPC实现gRPC(目前仍处于alpha release版本),我们建议你为所有的gRPC 服务器和客户端都使用proto3以避免兼容性问题。

  注意两个版本的语言APIs并不是完全兼容的,为了避免给原来的用户造成不便,我们将会继续维护之前的那个版本(译注:proto2)。

七、最后说一点历史

Protocol buffers最初被Google开发用来作为处理索引服务器的request/response协议。在protocol buffers诞生之前,有一个需要手动编码/解码requestsresponses的协议,这个协议支持一个数字版本号,这导致了一个非常丑陋的代码,如下所示:

  1. if (version == 3) {
  2. ...
  3. } else if (version > 4) {
  4. if (version == 5) {
  5. ...
  6. }
  7. ...
  8. }

很显然的,格式化的协议也导致了复杂的新版本推出问题,因为开发人员必须确保所有服务器请求的发起者和实际的请求处理者之间都要理解新的协议。

Protocol buffers 就是用来解决这些问题的:

  1、可以很容易的插入新字段,中间的服务器可以简单的解析它而不需要了解所有字段。

  2、格式更具有自描述性,可以被不同的语言处理(比如JAVAC++Python等)。

  3、自动产生序列化和反序列化代码从而避免了手动解析。

  4、除了应用在具有短暂生命周期的RPC请求中,人们开始使用protocol buffers 作为一种便利的自描述格式来存储数据(比如在Bigtable中)。

  5、服务器的RPC接口开始被声明为协议文件的一部分,通过protocol 编译器产生stub类,该类可以被用户根据实际实现的服务器接口进行重写。

?