工作中可能面臨如下需求:
- 將結構化數據,比如類,通過網絡傳輸到其他地方。
- 高成本計算得到的數據,要臨時保存到磁盤中,希望下一次讀取進來直接就是格式化好的數據;而不再需要一個個地讀取進來,在內存中重建構建數據結構。
我們可以用序列化/反序列化來完成上述任務。序列化是指,將用複雜數據結構構建的數據,變爲字符串或者二進制流;反序列化則是相反過程,直接從字符串或者二進制流中完全恢復內存現場。
目前常用的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對象。