WHAT ?

以前偶尔在知乎上听过 Protobuf 的大名,但菜鸟如我也仅仅只是知道这玩意非常牛逼之类的,却压根不知道这是啥。

序列化与反序列化

要知道 Protobuf 是什么,就首先要知道什么是序列化和反序列化,以及为什么要有这个东西。宇宙最牛逼网站 StackOverflow 上也有一个帖子刚好就是说的这件事:https://stackoverflow.com/questions/633402/what-is-serialization 。最高赞用了简短的两句话就阐明了序列化的 What 和 Why,“序列化指的是将内存中的一个对象转化成字节流的这个过程,这样做的好处是可以让你方便的将这个字节流存储到硬盘里或者是通过网络传输给其他用户。而反序列化则正好相反,即将一串字节流恢复成内存中的一个对象。”

工作之前我只写过那种功能简陋的 HTTP 服务器,恰好这个 HTTP 服务器要发送的内容本身就是一个字符串,所以也就不存在什么要把发送的内容先转化为字节流的过程,直接暴力调 send() 就完了。可事实是现实中的服务器程序往往要发送的内容本身并不是一个字符串,更常见的应该是发送一些结构化的数据,例如一个用户的各种信息,这就要想办法将结构体转化成可以直接 send() 的字节流,此时序列化和反序列化就派上了用场。

常见的序列化方法有 XML 和 JSON。可以想到,将一个结构化的数据序列化成 XML 或者 JSON 就是将结构化数据中的每一个字段转换成 XML 或者 JSON 格式中的一个字段,这样一个结构化的数据就变成了一个字符串,之后再进行存盘或者通过网络发送也就十分简单了。

Protobuf

Protobuf 的全称是 Protocol Buffers ,这是谷歌开发的一款用来进行序列化和反序列的工具,更多信息可以在官方网站上找到。那么使用 Protobuf 而不是其他方式来进行序列化有什么好处呢?官方网站上的一段话也说得十分清楚:

Why Use Protocol Buffers?

The example we're going to use is a very simple "address book" application that can read and write people's contact details to and from a file. Each person in the address book has a name, an ID, an email address, and a contact phone number.

How do you serialize and retrieve structured data like this? There are a few ways to solve this problem:

  • The raw in-memory data structures can be sent/saved in binary form. Over time, this is a fragile approach, as the receiving/reading code must be compiled with exactly the same memory layout, endianness, etc. Also, as files accumulate data in the raw format and copies of software that are wired for that format are spread around, it's very hard to extend the format.
  • You can invent an ad-hoc way to encode the data items into a single string – such as encoding 4 ints as "12:3:-23:67". This is a simple and flexible approach, although it does require writing one-off encoding and parsing code, and the parsing imposes a small run-time cost. This works best for encoding very simple data.
  • Serialize the data to XML. This approach can be very attractive since XML is (sort of) human readable and there are binding libraries for lots of languages. This can be a good choice if you want to share data with other applications/projects. However, XML is notoriously space intensive, and encoding/decoding it can impose a huge performance penalty on applications. Also, navigating an XML DOM tree is considerably more complicated than navigating simple fields in a class normally would be.

Protocol buffers are the flexible, efficient, automated solution to solve exactly this problem. With protocol buffers, you write a .proto description of the data structure you wish to store. From that, the protocol buffer compiler creates a class that implements automatic encoding and parsing of the protocol buffer data with an efficient binary format. The generated class provides getters and setters for the fields that make up a protocol buffer and takes care of the details of reading and writing the protocol buffer as a unit. Importantly, the protocol buffer format supports the idea of extending the format over time in such a way that the code can still read data encoded with the old format.

安装 Protobuf

下载 Release

https://github.com/protocolbuffers/protobuf/releases

安装

https://github.com/protocolbuffers/protobuf/blob/master/src/README.md

./configure --prefix=XXX
make
make check
sudo make install
sudo ldconfig # refresh shared library cache.

安装好 Protobuf 后可以在 bin/ 目录下找到一个可执行文件 protoc ,这个程序的作用下面会介绍。除此之外安装目录下还会有 include/lib/ 两个目录,顾名思义,分别存放了 C++ 程序会用到的头文件和库文件。

当编译一个使用了 Protobuf 的程序时,需要添加一些编译选项,目的自然是引入上面说的头文件和库文件。具体需要的编译选项可以通过 pkg-config --cflags --libs protobuf 指令得到,编译时加上即可。

Protobuf 基本使用

https://developers.google.com/protocol-buffers/docs/cpptutorial

在这篇博客我不想大段大段的介绍 Protobuf 的原理,而只是简单的介绍它是如何使用的,更具体的就是如何使用 Protobuf 在 C++ 程序中序列化/反序列化一个结构化的数据。所以这甚至连 Tutorial 都算不上,英语好的也可以去上面的链接中看官方文档提供的 Tutorial 。

定义一个结构

前面说过,Protobuf 被用来序列化一个结构化的数据,所以在使用 Protobuf 时就要先定义这个结构化数据。具体来说就需要写一个 .proto 文件,下面是一个示例文件 user.proto

syntax = "proto2";

message user {
    required string name = 1;
    optional int32 age = 2;
}

第一行指定当前的 .proto 文件使用 proto2 语法。后面的部分定义了一个希望被序列化的结构 user ,可以看到定义格式类似于结构体的定义。其中指定了这个结构中有两个字段 nameage ,每个字段都有一些修饰的关键字例如 requiredoptional ,以及类型 stringint32

这样一个 Protobuf 的结构或者专业点叫 message 就已经定义好了,但这个 message 如何使用在 C++ 程序中呢。这时候我们之前安装 Protobuf 的时候生成的可执行文件 protoc 就派上用场了。

编译 .proto 文件

protoc -I=$SRC_DIR --cpp_out=$DST_DIR user.proto

-I 后面是项目源文件的目录,如果不指定则默认当前目录。--cpp_out 是生成出来的文件要保存到的目录。最后是 proto 文件的路径。

使用这条命令就可以把 .proto 文件编译成一个 .cc 文件和一个 .h 文件。这两个文件才是最终要用在 C++ 程序中的文件。

在 C++ 中使用

在生成的 user.pb.h 中可以看到 Protobuf 生成了一个 class user ,其中也有成员 name_age_ ,并且这个类中还封装了很多设置这两个值的方法:

所以如果想要在我们自己的 C++ 程序中使用这个 Protobuf 生成的结构,只需要引入这个头文件,然后就可以定义这个 user 类对象,定义好后就可以通过上面的诸如 set_name()set_age() 等接口来设置对象的值了。

设置好值之后,就需要序列化这个对象了,Protobuf 提供了两个非常方便的成员函数来进行序列化和反序列化的工作:

  • bool SerializeToString(string* output) const;
  • bool ParseFromString(const string& data);

顾名思义,SerializeToString 将一个 Protobuf 结构序列化成一个 std::string,参数 output 就是用来接受序列化后的数据的。注意,序列化后的数据其实是二进制数据而不是文本数据,所以这里只是将 string 作为一个方便的容器来用的。将这个结构序列化成二进制后就可以方便的将这串数据存盘或者通过网络传输了。类似的,ParseFromString 将一个 string 中的二进制数据反序列化成一个 Protobuf 结构。

下面用这些简单的接口来测试一下:

#include <iostream>
#include <string>
#include "user.pb.h"

using namespace std;

int main(){
    user u;
    u.set_name(string("Jack"));
    u.set_age(21);

    string binary_data;
    u.SerializeToString(&binary_data);
    cout << "Binary data size: " << binary_data.size() << endl;

    u.clear_name();                     
    u.clear_age();
    u.ParseFromString(binary_data);
    cout << u.name() << " " << u.age() << endl;

    return 0;
}

可以看到,Protobuf 生成的二进制数据对空间的利用率非常高,本来四字节的字符串和一个整型,序列化成二进制后居然只占八个字节!这更能说明 Protobuf 相对于 JSON 或者 XML 的好处,毕竟随便加一个大括号或者尖括号都不止八字节了 xd

Last modification:September 24th, 2020 at 06:06 pm