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大小。
例如,当需要传输一个带有name
和email
字段的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效率都要高效,有兴趣的朋友也可以深入了解一下。