Protobuf初探

文 · Mark

对于软件开发者,尤其是移动端(Android & iOS)开发者来说,XML和JSON两种文件传输格式并不陌生,尤其是JSON,仅在上述两种开发领域中就广泛应用。随之而来的,是各种JSON的解析库,此文中,笔者不想介绍各个平台丰富多彩的JSON解析库,而是想和大家分享发现的一种新的文件传输格式,Protobuf。

Protobuf是什么鬼

初次相遇Protobuf,这是我的第一反应。那么Protobuf到底是什么鬼?

Protobuf是一种灵活高效的、用于跨平台数据通信的数据传输格式,全称Protocol Buffers,类似XML和JSON。下面,我们先看一个protobuf的简单例子,如下:

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;
}

其实,Protobuf出身名门Google,采用C++实现,在Google内部已应用多年,并有与平台无关,与语言无关,可高效序列化传输等特点,当前最新的Protobuf版本为3.0。

发展现状

Google最初为了解决统一内部传输问题,自己制定了一套高效的数据传输格式定义协议,即Protobuf,并逐步在Google内部项目中使用。随着Protobuf在Google内部被越来越多的项目所采用,本身性能也在逐步被改良提高。
在2008年7月7日,将Protobuf贡献给开源社区进行了开源,目前,Github上可以找到相应源码。
目前Protobuf不仅在Google内部广泛使用,在RPC通信中广泛使用,在国外,Facebook在部分项目中采用了Protobuf作为通信的编解码工具。在国内,Protobuf在百度,腾讯TDW平台,阿里巴巴部分项目中,也被作为基础库充分利用。除此之外,还有一些其他公司也在使用Protobuf左右数据通信的基础类库。
但是,在移动平台开发中,应用还不是很广泛。

支持的语言

Google官方对Protobuf提供C++、Java、Python三种语言提供官方支持。对于proto3,还支持Go、JavaNano、Ruby和C#。当然,几乎所有当前的编程语言,都有相应的Protobuf支持库,所以,对于Protobuf的支持的语言还是很全面的。

工作原理

使用Protobuf,只需要根据Protobuf的语法规范,定义需要传输的数据内容格式,定义Protobuf的文件为.proto文件,例如,定义一个具有name和email字段的Person信息对象时,.proto文件内容如下:

message Person {
    required string name = 1;
    optional string email = 2;
}

定义好数据格式之后,我们只需要使用我们需要的语言的Protobuf编译器对.proto文件进行编译即可,输出我们需要的对应语言的Model定义类源码文件,在该文件中会自动生成序列化方法,getter和setter方法等,例如对于C++语言,上述.proto文件编译命令为:

protoc --cpp_out=cpp_dir person.proto

protobuf编译器根据.proto文件对Message Person定义,编译生成对应的源码文件,包含.h、.cpp文件,其中.h文件如下,下面我们就看一下具体生成了哪些内容。

基本Person类框架,包括Person对象的构建函数,析构函数,等于运算符重载,Descriptor函数等;

  class Person;
...
  class Person : public ::google::protobuf::Message {
  public:
    Person();
    virtual ~Person();

    Person(const Person& from);

    inline Person& operator=(const Person& from) {
      CopyFrom(from);
      return *this;
    }

    inline const ::google::protobuf::UnknownFieldSet& unknown_fields() const {
      return _unknown_fields_;
    }

    inline ::google::protobuf::UnknownFieldSet* mutable_unknown_fields() {
      return &_unknown_fields_;
    }

    static const ::google::protobuf::Descriptor* descriptor();
    static const Person& default_instance();

由剩余部分代码可以清晰的看出,Protobuf编译器已经帮我们生成好了Model构建的方法,各属性的getter、setter方法、以及序列化方法,在后续过程中,我们只需要调用相关方法即可;

    // required string name = 1;
    inline bool has_name() const;
    inline void clear_name();
    static const int kNameFieldNumber = 1;
    inline const ::std::string& name() const;
    inline void set_name(const ::std::string& value);
    inline void set_name(const char* value);
    inline void set_name(const char* value, size_t size);
    inline ::std::string* mutable_name();
    inline ::std::string* release_name();
    inline void set_allocated_name(::std::string* name);

    // optional string email = 2;
    inline bool has_email() const;
    inline void clear_email();
    static const int kEmailFieldNumber = 2;
    inline const ::std::string& email() const;
    inline void set_email(const ::std::string& value);
    inline void set_email(const char* value);
    inline void set_email(const char* value, size_t size);
    inline ::std::string* mutable_email();
    inline ::std::string* release_email();
    inline void set_allocated_email(::std::string* email);

    // @@protoc_insertion_point(class_scope:Person)
  private:
    inline void set_has_name();
    inline void clear_has_name();
    inline void set_has_email();
    inline void clear_has_email();

那么,我们通过protobuf编译器protoc编译出了我们需要的Model定义类Person.h和Person.cpp,剩下的工作只需要调用进行使用了。下面我们简单调用一下我们生成的相应Person Model代码。
调用代码如下:

#include <iostream>
#include <fstream>
#include <string>

#include "person.pb.h"

using namespace std;


// Main function:  Reads the entire person info. from a file,
//   adds one person based on user input, then writes it back out to the same
//   file.
int main(int argc, char* argv[]) {
  // Verify that the version of the library that we linked against is
  // compatible with the version of the headers we compiled against.
  GOOGLE_PROTOBUF_VERIFY_VERSION;

  ...

  Person *person = new Person();
  person->set_name("Mark CJ");
  person->set_email("markcjemail@google.com");

  cout << "Person Info : name " << person.name() << ", email : " << person.email() << endl;

  {
    // Read the existing person object.
    fstream input(argv[1], ios::in | ios::binary);
    if (!input) {
      cout << argv[1] << ": File not found.  Creating a new file." << endl;
    } else if (!person.ParseFromIstream(&input)) {
      cerr << "Failed to parse person object." << endl;
      return -1;
    }
  }

 ...

  {
    // Write the new person object back to disk.
    fstream output(argv[1], ios::out | ios::trunc | ios::binary);
    if (!person.SerializeToOstream(&output)) {
      cerr << "Failed to write person object." << endl;
      return -1;
    }
  }

  // Optional:  Delete all global objects allocated by libprotobuf.
  google::protobuf::ShutdownProtobufLibrary();

  return 0;
}

Protobuf优势

Protobuf是用于结构化数据串行化的灵活、高效、自动的方法,类似XML,不过它比XML更小、更快、也更简单。你可以定义自己的数据结构,然后使用代码生成器生成的代码来读写这个数据结构。你甚至可以在无需重新部署程序的情况下更新数据结构。

体积更小、速度更快

相对于XML,Protobuf只有XML文件的1/10到1/3大小。
例如,当需要传输一个带有nameemail字段的Person对象信息时,使用XML格式如下

<person>
    <name>John Doe</name>
    <email>jdoe@example.com</email>
</person>

如果剔除空格,该XML体积为69字节,会大概话费5000-10000 nm解析上述数据。
而使用Protobuf的message形式,转为人类可读内容如下:

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

但当上述Protobuf的Person信息转为二进制形式(Protobuf传输时使用二进制)时,约28字节,大概话费100-200nm解析使用。

以下是针对于Protobuf及相关竞品做的一份性能测试

浅蓝色为序列化时间,深蓝色为反序列化时间

内存占用情况对比

书写简单、更少歧义

当编程输出Model内容时,采用C++,代码如下:

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

但当采用XML,对获取到的Person信息进行解析时,代码如下:

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

相比于JSON,除了语义更简单之外,Protobuf中被编号的字段可以排除所谓的版本检查,保证无偿的向后兼容。
由上面所述可见,我们也可以使用Protobuf中的@required@optional@repeated等属性,将一些常识性的调试操作,转换为正式的拓展,比如将原来@optional的字段内容转换为
@required的字段。

Protobuf劣势

虽然Protobuf的效率以及体积控制很出色,但是万物都有优点,也有缺点,当然Protobuf也不例外。

相对于XML, Protobuf的功能略显简单,无法表达较为复杂的概念定义,所以,对于复杂的定义需求,无法有效的实现。

由于XML在多行业中被广泛、长期的使用,所以,使用XML已经成为了部分行业的标准工具,而Protobuf只在Google内部使用较多,所以对于被更广泛的其他行业所使用,还有很长的路要走。
为了缩减Protobuf的传输数据文件大小,也为了加快解析速度,Protobuf采用二进制格式进行存储,所以导致存储后或传输过程中的数据,对人类可读性差,不利于中间代码数据调试。

和XML相比,Protobuf也适用描述标记语言的传输,比较适用于描述数据结构,而XML在这两方面,均可适用。

而相对于JSON,Protobuf在序列化速度和反序列化速度方面还略有差距,这一点也是Protobuf需要补强的一部分。同时,在服务端和Web端数据通信中,JSON的使用广泛性还是要高于Protobuf,这也源于前端原生库及第三方库对JSON的有效支持,而Protobuf在Web端,还没有如此广泛的支持。

小结

以上只是对Protobuf的初探内容,本想找到一种可以替代JSON的解决方案,但是并不是完全没有收获,虽然现在Protobuf没有在移动端广泛应用,因为当前JSON的各项性能均与其类似,但是Protobuf有些设计思想还是可以供大家借鉴的。如果有朋友想深入了解,可以访问Google官网及查看相关源码,笔者文笔较差,欢迎批评指正,多多交流。

在学习Protobuf的过程中,发现了一个名叫Protostuff的好东西,protostuff针对protobuf进行了部分优化,包括可选免去预编译等操作,初探比XML及JSON效率都要高效,有兴趣的朋友也可以深入了解一下。