C++ 序列化和反序列化

可為測控發表於2023-01-14

序列化
1、背景
1、在TCP的連線上,它傳輸資料的基本形式就是二進位制流,也就是一段一段的1和0。

2、在一般程式語言或者網路框架提供的API中,傳輸資料的基本形式是位元組,也就是Byte。一個位元組就是8個二進位制位,8個Bit。

二進位制流和位元組流本質上是一樣的。對於我們編寫的程式來說,它需要透過網路傳輸的資料是結構化的資料,比如,一條命令、一段文字或者一條訊息。對應程式碼中,這些結構化的資料都可以用一個類或者一個結構體來表示。
序列化的用途除了用於在網路上傳輸資料以外,

將結構化資料儲存在檔案中(將物件儲存於硬碟上),因為檔案內儲存資料的形式也是二進位制序列。

問題:
在記憶體裡存放的任何資料,它最基礎的儲存單元也是二進位制位元,也就是說,我們應用程式操作的物件,它在記憶體中也是使用二進位制儲存的,既然都是二進位制,為什麼不能直接把記憶體中,物件對應的二進位制資料直接透過網路傳送出去,或者儲存在檔案中呢?為什麼還需要序列化和反序列化呢?

記憶體裡存的東西,不通用, 不同系統, 不同語言的組織可能都是不一樣的, 而且還存在很多引用,指標,並不是直接資料塊。記憶體中的物件資料應該具有語言獨特性,例如表達相同業務的User物件(id/name/age欄位),Java和PHP在記憶體中的資料格式應該不一樣的,如果直接用記憶體中的資料,可能會造成語言不通。只要對序列化的資料格式進行了協商,任何2個語言直接都可以進行序列化傳輸、接收。
一個資料結構,裡面儲存的資料是經過非常多其他資料透過非常複雜的演演算法生成的,因為資料量非常大,因此生成該資料結構所用資料的時間可能要非常久,生成該資料結構後又要用作其他的計算,那麼你在除錯階段,每次執行個程式,就光生成資料結構就要花上這麼長的時間。假設你確定生成資料結構的演演算法不會變或不常變,那麼就能夠透過序列化技術生成資料結構資料儲存到磁碟上,下次又一次執行程式時僅僅須要從磁碟上讀取該物件資料就可以,所花費時間也就讀一個檔案的時間。
雖然都是二進位制的資料,但是序列化的二進位制資料是透過一定的協議將資料欄位進行拼接。第一個優勢是:不同的語言都可以遵循這種協議進行解析,實現了跨語言。第二個優勢是:這種資料可以直接持久化到磁碟,從磁碟讀取後也可以透過這個協議解析出來。
2、定義
要想使用網路框架的API來傳輸結構化的資料,必須得先實現結構化的資料與位元組流之間的雙向轉換。這種將結構化資料轉換成位元組流的過程,稱為序列化,反過來轉換,就是反序列化。

簡單來說,序列化就是將物件例項的狀態轉換為可保持或傳輸的格式的過程。與序列化相對的是反序列化,它依據流重構物件。這兩個過程結合起來,能夠輕鬆地儲存和資料傳輸。
比如,能夠序列化一個物件,然後使用HTTP 透過 Internet 在client和server之間傳輸該物件。
3、序列化評價指標
1、可讀性
序列化後的資料最好是易於人類閱讀的
2、實現複雜度
實現的複雜度是否足夠低
3、效能
序列化和反序列化的速度越快越好
4、資訊密度
序列化後的資訊密度越大越好,也就是說,同樣的一個結構化資料,序列化之後佔用的儲存空間越小越好
03 | 08 7a 68 61 6e 67 73 61 6e | 17 | 01
User | z h a n g s a n | 23 | true
1
2
1.首先我們需要標識一下這個物件的型別,這裡面我們用一個位元組來表示型別,比如用 03 表示這是一個 User 型別的物件。
2.我們約定,按照 name、age、married 這個固定順序來序列化這三個屬性。按照順序,第一個欄位是 name,我們不存欄位名,直接存欄位值“zhangsan”就可以了,由於名字的長度不固定,我們用第一個位元組 08 表示這個名字的長度是 8
個位元組,後面的 8 個位元組就是 zhangsan。
3.第二個欄位是年齡,我們直接用一個位元組表示就可以了,23 的 16 進位制是 17 。
4.最後一個欄位是婚姻狀態,我們用一個位元組來表示,01 表示已婚,00 表示未婚,這裡面儲存一個 01。
同樣的一個User物件,JSON序列化後({"name":"zhangsan","age":"23","married":"true"})

JSON序列化後需要47個位元組,專用的序列化方法只要12個位元組就夠了。
專用的序列化方法顯然更高效,序列化出來的位元組更少,在網路傳輸過程中的速度也更快。但缺點是,需要為每種物件型別定義專門的序列化和反序列化方法,實現起來太複雜了,大部分情況下是不划算的。
4、序列化例項
案例1:

//Srlz1.cpp: 將一個類的一個物件序列化到檔案
#include <iostream>
#include <fcntl.h>
#include <vector>
#include <stdio.h>
using namespace std;
//定義類CA
//資料成員:int x;
//成員函式:Serialize:進行序列化函式
// Deserialize反序列化函式
// Show:資料成員輸出函式
class CA
{
private:
int x; //定義一個類的資料成員。 public:
CA() //預設建構函式
{
x = ;
}
CA(int y):x(y) //定義建構函式,用初始化列表初始化資料成員
{
}
virtual ~CA() //解構函式
{
}
  public:
//序列化函式:Serialize
//成功,返回0,失敗,返回0;
int Serialize(const char* pFilePath) const
{
int isSrlzed = -;
FILE* fp; //define a file pointer
//以讀寫方式開啟檔案,並判斷是否開啟;
       if ((fp = fopen(pFilePath, "w+")) == NULL)
  {
printf("file opened failure\n");
return -; //若開啟失敗,則返回-1;
}
//呼叫fwrite函式,將物件寫入檔案;
       isSrlzed = fwrite(&x, sizeof(int), , fp);        //判斷寫入是否成功;
       if ((- == isSrlzed) || ( == isSrlzed))
{
printf("Serialize failure\n");
return -; //若寫入失敗,則返回-1;
}
if(fclose(fp) != ) //關閉檔案
{
printf("Serialize file closed failure.\n");
return -;
}
printf("Serialize succeed.\n");
return ; //序列化成功,返回0;
 } //反序列化函式:
//成功,返回0,失敗,返回-1;
int Deserialize(const char* pFilePath)
{
int isDsrlzed = -;
FILE* fp;
//以讀寫方式開啟檔案,並判斷是否開啟;
if ((fp = fopen(pFilePath, "r+")) == NULL)
{
printf("file opened failure.\n");
return -;
}
//呼叫fread函式,讀取檔案中的物件 ;
       isDsrlzed = fread(&x, sizeof(int), , fp);
       //判斷是否成功讀入
if ((- == isDsrlzed)||( == isDsrlzed))
{
printf("Deserialize failure.\n");
return -; //若讀取失敗,則返回-1;
}
if(fclose(fp) != )
{
printf("Deserialize file closed failure.\n");
return -;
}
printf("Deserialize succeed.\n");
return ; //反序列化成功,則返回0;
     } //成員物件輸出顯示函式
void Show()
{
cout<< "in Show():"<< x << endl;
}
};


int main(int argc, char const *argv[])
{
  CA as(); //定義一個類物件,並初始化;
  //呼叫序列化函式,將物件as序列化到檔案data.txt中;
  as.Serialize("data.txt");  
  CA ad; //定義一個類物件,用來記錄反序列化物件
  //呼叫反序列化函式,將檔案data.txt中的物件反序列化到ad物件;
  ad.Deserialize("data.txt");   
  ad.Show(); //呼叫輸出顯示函式;   
return ;
}

更詳細的案例,簡單註釋:
1、CharVec.h : 定義一個 vector < char> 型別位元組陣列,也就是一個定義的容器,為下面的DataStream中存放資料提供介面:

#ifndef CHARVEC_H
#define CHARVEC_H
#include <memory>
class CharVec{
public:
CharVec();
CharVec(const CharVec &vec);
CharVec &operator =(const CharVec &vec);
~CharVec();

bool operator ==(const CharVec &vec) const;

size_t size() const;//vector裡實際存放資料的大小
size_t capacity() const;//capacity是vector的記憶體分配大小
char *begin() const;
char *end() const;

void push(const char *data, int len);
void push(const std::string &str);
void push(char c);
void removeFromFront(int len);
void clear();
private:
void checkAndAlloc();
void reallocate();
void free();
std::pair<char *, char *> allocAndCopy(char *begin, char *end);
private:
char *m_Elements; // 首元素
char *m_FirstFree; // 最後一個實際元素之後的位置
char *m_Cap; // 分配記憶體末尾之後的位置
std::allocator<char> m_Allocator; // 記憶體分配器
};
#endif // CHARVEC_H


2、CharVec.cpp :對CharVec.h 宣告的函式進行定義

// CharVec.cpp
#include "CharVec.h"
CharVec::CharVec() :m_Elements(nullptr), m_FirstFree(nullptr),m_Cap(nullptr)
{}//建構函式
CharVec::CharVec(const CharVec &vec)//複製構造
{
auto newData = allocAndCopy(vec.begin(), vec.end());//allocAndCopy分配空間,並且初始化
m_Elements = newData.first;
m_FirstFree = newData.second;
m_Cap = newData.second;
}
CharVec &CharVec::operator =(const CharVec &vec)//=過載
{
auto newData = allocAndCopy(vec.begin(), vec.end());
free();
m_Elements = newData.first;
m_FirstFree = newData.second;
m_Cap = newData.second;
return *this;
}
CharVec::~CharVec()//析構
{
free();
}

bool CharVec::operator ==(const CharVec &vec) const//==過載
{
if (m_Elements == vec.m_Elements &&
m_FirstFree == vec.m_FirstFree &&
m_Cap == vec.m_Cap) {
return true;
}
return false;
}
size_t CharVec::size() const//當前元素數目
{
return m_FirstFree - m_Elements;
}
size_t CharVec::capacity() const//容器總的空間大小
{
return m_Cap - m_Elements;
}

char *CharVec::begin() const
{
return m_Elements;
}
char *CharVec::end() const
{
return m_FirstFree;
}
void CharVec::push(const char *data, int len)
{
if (len <= 0) {
return ;
}
for (int i = 0; i < len; ++i) {
push(data[i]);
}
}
void CharVec::push(const std::string &str)
{
push(str.c_str(), str.size());
}
void CharVec::push(char c)
{
checkAndAlloc();
m_Allocator.construct(m_FirstFree++, c);
}
void CharVec::removeFromFront(int len)//從m_Element開始釋放掉len長度的資料。
{
if (len > size()) {
return ;
}
char *from = m_Elements;
char *to = m_Elements + len;
m_Elements += len;
for (int i = 0; i < len; ++i) {
m_Allocator.destroy(--to);
}
m_Allocator.deallocate(from, m_Elements - from);
}
void CharVec::clear()//容器清空操作
{
free();
m_Elements = nullptr;
m_FirstFree = nullptr;
m_Cap = nullptr;
}
//checkAndAlloc()會先判斷size是不是和capacity相等,
//然後呼叫reallocate進行記憶體的分配,重新分配的空間是原來的2倍,
//然後資料轉移,使用std::move而不是複製可以提高效率。
void CharVec::checkAndAlloc()
{
if (size() == capacity()) {
reallocate();
}
}
void CharVec::reallocate()//類似vector的擴容操作
{
auto newCapacity = size() ? 2 * size() : 1;//重新分配的空間是原來的2倍
auto newData = m_Allocator.allocate(newCapacity);//allocate分配空間
auto dest = newData;
auto ele = m_Elements;
for (size_t i = 0; i != size(); ++i) {
m_Allocator.construct(dest++, std::move(*ele++));//construct初始構造
}
free();
m_Elements = newData;
m_FirstFree = dest;
m_Cap = m_Elements + newCapacity;
}
void CharVec::free()
{
if (m_Elements) {
for (auto p = m_FirstFree; p != m_Elements;) {
m_Allocator.destroy(--p);//destroy析構物件,此時空間還是可以使用
}
m_Allocator.deallocate(m_Elements, m_Cap - m_Elements);//deallocate回收空間
}
}

std::pair<char *, char *> CharVec::allocAndCopy(char *begin, char *end)
{
auto startPos = m_Allocator.allocate(end - begin);
return {startPos, std::uninitialized_copy(begin, end, startPos)};
}


127
3、DataHeader類的宣告:定義id,及headerlen,totalLen相關客戶屬性

// DataHeader.h

#ifndef DATAHEADER_H
#define DATAHEADER_H

struct DataHeader
{
DataHeader(int id = 0);
bool operator==(const DataHeader &header);
void reset();
const static int s_HeaderLen = 3 * sizeof(int);
int m_Id;
int m_HeaderLen;
int m_TotalLen;
};
#endif // DATAHEADER_H


3、DataStream.h:

支援序列化和反序列化操作,
列舉繼承char型別意思是說這個列舉裡的列舉值底層是用char來儲存的
支援是序列化相關的資料型別,
往這個類的寫資料和讀資料都是提供了兩種形式,
流式運運算元(<< 或者 >> )和函式(readVal,writeVal)
// DataStream.h
#ifndef DATASTREAM_H
#define DATASTREAM_H
#include <memory>
#include <map>
#include <list>
#include <vector>
#include <set>
#include "DataHeader.h"
#include "CharVec.h"

class CustomTypeInterface;//前向宣告
class DataStream
{
public:
DataStream(std::unique_ptr<DataHeader> *header = nullptr);
DataStream(const DataStream &stream);

DataStream& operator =(const DataStream &stream);

enum class DataType : char {
UnKnown,
Boolean,
Char,
WChar,
Int,
UInt,
Int64,
Double,
String,
WString,
Vector,
List,
Map,
Set,
CustomType,
};

bool operator == (const DataStream &stream) const;
// 指陣列裡存放的資料
int totalSize() const { return m_Header->m_TotalLen; }//資料的總長
int headerSize() const { return m_Header->m_HeaderLen; }//頭部的長度
int dataSize() const {return m_Header->m_TotalLen - m_Header->m_HeaderLen;}//內容資料的長度

void clear();

// write
void writeHeader();
void writeData(const char *data, int len);
//這裡是寫入不同資料型別的資料
DataStream& operator<<(char val);
void writeVal(char val);

DataStream& operator<<(wchar_t val);
void writeVal(wchar_t val);

DataStream& operator <<(bool val);
void writeVal(bool val);

DataStream& operator <<(int val);
void writeVal(int val);

DataStream& operator <<(unsigned int val);
void writeVal(unsigned int val);

DataStream& operator <<(int64_t val);
void writeVal(int64_t val);

DataStream& operator <<(double val);
void writeVal(double val);

DataStream& operator <<(const std::string &val);
void writeVal(const std::string &val);

DataStream& operator <<(const std::wstring &val);
void writeVal(const std::wstring &val);

DataStream& operator <<(CustomTypeInterface *val);
void writeVal(CustomTypeInterface *val);
//這裡是往不同的STL容器中寫入模板型別的資料
template<typename T>
DataStream& operator <<(const std::vector<T>& val);

template<typename T>
void writeVal(const std::vector<T>& val);

template<typename T>
DataStream& operator <<(const std::list<T>& val);

template<typename T>
void writeVal(const std::list<T>& val);

template<typename T1, typename T2>
DataStream& operator <<(const std::map<T1, T2>& val);

template<typename T1, typename T2>
void writeVal(const std::map<T1, T2>& val);

template<typename T>
DataStream& operator <<(const std::set<T>& val);

template<typename T>
void writeVal(const std::set<T>& val);

// read
void readHeader(const char *data);

template<typename T>
bool readData(T *val);

bool operator>>(char &val);
bool readVal(char &val);

bool operator>>(wchar_t& val);
bool readVal(wchar_t &val);

bool operator>>(bool &val);
bool readVal(bool &val);

bool operator>>(int &val);
bool readVal(int &val);

bool operator>>(unsigned int &val);
bool readVal(unsigned int &val);

bool operator>>(int64_t &val);
bool readVal(int64_t &val);

bool operator>>(double &val);
bool readVal(double &val);

bool operator>>(std::string &val);
bool readVal(std::string &val);

bool operator>>(std::wstring &val);
bool readVal(std::wstring &val);

bool operator>>(CustomTypeInterface *val);
bool readVal(CustomTypeInterface *val);

template<typename T>
bool operator>>(std::vector<T> &val);

template<typename T>
bool readVal(std::vector<T> &val);

template<typename T>
bool operator>>(std::list<T> &val);

template<typename T>
bool readVal(std::list<T> &val);

template<typename T1, typename T2>
bool operator>>(std::map<T1, T2> &val);

template<typename T1, typename T2>
bool readVal(std::map<T1, T2> &val);

template<typename T>
bool operator>>(std::set<T> &val);

template<typename T>
bool readVal(std::set<T> &val);

// Serialize and Deserialize
int Serialize(char *buf) const;
bool Deserialize(const char *buf, int len);
private:
std::unique_ptr<DataHeader> m_Header;//儲存的客戶型別指標
CharVec m_DataBuffer;//儲存的容器
int m_IsFirstWrite;//判斷是否為第一次寫入
};



4、DataStream.cpp:DataStream.h檔案相關函式的實現

#include "DataStream.h"
#include "CustomTypeInterface.h"

DataStream::DataStream(std::unique_ptr<DataHeader> *header) :
m_IsFirstWrite(true)//建構函式的實現
{
if (header == nullptr) {//header物件為空指標,重置新的物件指標
m_Header.reset(new DataHeader);
}
else {
m_Header.reset(header->release());//release()釋放關聯的原始指標,unique_ptr相關的函式
}
}

 

DataStream::DataStream(const DataStream &stream)//複製構造
{
operator =(stream);
}

 

DataStream &DataStream::operator =(const DataStream &stream)//=過載
{
if (&stream == this) {//比較物件和原物件相同,沒有賦值的必要了
return *this;
}

m_Header.reset(new DataHeader);//過載並且初始化
*m_Header = *stream.m_Header;//相關賦值操作
m_DataBuffer = stream.m_DataBuffer;
m_IsFirstWrite = stream.m_IsFirstWrite;
return *this;
}

bool DataStream::operator ==(const DataStream &stream) const//==過載
{
if (&stream == this) {
return true;
}
if (m_Header.get() == stream.m_Header.get() &&
m_DataBuffer == stream.m_DataBuffer) {
return true;
}
return false;
}

void DataStream::clear()
{
m_IsFirstWrite = true;
m_DataBuffer.clear();
m_Header->reset();
}

 

void DataStream::writeHeader()
{
int headerLen = DataHeader::s_HeaderLen;
writeData((char *)&(m_Header->m_TotalLen), sizeof(int));
writeData((char *)&headerLen, sizeof(int));
writeData((char *)&m_Header->m_Id, sizeof(int));
m_Header->m_HeaderLen = headerLen;
}

 

void DataStream::writeData(const char *data, int len)
{
if (len == 0) {
return ;
}
//把他的type寫入,
//如果是第一寫入,先把header寫入, 然後再寫資料,更新totalLen
if (m_IsFirstWrite) {
m_IsFirstWrite = false;
writeHeader();
}
//然後在把資料寫入
m_DataBuffer.push(data, len);
m_Header->m_TotalLen += len;//更新totalLen
memcpy(m_DataBuffer.begin(), &m_Header->m_TotalLen, sizeof(int));
}

 

void DataStream::writeVal(char val)
{
char type = (char)DataType::Char;
writeData((char *)&(type), sizeof(char));
writeData(&val, sizeof(char));
}

void DataStream::writeVal(const std::string &val)
{
char type = (char)DataType::String;
writeData((char *)&(type), sizeof(char));
int size = val.size();
writeVal(size);
writeData(val.c_str(), size);
}

 

void DataStream::writeVal(CustomTypeInterface *val)
{
val->serialize(*this, (char)DataType::CustomType);
}

 

void DataStream::readHeader(const char *data)
{
int *p = (int *)data;
m_Header->m_TotalLen = *p++;
m_Header->m_HeaderLen = *p++;
m_Header->m_Id = *p++;
m_Header->m_TotalLen -= m_Header->m_HeaderLen;
m_Header->m_HeaderLen = 0;
}

//從dataBuffer的資料取出來,然後更新totalLen.
//由於這個函式是模板函式,所以我們把他放在了標頭檔案。
/*template<typename T>
bool DataStream::readData(T *val)
{
int size = m_DataBuffer.size();
int count = sizeof(T);
if (size < count) {
return false;
}
*val = *((T*)m_DataBuffer.begin());
m_DataBuffer.removeFromFront(count);
m_Header->m_TotalLen -= count;
return true;
}*/
//先讀取出來型別,然後讀取資料
bool DataStream::readVal(char &val)
{
char type = 0;
if (readData(&type) && type == (char)DataType::Char) {
return readData(&val);
}
return false;
}

 

bool DataStream::readVal(std::string &val)
{
char type = 0;
if (readData(&type) && type == (char)DataType::String) {
int len = 0;
if (readVal(len) && len > 0) {
val.assign(m_DataBuffer.begin(), len);
m_DataBuffer.removeFromFront(len);
m_Header->m_TotalLen -= len;
}
return true;
}
return false;
}

 

bool DataStream::readVal(CustomTypeInterface *val)
{
return val->deserialize(*this, (char)DataType::CustomType);
}

 

int DataStream::Serialize(char *buf) const//序列化
{
int totalLen = m_Header->m_TotalLen;
int size = m_DataBuffer.size();
if (size <= 0 || totalLen == 0 || size != totalLen) {
return 0;
}
memcpy(buf, m_DataBuffer.begin(), totalLen);
return totalLen;
}

 

bool DataStream::Deserialize(const char *buf, int len)//反序列化
{
if (buf == nullptr || len <= 0) {
return false;
}
readHeader(buf);
m_DataBuffer.clear();
m_DataBuffer.push(buf + DataHeader::s_HeaderLen, len - DataHeader::s_HeaderLen);
return true;
}



5、CustomTypeInterface:自定義型別,比如你自己定義了一個結構體,怎樣傳輸它呢。我們為自定義的結構體定義一個介面類。

class CustomTypeInterface
{
public:
virtual ~CustomTypeInterface() = default;
virtual void serialize(DataStream &stream, char type) const = 0;
virtual bool deserialize(DataStream &stream, char type) = 0;
};

1
2
3
4
5
6
7
8
測試

#include <iostream>
#include "DataStream.h"
#include "CustomTypeInterface.h"

class Test : public CustomTypeInterface
{
public:
SerializeAndDeserialize(Test, m_A * m_B);
public:
int m_A;
bool m_B;
};
int main(int argc, char *argv[])
{
char c1 = 'c';

Test t;
t.m_A = 1;
t.m_B = false;

DataStream stream;
stream.writeVal(c1);
stream.writeVal(&t);
int size = stream.totalSize();
char *data = new char[size];
stream.Serialize(data);

DataStream stream2;
stream2.Deserialize(data, size);

char c2;
Test t2;
stream2.readVal(c2);
stream2.readVal(&t2);
std::cout << c2 << t2.m_A << t2.m_B;
return 0;

}



相關文章