工作中可能面临如下需求:
- 将结构化数据,比如类,通过网络传输到其他地方。
- 高成本计算得到的数据,要临时保存到磁盘中,希望下一次读取进来直接就是格式化好的数据;而不再需要一个个地读取进来,在内存中重建构建数据结构。
我们可以用序列化/反序列化来完成上述任务。序列化是指,将用复杂数据结构构建的数据,变为字符串或者二进制流;反序列化则是相反过程,直接从字符串或者二进制流中完全恢复内存现场。
目前常用的C++序列化库有Google Protobuf,Boost.Serialization,Thrift。根据反映的情况,效率上Protobuf最强,Thrift围绕序列化的功能最丰富,Boost.Serialization则直接支持STL容器的序列化。编码上讲,protobuf较难,Boost.Serialization最简单。还有资料说,Protobuf环境构建最简单,Boost最难。不过Boost的prebuilt binary package在sourceforge上随时可以下载到,解压无需编译就可用全部功能。
Boost.Serialization的教程很多,不过大部分是官网教程的中文翻译。实际上官网教程比较初级,实用到业务上还不够。这里解答几个业务上可能遇到的疑问。
如何序列化动态数组
根据资料,Boost.Serialization不能直接序列化数组,必须将数组包括到一个类中。教程中,Boost.Serialization可直接序列化类中的固定数组:
class bus_route
{
friend class boost::serialization::access;
bus_stop * stops[10];
template<class Archive>
void serialize(Archive & ar, const unsigned int version)
{
ar & stops;
}
public:
bus_route(){}
};
显然,相当多情况下,我们会new一个动态数组来使用,上述教程就没明说怎么做。为了序列化动态数组,我们必须先定义数组长度,然后用for循环进行序列化:
template<typename T>
struct Array {
Array() { arr_ = 0; len_ = 0; }
~Array() { if (arr_) delete[] arr_; arr_ = 0; len_ = 0; }
T* arr_;
size_t len_;
};
使用非侵入式方法来写:
namespace boost {
namespace serialization {
template<typename Archive, typename T>
void serialize(Archive & ar, Array<T>& g, const unsigned int version)
{
ar & g.len_;
if (g.arr_ == 0) {
g.arr_ = new T[g.len_];
}
for (size_t i = 0; i < g.len_; ++i) {
ar & g.arr_[i];
}
}
}
}
先要序列化和反序列化动态数组的长度信息,然后恢复数组数据。serialize函数既用于序列化也用于反序列化。
进行多次序列化/反序列化
内存序列化的典型代码如下:
std::stringstream ss;、
boost::archive::text_oarchive oa(ss);
oa << serializa_object1;//序列化
boost::archive::text_iarchive ia(ss);
ia >> deserializa_object1;
这里用std::stringstream ss作为序列化辅助工具,在调试模式下我们可以监视ss的内容,看到object被序列化后的字符串内容。接下来,我们再次进行序列化操作,然后会报错:
std::stringstream ss;
boost::archive::text_oarchive oa(ss);
oa << serializa_object1;//序列化
boost::archive::text_iarchive ia(ss);
ia >> deserializa_object1;
oa << serializa_object2;//序列化第二次
ia >> deserializa_object2;//报错
在调试模式下可以看到,ss的内容没变,程序报错。因此我们要重初始化std::stringstream ss才行:
std::stringstream ss;
boost::archive::text_oarchive oa(ss);
oa << serializa_object1;//序列化
boost::archive::text_iarchive ia(ss);
ia >> deserializa_object1;
ss.clear();
oa << serializa_object2;//序列化第二次
ia >> deserializa_object2;//正确
序列化到指定缓冲区
在网络传输业务中,我们需要把序列化的内容放到char*缓冲区中,让asio传输数据。如果使用std::stringstream ss作为序列化辅助工具,那么数据内容需要从ss中提取:
std::stringstream ss;
boost::archive::text_oarchive oa(ss);
oa << serializa_object1;//序列化
std::string content = ss.str();//提取内容
然后std::string content就可以进行后续处理,比如直接用content.c_str()当作asio缓冲区,或者讲数据复制到char *buffer。
问题来了,序列化要转录一遍,效率未免有点低?如果直接让Boost.Serialization把序列化数据存放到指定的char *buffer,从指定的char *buffer反序列化数据,效率就高了。一个可用代码如下:
char i_buffer[4096];
char o_buffer[4096];//缓冲区
boost::iostreams::basic_array_sink<char> sr(o_buffer, 4096);
boost::iostreams::stream< boost::iostreams::basic_array_sink<char> > os(sr);//序列化用
boost::iostreams::basic_array_source<char> device(i_buffer, 4096);
boost::iostreams::stream<boost::iostreams::basic_array_source<char> > is(device);//反序列化用
Array<int> arr;
arr.len_ = 10;
arr.arr_ = new int[arr.len_];
int id = 0;
std::for_each(arr.arr_, arr.arr_ + 10, [&](int &arg) {arg = ++id; });//填充数据
std::for_each(arr.arr_, arr.arr_ + 10, [](int &arg) {std::cout << arg << " "; });
std::cout << std::endl;//打印
boost::archive::binary_oarchive oa(os);
oa << arr;
memcpy(i_buffer, o_buffer, 4096);//模拟网络传输
Array<int> darr;
boost::archive::binary_iarchive ia(is);
ia >> darr;//反序列化
std::for_each(darr.arr_, darr.arr_ + 10, [&](int &arg) {std::cout << arg << " "; });
std::cout << std::endl;
//重初始化,以重复利用
memset(i_buffer, 0, 4096);
memset(o_buffer, 0, 4096);
os.close();
os.open(sr);
is.close();
is.open(device);
std::for_each(arr.arr_, arr.arr_ + 10, [&](int &arg) {arg = ++id; });
oa << arr;
memcpy(i_buffer, o_buffer, 4096);
ia >> darr;
std::for_each(darr.arr_, darr.arr_ + 10, [&](int &arg) {std::cout << arg << " "; });
std::cout << std::endl;
这样我们序列化的数据都固定存放在char* buffer上,不再需要从std::string转一道。
模板库序列化
Eigen是一个基于模板技术的数学库。因为模板技术,它无需编译即可运行,但是编译期报错就头疼,此外对于序列化也造成一定困扰。比如可能理所当然的写出下面的错误代码:
template<typename Archive, typename T>
void serialize(Archive &ar, Eigen::Matrix3d &mat, const unsigned int version) {//报错!!
for(int i = 0; i < 3; ++i){
for(int j = 0; j < 3; ++j){
ar & mat(i,j);
}
}
}
原因是,Eigen::Matrix3d不是真实类名,这是一个带有很长模板参数的类。那么正确代码应该是:
template< class Archive,
class S,
int Rows_,
int Cols_,
int Ops_,
int MaxRows_,
int MaxCols_>
inline void save(
Archive & ar,
const Eigen::Matrix<S, Rows_, Cols_, Ops_, MaxRows_, MaxCols_> & g,
const unsigned int version)
{
int rows = g.rows();
int cols = g.cols();
ar & rows;
ar & cols;
ar & boost::serialization::make_array(g.data(), rows * cols);
}
template< class Archive,
class S,
int Rows_,
int Cols_,
int Ops_,
int MaxRows_,
int MaxCols_>
inline void load(
Archive & ar,
Eigen::Matrix<S, Rows_, Cols_, Ops_, MaxRows_, MaxCols_> & g,
const unsigned int version)
{
int rows, cols;
ar & rows;
ar & cols;
g.resize(rows, cols);
ar & boost::serialization::make_array(g.data(), rows * cols);
}
template< class Archive,
class S,
int Rows_,
int Cols_,
int Ops_,
int MaxRows_,
int MaxCols_>
inline void serialize(
Archive & ar,
Eigen::Matrix<S, Rows_, Cols_, Ops_, MaxRows_, MaxCols_> & g,
const unsigned int version)
{
split_free(ar, g, version);//让读取和加载分为两个函数
}
然后我们就可以直接序列化Eigen库了。
然而后面还有一个问题,如果把Eigen对象作为类成员,那么还有问题在:
struct Data {
double val_;
Eigen::Vector3d vec_;
};
template<typename Archive>
void serialize(Archive &ar, Data &res, const unsigned int version) {
ar & res.val;
ar & res.vec_;//报错!!!
}
这是因为隶属于Data类的serialize函数,带有复杂的模板参数。遗憾的是,我还没找到解法,目前可行的策略是用侵入式写法:
struct Data {
double val_;
Eigen::Vector3d vec_;
friend class boost::serialization::access;
template<class Archive>
void serialize(Archive & ar, const unsigned int version)
{
ar & val_;
ar & vec_;//正确
}
};
常见报错
- 报错“invalid signature”,是serialize函数出了问题,比如没有编写serialize函数,或者找不到可以重载serialize函数,比如上面对Eigen矩阵的序列化,要填入长长的模板参数才是正确的serialize函数。
- 报错“length_error”,尤其是boost::archive::binary_iarchive ia对象初始化的时候报错。这是因为ia在初始化的时候,就已经读取了缓冲区数据并进行解析,缓冲区的第一个数据指代了需要读取缓冲区的长度,如果缓冲区没有数据,那么就会报错。因此要在数据传入缓冲区后,才能构建一个ia对象。