最经常使用的两种C++序列化方案的使用心得(protobuf和boost serialization)

导读


1. 什么是序列化?

2. 为什么要序列化?优点在哪里?

3. C++对象序列化的四种方法

4. 最经常使用的两种序列化方案使用心得


正文


1. 什么是序列化?

程序猿在编写应用程序的时候往往须要将程序的某些数据存储在内存中,然后将其写入某个文件或是将它传输到网络中的还有一台计算机上以实现通讯。这个将程序数据转化成能被存储并传输的格式的过程被称为“序列化”(Serialization),而它的逆过程则可被称为“反序列化”(Deserialization)。

简单来说,序列化就是将对象实例的状态转换为可保持或传输的格式的过程。与序列化相对的是反序列化,它依据流重构对象。这两个过程结合起来,能够轻松地存储和数据传输。比如,能够序列化一个对象,然后使用 HTTP 通过 Internet 在client和server之间传输该对象。

总结

序列化:将对象变成字节流的形式传出去。

反序列化:从字节流恢复成原来的对象。


2. 为什么要序列化?优点在哪里?

简单来说,对象序列化通经常使用于两个目的: 

(1) 将对象存储于硬盘上  ,便于以后反序列化使用

(2)在网络上传送对象的字节序列


对象序列化的优点在哪里?网络传输方面的便捷性、灵活性就不说了,这里举个我们常常可能发生的需求:你有一个数据结构,里面存储的数据是经过非常多其他数据通过非常复杂的算法生成的,因为数据量非常大,算法又复杂,因此生成该数据结构所用数据的时间可能要非常久(或许几个小时,甚至几天),生成该数据结构后又要用作其他的计算,那么你在调试阶段,每次执行个程序,就光生成数据结构就要花上这么长的时间,无疑代价是非常大的。假设你确定生成数据结构的算法不会变或不常变,那么就能够通过序列化技术生成数据结构数据存储到磁盘上,下次又一次执行程序时仅仅须要从磁盘上读取该对象数据就可以,所花费时间也就读一个文件的时间,可想而知是多么的快,节省了我们的开发时间。


3. C++对象序列化的四种方法

将C++对象进行序列化的方法一般有四种,以下分别介绍:


3.1 Google Protocol Buffers(protobuf)

Google Protocol Buffers (GPB)是Google内部使用的数据编码方式,旨在用来取代XML进行数据交换。可用于数据序列化与反序列化。主要特性有:

  • 高效
  • 语言中立(Cpp, Java, Python)
  • 可扩展

官方文档


3.2 Boost.Serialization

Boost.Serialization能够创建或重建程序中的等效结构,并保存为二进制数据、文本数据、XML或者实用户自己定义的其它文件。该库具有下面吸引人的特性:

  • 代码可移植(实现仅依赖于ANSI C++)。
  • 深度指针保存与恢复。
  • 能够序列化STL容器和其它经常使用模版库。
  • 数据可移植。
  • 非入侵性。

3.3 MFC Serialization

Windows平台下可使用MFC中的序列化方法。MFC 对 CObject 类中的序列化提供内置支持。因此,全部从 CObject 派生的类都可利用 CObject 的序列化协议。

MSDN中的介绍


3.4 .Net Framework

.NET的执行时环境用来支持用户定义类型的流化的机制。它在此过程中,先将对象的公共字段和私有字段以及类的名称(包含类所在的程序集)转换为字节流,然后再把字节流写入数据流。在随后对对象进行反序列化时,将创建出与原对象全然同样的副本。


3.5 简单总结

这几种序列化方案各有优缺点,各有自己的适用场景。当中MFC和.Net框架的方法适用范围非常窄,仅仅适用于Windows下,且.Net框架方法还须要.Net的执行环境參考文献1从序列化时间、反序列化时间和产生数据文件大小这几个方面比較了前三种序列化方案,得出结论例如以下(仅供參考):

  • Google Protocol Buffers效率较高,可是数据对象必须预先定义,并使用protoc编译,适合要求效率,同意自己定义类型的内部场合使用。
  • Boost.Serialization 使用灵活简单,并且支持标准C++容器。
  • 相比而言,MFC的效率较低,可是结合MSVS平台使用最为方便。

为了考虑平台的移植性、适用性和高效性,推荐大家使用Google的protobuf和Boost的序列化方案,以下介绍我使用这两种方案的心得及注意事项。


4. 最经常使用的两种序列化方案使用心得

关于这两种方案的详细使用和演示样例没什么好写的,由于优秀的參考资料非常多,请看后面给出的相关參考资料,这里仅仅给出我使用时的一些心得,方便大家在选择序列化方案时有个正确的參考,避免选择错误,浪费时间。


4.1 Google Protocol Buffers

protobuf相对而言效率应该是最高的,无论是安装效率还是使用效率,protobuf都非常高效,并且protobuf不仅用于C++序列化,还可用于Java和Python的序列化,使用范围非常广。但在使用过程中要注意两个问题:


(1)protobuf支持的数据类型不是非常丰富

protobuf属于轻量级的,因此不能支持太多的数据类型,以下是protobuf支持的基本类型列表,一般都能满足需求,只是在选择方案之前,还是先看看是否都能支持,以免前功尽弃。相同该表也值得收藏,作为我们在定义类型时做參考。

.proto type

c++

notes

double

double

 

float

float

 

int32

int32

使用可变长编码方式,负数时不够高效,应该使用sint32

int64

int64

同上

uint32

uint32

使用可变长编码方式

uint64

uint64

同上

sint32

int32

使用可变长编码方式,有符号的整型值,编码时比通常的int32高效

sint64

sint64

同上

fixed32

uint32

总是4个字节,假设数值总是比2^28大的话,这个类型会比uint32高效

fixed64

uint64

总是8个字节,假设数值总是比2^56大的话,这个类型会比uint64高效

sfixed32

int32

总是4个字节

sfixed64

int64

总是8个字节

bool

bool

 

string

string

一个字符串必须是utf-8编码或者7-bitascii编码的文本

bytes

string

可能包括随意顺序的字节数据


(2)protobuf不支持二维数组(指针),不支持STL容器序列化

这个缺陷挺大,由于稍复杂点的数据结构或类结构里出现二维数组、二维指针和STL容器(set、list、map等)非常频繁,但由于protobuf简单的实现机制,仅仅支持一维数组和指针(用repeated修饰符修饰),不能使用repeated repeated来支持二维数组,也不支持STL,因此在选择该方案之前,一定 要确保你的数据结构里没有这些不支持的类型。


(3)protobuf嵌套后会改变类名称

protobuf支持类的嵌套,即在一个自己定义类型中能够定义还有一个自己定义类型,但注意嵌套的自己定义类型在经过protobuf处理后生成的类名称并非你定义的类名称,而是加上了外层的类名称作为前缀,以下举一个简单的样例:

message DFA {
    required int32 _size = 1;
 
    message accept_pair {
      required bool is_accept_state = 1;
      required bool is_strict_end = 2;
      optional string app_name = 3;
    }
 
    repeated accept_pair accept_states = 2;
}

那么嵌套中的accept_pair 生成后的类不是accept_pair 而是DFA_accept_pair 。假设不想改类名称,将accept_pair 拿到外面与DFA平行定义就可以。


4.2 Boost.Serialization

Boost库是个非常庞大的库,功能非常丰富,序列化仅仅是当中的一个小分支,但为了使用Boost的序列化方案,你须要安装整个Boost库,所花费的磁盘空间和时间都非常多,相同支持的序列化功能也非常强大,既支持二维数组(指针),也支持STL容器,更不须要我们用某种特殊的格式又一次定义我们的类结构,其非侵入的性质使得我们无须修改已有的类结构就可以序列化,这时非常赞的一个性质。可是因为体积庞大,安装复杂,假设仅仅是简单的序列化,不是必需使用该方案,仅仅有protobuf不能满足你的需求时,才应该考虑该方案。


(1)安装boost库遇到的一系列问题

安装boost库本事就是一项非常费时的project,假设期间出现了各种错误,更加耗时耗耐心。我们能够从官网下载Boost库的二进制源代码进行安装,安装方法请參考网络或后面我给出的參考资料,以下给出安装时的注意事项:

注意1:要用root权限进行安装,否则会在安装过程中报错,提示权限不足。

注意2:boost库的安装依赖一些环境,通常有Python、bzip2和zlib,它们所在的软件包分别为:

Ubuntu下:

zlib1g-dev 
libbz2-dev 
libpython2.7-dev (and libpython3.3-dev)

Fedora/Redhat下:
zlib-devel 
libbz2-devel 
python-devel (and python3-devel) 

这也是安装过程中报错的主要来源。

报错1:假设Python库不完整,可能会报“ fatal error: pyconfig.h: No such file or directory compilation terminated.”错误。解决方法例如以下:

Fedora系统:sudo yum install python-devel

Ubuntu系统:sudo apt-get  install python-dev


报错2:报错 “ libs/iostreams/src/bzip2.cpp:20:56: fatal error: bzlib.h: No such file or directory”,解决方式:

Fedora系统:sudo yum install bzip2-devel

Ubuntu系统或Debian系统:sudo apt-get install libbz2-dev


通常对于这些错误,在Ubuntu系统下一般能够通过sudo apt-get install libboost-all-dev所有解决,但不一定行得通。


(2)成功安装后,假设未指定安装位置,那么默认将会安装到/usr/local/lib和/usr/local/include下,那么我们在使用Boost库进行编译时就须要使用-L和-I參数加上详细的lib和include路径,像以下这样:

g++ -o test boost_test.cpp -I$BOOST_INCLUDE -L$BOOST_LIB -lboost_serialization

假设认为每次都这样非常麻烦,那么能够将我们所要用到的lib和include文件增加到环境变量中,像以下这样:

sudo cp /usr/local/lib/libboost_serialization.* /usr/lib
sudo cp -r /usr/local/include/boost /usr/include

然后在编译时直接g++ -o test boost_test.cpp -lboost_serialization就可以。

注意:boost以下有两个序列化lib文件:ibboost_serialization.lib 和 libboost_wserialization.lib,那么这两者有什么差别呢?

事实上'w' 表示使用的是宽字符,比如 wchar_t。


(3)boost不尽人意的地方

  • 基本类型指针非常难序列化,比如int *array,官网上是这么说的:
    “By default, data types designated primitive by Implementation Levelclass serialization trait are never tracked. If it is desired totrack a shared primitive object through a pointer (e.g. along used as a reference count), It should be wrappedin a class/struct so that it is an identifiable type.The alternative of changing the implementation level of alongwould affect alllongs serialized in the wholeprogram - probably not what one would intend.”
    也就是说假设你想序列化原生类型的指针,须要给其加上struct或class使其变为类类型再序列化,可见有些麻烦,这种需求往往也非常频繁,鉴于序列化机制的实现原理,boost库临时还不能非常好的支持基本类型的指针序列化。
  • 不能序列化变长数组(variable-sized array),会报错说变长数组不是模板类类型。


(4)假设须要定义一个对象数组,如定义含有2个元素的class A对象数组,那么必须用A a[2]定义而不能用对象的指针A *a = new A[2]定义,这样序列化a后默认当作一个A对象处理,因此仅仅能存储一个对象的值,后面的不会存储。


(5)所谓boost非常人性的非侵入性质也有一定的条件:假设不想修改原来的类,那么原来的类属性必须是public的,这非常easy解释,由于你必需要能在别处訪问到这些属性并定义其序列化方式,当然这也在其他地方暴露了类的结构,具有一定的劣势。这种条件往往非常难满足,由于我们定义的类属性一般都是private的,假设是这样,且仍想要使用非侵入性质,那么需要在类中加入下面声明来开放訪问给 serialization 库:

friend class boost::serialization::access; 

这种方式比让成员public更好。


參考资料

posted @ 2014-08-28 09:48  mfrbuaa  阅读(9513)  评论(0编辑  收藏  举报