<audio id="audio" title="15 | 序列化：简单通用的数据交换格式有哪些？" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/32/8d/32f57c4e71ed343adef8d531f031f28d.mp3"></audio>

你好，我是Chrono。

在前面的三个单元里，我们学习了C++的语言特性和标准库，算是把C++的编程范式、生命周期、核心特性、标准库的内容整体过了一遍。从今天起，我们的学习之旅又将开启一个新的篇章。

C++语言和标准库很强大，功能灵活，组件繁多，但也只能说是构建软件这座大厦的基石。想要仅凭它们去“包打天下”，不能说是绝对不可行，但至少是“吃力难讨好”。

还是那句老话：“不要重复发明轮子。”（Reinventing the wheel）虽然很多C++程序员都热衷于此，但我觉得对于你我这样的“凡人”，还是要珍惜自己的时间和精力，把有限的资源投入到能有更多产出的事情上。

所以，接下来的这几节课，我会介绍一些第三方工具，精选出序列化/反序列化、网络通信、脚本语言混合编程和性能分析这四类工具，弥补标准库的不足，节约你的开发成本，让你的工作更有效率。

今天，我先来说一下序列化和反序列化。这两个功能在软件开发中经常遇到，你可能很熟悉了，所以我只简单解释一下。

序列化，就是把内存里“活的对象”转换成静止的字节序列，便于存储和网络传输；而反序列化则是反向操作，从静止的字节序列重新构建出内存里可用的对象。

我借用《三体》里的内容，打一个形象的比喻：序列化就是“三体人”的脱水，变成干纤维，在乱纪元方便存储运输；反序列化就是“三体人”的浸泡，在恒纪元由干纤维再恢复成活生生的人。（即使没读过《三体》，也是很好理解的吧？）

接下来，我就和你介绍三种既简单又高效的数据交换格式：JSON、MessagePack和ProtoBuffer，看看在C++里怎么对数据做序列化和反序列化。

## JSON

JSON是一种轻量级的数据交换格式，采用纯文本表示，所以是“human readable”，阅读和修改都很方便。

由于JSON起源于“最流行的脚本语言”JavaScript，所以它也随之得到了广泛的应用，在Web开发领域几乎已经成为了事实上的标准，而且还渗透到了其他的领域。比如很多数据库就支持直接存储JSON数据，还有很多应用服务使用JSON作为配置接口。

在[JSON的官方网站](https://www.json.org/json-zh.html)上，你可以找到大量的C++实现，不过用起来都差不多。因为JSON本身就是个KV结构，很容易映射到类似map的关联数组操作方式。

如果不是特别在意性能的话，选个你自己喜欢的就好。否则，你就要做一下测试，看哪一个更适合你的应用场景。

不过我觉得，JSON格式注重的是方便易用，在性能上没有太大的优势，所以**一般选择JSON来交换数据，通常都不会太在意性能（不然肯定会改换其他格式了），还是自己用着顺手最重要**。

下面就来说说我的个人推荐：“[JSON for Modern C++](https://github.com/nlohmann/json)”这个库。

JSON for Modern C++可能不是最小最快的JSON解析工具，但功能足够完善，而且使用方便，仅需要包含一个头文件“json.hpp”，没有外部依赖，也不需要额外的安装、编译、链接工作，适合快速上手开发。

JSON for Modern C++可以用“git clone”下载源码，或者更简单一点，直接用wget获取头文件就行：

```
git clone git@github.com:nlohmann/json.git    # git clone
wget https://github.com/nlohmann/json/releases/download/v3.7.3/json.hpp  # wget 

```

JSON for Modern C++使用一个json类来表示JSON数据，为了避免说的时候弄混，我给这个类起了个别名json_t：

```
using json_t = nlohmann::json;

```

json_t的序列化功能很简单，和标准容器map一样，用关联数组的“[]”来添加任意数据。

你不需要特别指定数据的类型，它会自动推导出恰当的类型。比如，连续多个“[]”就是嵌套对象，array、vector或者花括号形式的初始化列表就是JSON数组，map或者是花括号形式的pair就是JSON对象，非常自然：

```
json_t j;                                   // JSON对象

j[&quot;age&quot;] = 23;                              // &quot;age&quot;:23
j[&quot;name&quot;] = &quot;spiderman&quot;;                    // &quot;name&quot;:&quot;spiderman&quot;
j[&quot;gear&quot;][&quot;suits&quot;] = &quot;2099&quot;;                // &quot;gear&quot;:{&quot;suits&quot;:&quot;2099&quot;}
j[&quot;jobs&quot;] = {&quot;superhero&quot;};                  // &quot;jobs&quot;:[&quot;superhero&quot;]  

vector&lt;int&gt; v = {1,2,3};                   // vector容器
j[&quot;numbers&quot;] = v;                          // &quot;numbers&quot;:[1,2,3]

map&lt;string, int&gt; m =                       // map容器
    {{&quot;one&quot;,1}, {&quot;two&quot;, 2}};               // 初始化列表 
j[&quot;kv&quot;] = m;                               // &quot;kv&quot;:{&quot;one&quot;:1,&quot;two&quot;:2}

```

添加完之后，用成员函数dump()就可以序列化，得到它的JSON文本形式。默认的格式是紧凑输出，没有缩进，如果想要更容易阅读的话，可以加上指示缩进的参数：

```
cout &lt;&lt; j.dump() &lt;&lt; endl;         // 序列化，无缩进
cout &lt;&lt; j.dump(2) &lt;&lt; endl;        // 序列化，有缩进，2个空格

```

json_t的反序列化功能同样也很简单，只要调用静态成员函数parse()就行，直接得到JSON对象，而且可以用auto自动推导类型：

```
string str = R&quot;({                // JSON文本，原始字符串
    &quot;name&quot;: &quot;peter&quot;,
    &quot;age&quot; : 23,
    &quot;married&quot; : true
})&quot;;

auto j = json_t::parse(str);    // 从字符串反序列化
assert(j[&quot;age&quot;] == 23);        // 验证序列化是否正确
assert(j[&quot;name&quot;] == &quot;peter&quot;);

```

json_t使用异常来处理解析时可能发生的错误，如果你不能保证JSON数据的完整性，就要使用try-catch来保护代码，防止错误数据导致程序崩溃：

```
auto txt = &quot;bad:data&quot;s;        // 不是正确的JSON数据

try                             // try保护代码
{
    auto j = json_t::parse(txt);// 从字符串反序列化
}
catch(std::exception&amp; e)        // 捕获异常
{
    cout &lt;&lt; e.what() &lt;&lt; endl;
}

```

对于通常的应用来说，掌握了基本的序列化和反序列化就够用了，不过JSON for Modern C++里还有很多高级用法，比如SAX、BSON、自定义类型转换等。如果你需要这些功能，可以去看[它的文档](https://github.com/nlohmann/json)，里面写得都很详细。

## MessagePack

说完JSON，再来说另外第二种格式：MessagePack。

它也是一种轻量级的数据交换格式，与JSON的不同之处在于它不是纯文本，而是二进制。所以MessagePack就比JSON更小巧，处理起来更快，不过也就没有JSON那么直观、易读、好修改了。

由于二进制这个特点，MessagePack也得到了广泛的应用，著名的有Redis、Pinterest。

MessagePack支持几乎所有的编程语言，你可以在[官网](https://msgpack.org/)上找到它的C++实现。

我常用的是官方库msgpack-c，可以用apt-get直接安装。

```
apt-get install libmsgpack-dev

```

但这种安装方式有个问题，可能发行方仓库里的是老版本（像Ubuntu 16.04就是0.57），缺失很多功能，所以最好是从[GitHub](https://github.com/msgpack/msgpack-c)上下载最新版，编译时手动指定包含路径：

```
git clone git@github.com:msgpack/msgpack-c.git

g++ msgpack.cpp -std=c++14 -I../common/include -o a.out

```

和JSON for Modern C++一样，msgpack-c也是仅头文件的库（head only），只要包含一个头文件“msgpack.hpp”就行了，不需要额外的编译链接选项（C版本需要用“-lmsgpackc”链接）。

但MessagePack的设计理念和JSON是完全不同的，它没有定义JSON那样的数据结构，而是比较底层，只能对基本类型和标准容器序列化/反序列化，需要你自己去组织、整理要序列化的数据。

我拿vector容器来举个例子，调用pack()函数序列化为MessagePack格式：

```
vector&lt;int&gt; v = {1,2,3,4,5};              // vector容器

msgpack::sbuffer sbuf;                    // 输出缓冲区
msgpack::pack(sbuf, v);                   // 序列化

```

从代码里你可以看到，它的用法不像JSON那么简单直观，**必须同时传递序列化的输出目标和被序列化的对象**。

输出目标sbuffer是个简单的缓冲区，你可以把它理解成是对字符串数组的封装，和`vector&lt;char&gt;`很像，也可以用data()和size()方法获取内部的数据和长度。

```
cout &lt;&lt; sbuf.size() &lt;&lt; endl;            // 查看序列化后数据的长度

```

除了sbuffer，你还可以选择另外的zbuffer、fbuffer。它们是压缩输出和文件输出，和sbuffer只是格式不同，用法是相同的，所以后面我就都用sbuffer来举例说明。

MessagePack反序列化的时候略微麻烦一些，要用到函数unpack()和两个核心类：object_handle和object。

函数unpack()反序列化数据，得到的是一个object_handle，再调用get()，就是object：

```
auto handle = msgpack::unpack(          // 反序列化
            sbuf.data(), sbuf.size());  // 输入二进制数据
auto obj = handle.get();                // 得到反序列化对象

```

这个object是MessagePack对数据的封装，相当于JSON for Modern C++的JSON对象，但你不能直接使用，必须知道数据的原始类型，才能转换还原：

```
vector&lt;int&gt; v2;                       // vector容器
obj.convert(v2);                      // 转换反序列化的数据

assert(std::equal(                    // 算法比较两个容器
      begin(v), end(v), begin(v2)));

```

因为MessagePack不能直接打包复杂数据，所以用起来就比JSON麻烦一些，你必须自己把数据逐个序列化，连在一起才行。

好在MessagePack又提供了一个packer类，可以实现串联的序列化操作，简化代码：

```
msgpack::sbuffer sbuf;                         // 输出缓冲区
msgpack::packer&lt;decltype(sbuf)&gt; packer(sbuf);  // 专门的序列化对象

packer.pack(10).pack(&quot;monado&quot;s)                // 连续序列化多个数据
      .pack(vector&lt;int&gt;{1,2,3});

```

对于多个对象连续序列化后的数据，反序列化的时候可以用一个偏移量（offset）参数来同样连续操作：

```
for(decltype(sbuf.size()) offset = 0;          // 初始偏移量是0
    offset != sbuf.size();){                   // 直至反序列化结束

    auto handle = msgpack::unpack(            // 反序列化
            sbuf.data(), sbuf.size(), offset);  // 输入二进制数据和偏移量
    auto obj = handle.get();                  // 得到反序列化对象
}

```

但这样还是比较麻烦，能不能像JSON那样，直接对类型序列化和反序列化呢？

MessagePack为此提供了一个特别的宏：MSGPACK_DEFINE，把它放进你的类定义里，就可以像标准类型一样被MessagePack处理。

下面定义了一个简单的Book类：

```
class Book final                       // 自定义类
{
public:
    int         id;
    string      title;
    set&lt;string&gt; tags;
public:
    MSGPACK_DEFINE(id, title, tags);   // 实现序列化功能的宏
};

```

它可以直接用于pack()和unpack()，基本上和JSON差不多了：

```
Book book1 = {1, &quot;1984&quot;, {&quot;a&quot;,&quot;b&quot;}};  // 自定义类

msgpack::sbuffer sbuf;                    // 输出缓冲区
msgpack::pack(sbuf, book1);              // 序列化

auto obj = msgpack::unpack(              // 反序列化
      sbuf.data(), sbuf.size()).get();   // 得到反序列化对象

Book book2;
obj.convert(book2);                      // 转换反序列化的数据

assert(book2.id == book1.id);
assert(book2.tags.size() == 2);
cout &lt;&lt; book2.title &lt;&lt; endl;

```

使用MessagePack的时候，你也要注意数据不完整的问题，必须要用try-catch来保护代码，捕获异常：

```
auto txt = &quot;&quot;s;                      // 空数据
try                                  // try保护代码
{
    auto handle = msgpack::unpack(   // 反序列化
        txt.data(), txt.size());
}
catch(std::exception&amp; e)            // 捕获异常
{
    cout &lt;&lt; e.what() &lt;&lt; endl;
}

```

## ProtoBuffer

第三个要说的库就是著名的[ProtoBuffer](https://github.com/protocolbuffers/protobuf)，通常简称为PB，由Google出品。

PB也是一种二进制的数据格式，但毕竟是工业级产品，所以没有JSON和MessagePack那么“轻”，相关的东西比较多，要安装一个预处理器和开发库，编译时还要链接动态库（-lprotobuf）：

```
apt-get install protobuf-compiler
apt-get install libprotobuf-dev

g++ protobuf.cpp -std=c++14 -lprotobuf -o a.out

```

**PB的另一个特点是数据有“模式”（schema）**，必须要先写一个IDL（Interface Description Language）文件，在里面定义好数据结构，只有预先定义了的数据结构，才能被序列化和反序列化。

这个特点既有好处也有坏处：一方面，接口就是清晰明确的规范文档，沟通交流简单无歧义；而另一方面，就是缺乏灵活性，改接口会导致一连串的操作，有点繁琐。

下面是一个简单的PB定义：

```
syntax = &quot;proto2&quot;;                    // 使用第2版

package sample;                        // 定义名字空间

message Vendor                        // 定义消息
{
    required uint32     id      = 1;  // required表示必须字段
    required string     name    = 2;  // 有int32/string等基本类型
    required bool       valid   = 3;  // 需要指定字段的序号，序列化时用
    optional string     tel     = 4;  // optional字段可以没有
}

```

有了接口定义文件，需要再用protoc工具生成对应的C++源码，然后把源码文件加入自己的项目中，就可以使用了：

```
protoc --cpp_out=. sample.proto       // 生成C++代码

```

由于PB相关的资料实在太多了，这里我就只简单说一下重要的接口：

- 字段名会生成对应的has/set函数，检查是否存在和设置值；
- IsInitialized()检查数据是否完整（required字段必须有值）；
- DebugString()输出数据的可读字符串描述；
- ByteSize()返回序列化数据的长度；
- SerializeToString()从对象序列化到字符串；
- ParseFromString()从字符串反序列化到对象；
- SerializeToArray()/ParseFromArray()序列化的目标是字节数组。

下面的代码示范了PB的用法：

```
using vendor_t = sample::Vendor;        // 类型别名

vendor_t v;                             // 声明一个PB对象
assert(!v.IsInitialized());            // required等字段未初始化

v.set_id(1);                            // 设置每个字段的值    
v.set_name(&quot;sony&quot;);
v.set_valid(true);

assert(v.IsInitialized());             // required等字段都设置了，数据完整
assert(v.has_id() &amp;&amp; v.id() == 1); 
assert(v.has_name() &amp;&amp; v.name() == &quot;sony&quot;);
assert(v.has_valid() &amp;&amp; v.valid());

cout &lt;&lt; v.DebugString() &lt;&lt; endl;       // 输出调试字符串

string enc;
v.SerializeToString(&amp;enc);              // 序列化到字符串 

vendor_t v2; 
assert(!v2.IsInitialized());
v2.ParseFromString(enc);               // 反序列化

```

虽然业界很多大厂都在使用PB，但我觉得它真不能算是最好的，IDL定义和接口都太死板生硬，还只能用最基本的数据类型，不支持标准容器，在现代C++里显得“不太合群”，用起来有点别扭。

不过它后面有Google“撑腰”，而且最近几年又有gRPC“助拳”，所以很多时候也不得不用。

PB的另一个缺点是官方支持的编程语言太少，通用性较差，最常用的proto2只有C++、Java和Python。后来的proto3增加了对Go、Ruby等的支持，但仍然不能和JSON、MessagePack相比。

## 小结

好了，今天我讲了三种数据交换格式：JSON、MessagePack和ProtoBuffer。

这三种数据格式各有特色，在很多领域都得到了广泛的应用，我来简单小结一下：

1. JSON是纯文本，容易阅读，方便编辑，适用性最广；
1. MessagePack是二进制，小巧高效，在开源界接受程度比较高；
1. ProtoBuffer是工业级的数据格式，注重安全和性能，多用在大公司的商业产品里。

有很多开源库支持这些数据格式，官方的、民间的都有，你应该选择适合自己的高质量库，必要的时候可以做些测试。

再补充一点，除了今天说的这三种，你还可以尝试其他的数据格式，比较知名的有Avro、Thrift，虽然它们有点冷门，但也有自己的独到之处（比如，天生支持RPC、可选择多种序列化格式和传输方式）。

## 课下作业

最后是课下作业时间，给你留两个思考题：

1. 为什么要有序列化和反序列化，直接memcpy内存数据行不行呢？
1. 你最常用的是哪种数据格式？它有什么优缺点？

欢迎你在留言区写下你的思考和答案，如果觉得今天的内容对你有所帮助，也欢迎分享给你的朋友。我们下节课见。

<img src="https://static001.geekbang.org/resource/image/a3/f1/a39719e615f1124d60b5b9ca51b88cf1.png" alt="">
