6

以前玩 C,Json、XML 什么的看多了,现在开始玩 C++,才发现我了解的世界太小了——原来 C++ 届还有 Google Protocol Buffers 这么好的东西。果然在 PC 上做开发真是好,不用考虑可执行程序的 size,可以放心放肆地用 C++。

Reference

Protocol Buffer Basics: C++
Google Protocol Buffers
Google Protocol Buffer 的使用和原理 - IBM
这是一份很有诚意的 Protocol Buffer 语法详解

简介

Protocol Buffers 又简称为 Protobuf、PB。是 Google 推出的一种数据交换格式。注意,这还是二进制的交换数据。

Protobuf 有自己的编译器,在 Linux 中叫做 protoc ,可以解释 .proto 文件并且声称对应语言的源文件。目前 Google 提供了三种语言:Java, C++, Python。后面我们就以 C++ 来说明,其他语言类似。

总结一下:我们所说的 Protobuf,其实可以说是包含以下几部分:

  • 一种数据交换格式,可以将 C++ 中定义的存储类的内容 与 二进制序列串 相互转换,主要用于数据传输或保存
  • 定义了一种源文件,扩展名为 .proto,使用这种源文件,可以定义存储类的内容
  • Google 提供了一个编译器 protoc,可以编译 .proto 编译成 .cc 文件,使之成为一个可以在 C++ 工程中直接使用的类。类的功能非常完善,后文辉具体说明。

.proto 的简单语法

我们来定义一个可以覆盖大多数使用情况的例子,定义一个高中的班级和班上学生的信息

// --------------------------------------
// File: School.HighSchool.proto
//
package School.HighSchool;

message Person
{
    optional int32   id = 1;
    optional int32   age = 2;
    optional string  first_name = 3;
    optional string  last_name = 4;
    optional bool    is_female = 5;
};

message Class
{
    optional int32   grade_num = 1;
    optional int32   class_num = 2;
    optional Person  head_teacher = 3;
    repeated Person  students = 4;
};

文件的建议命名为 “包名.消息名.proto”。对于 C++ 而言,就是 “命名空间.数据类.proto”。

上面这段的语义,我直接用 C++ 的概念来说明吧:

  1. 定义包的分类(命名空间)是 School 类别下的 HighSchool 子类别。
  2. 定义一个 个人 数据类,包含学生的 ID、姓、名、性别等等信息。
  3. 定义一个 班级 数据类,包含年级号和班级号、班主任信息、所有学生的信息。

这里有必要说明 optionalrepeated。前者表示这个数据类型是可选的,也就是说有可能不存在这样的一个数据信息。后者表示这个数据类型是多个的,可以理解为一个,或者说一个 set、一个集合,总之就是多个同类数据,类似于 C++ 中的 vector。对应于 JSON 中的 array。Repeated 类型的数据有可能是空的(成员为 0)。

optional 相对应的是 required 类型,表示这个数据类型是必须的。但是,大部分资料都建议不要用这种类型。

生成 C++ 类

上面的源文件,可以使用以下命令进行编译:

protoc -I=$SRC_DIR --cpp_out=$DST_DIR School.HighSchool.proto

编译完成后,生成两个文件:School.HighSchool.pb.ccSchool.HighSchool.pb.h

在类里面,大体结构是这样的:

namespace School {
namespace HighSchool {

class Person : public ::google::protobuf::Message {
    ...
}    // end of class Student

class Class : public ::google::protobuf::Message {
    ...
}    // end of class public

}}    // end of namespaces

对象函数方法

对象方法

一般而言生成的类,都有对应整个类的操作方法,常用的几个方法如下:

CopyFrom (...)    // operator= (...)  的具体实现
MergeFrom (...)
ByteSize () const
Swap (...)

而最重要的两个方法则是:

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

作用分别是序列化和反序列化,也就是

  1. 将 PB 的内容序列化(二进制化)到指定的 string 对象中。
  2. string 类型中解析出 PB 对象

get / set 方法

对于具体的数据成员,则给出具体的 get / set 方法。比如 Person 类的 id 成员,C++ 类会提供以下方法:

inline bool has_id() const;
inline void clear_id();
static const int kInt32IdNumber = 0;

inline ::google::protobuf::int32 id() const;
inline void set_id(::google::protobuf::int32 value);

所有的方法都按照字面意思就可以读懂,非常好理解。

而对于 repeated 属性的成员,比如 students,则比较复杂,使用了 STL:

inline int students_size() const;
inline void clear_students_size();

inline const ::School::HighSchool::Person &students(int index) const;
inline ::School::HighSchool::Person *mutable_students(int index);
inline ::School::HighSchool::Person *add_students();
inline const ::google::protobuf::RepeatedPtrField <::School::HighSchool::Person> &students() const;
inline ::google::protobuf::RepeatedPtrField <::School::HighSchool::Person> *mutable_students();

看起来比较复杂,其实还是很好理解的。请读者结合 C++ 的迭代器理解就好了。

支持的基本数据类型

Protobuf 中常用的基本数据类型及必要的说明如下:

  • double
  • float
  • int32, int64:负数的编码效率低于 sintXX 系列。当有负数,但是出现频率不高时使用
  • uint32, uint64
  • sint32, sint64:当负数出现的频率比较高时,比 int32 的效率高
  • fixed32, fixed64:注意,这不是 “定点数” 的意思,而是表示定长 4 字节的整形数据。如果数字长期大于 228 时,比 int32 效率更高
  • sfixed32, sfixed64
  • bool
  • string:ASCII / UTF-8 字符串
  • bytes:二进制序列

一些容易忽略的特性(坑)

笔者在使用 pb 的过程中遇到了一些坑(其实只是特性),这里列出来,读者在实际使用中应该留意一下:

强制小写

PB 的每个成员,不论你在 proto 文件中是怎么写的,最终都会给你转换成小写。比如定义了一个成员 optional bytes bytes_article_URL,最终生成的 get / set 方法是 bytes_article_url()set_bytes_article_url() 。但是呢,在 enum 类型里面的定义就不在此限。程序员要留意这个问题。

enum 值的定义

枚举值在 pb 中,使用非常方便。可是我们需要注意一种情况,就是如果 pb 遇到了一个它不认识的(未定义)的枚举值,它的解决方法是直接抛出 exception。对于 C++ 来说,未处理的 exception 就意味着程序退出,而且不是 core,而是类似于调用 exit() 的正常退出。对于一些基于 core 的监控机制可能无效。

为了解决这个问题,有两种方法:

  1. 每次针对 enum 类型进行 catch
  2. 不要使用 enum 类型,而是改为 uint 之类的。enum 的值可以转换成为普通的 uint / int 系列类型,既有了 enum 的明确语义,对于不方便做 catch 的程序而言,而更加方便

amc
924 声望223 粉丝

电子和互联网深耕多年,拥有丰富的嵌入式和服务器开发经验。现负责腾讯心悦俱乐部后台开发