比特幣原始碼學習筆記(二)
阿新 • • 發佈:2018-12-27
第二章
本章繼上一章交易建立之後介紹比特幣客戶端序列化資料的過程。
比特幣客戶端所有的序列化函式均在seriliaze.h中實現。其中,CDataStream類是資料序列化的核心結構。
CDataStream
CDataStream擁有一個字元類容器用來存放序列化之後的資料。它結合一個容器型別和一個流(stream)介面以處理資料。它使用6個成員函式實現這一功能:
class CDataStream { protected: typedef vector<char, secure_allocator<char> > vector_type; vector_type vch; unsigned int nReadPos; short state; short exceptmask; public: int nType; int nVersion; //...... }
- vch存有序列化後的資料。它是一個擁有自定義記憶體分配器的字元容器型別。該記憶體分配器將由該容器的實現在需要分配/釋放記憶體時呼叫。該記憶體分配器會在向作業系統釋放記憶體前清空記憶體中的資料以防止本機的其他程序訪問此資料,從而保證資料儲存的安全性。該記憶體分配器的實現在此不進行討論,讀者可於serialize.h自行查詢。
- nReadPos是vch讀取資料的起始位置。
- state是錯誤標識。該變數用於指示在序列化/反序列化當中可能出現的錯誤。
- exceptmask是錯誤掩碼。它初始化為ios::badbit | ios::failbit。與state類似,它被用於指示錯誤種類。
- nType的取值為SER_NETWORK,SER_DISK,SER_GETHASH,SER_SKIPSIG,SER_BLOCKHEADERONLY之一,其作用為通知CDataStream進行具體某種序列化操作。這5個符號被定義在一個列舉型別enum裡。每個符號均為一個int型別(4位元組),並且其值為2的次方。
enum
{
// primary actions
SER_NETWORK = (1 << 0),
SER_DISK = (1 << 1),
SER_GETHASH = (1 << 2),
// modifiers
SER_SKIPSIG = (1 << 16),
SER_BLOCKHEADERONLY = (1 << 17),
};
- nVersion是版本號。
CDataStream::read()與CDataStream::write()
CDataStream& read(char* pch, int nSize)
{
// Read from the beginning of the buffer
assert(nSize >= 0);
unsigned int nReadPosNext = nReadPos + nSize;
if (nReadPosNext >= vch.size())
{
if (nReadPosNext > vch.size())
{
setstate(ios::failbit, "CDataStream::read() : end of data");
memset(pch, 0, nSize);
nSize = vch.size() - nReadPos;
}
memcpy(pch, &vch[nReadPos], nSize);
nReadPos = 0;
vch.clear();
return (*this);
}
memcpy(pch, &vch[nReadPos], nSize);
nReadPos = nReadPosNext;
return (*this);
}
CDataStream& write(const char* pch, int nSize)
{
// Write to the end of the buffer
assert(nSize >= 0);
vch.insert(vch.end(), pch, pch + nSize);
return (*this);
}
CDataStream::read()從CDataStream複製nSize個字元到一個由char* pch所指向的記憶體空間。以下是它的實現過程:
- 計算將要從vch讀取的資料的結束位置,unsigned int nReadPosNext = nReadPos + nSize。
- 如果結束位置比vch的大小更大,則當前沒有足夠的資料供讀取。在這種情況下,通過呼叫函式setState()將state設為ios::failbit,並將所有的零複製到pch。
- 否則,呼叫memcpy(pch, &vch[nReadPos], nSize)複製nSize個字元,從vch的nReadPos位置開始,到由pch指向的一段預先分配的記憶體。接著從nReadPos向前移至下一個起始位置nReadPosNext(第22行)。
巨集READDATA()和WRITEDATA()
函式CDataStream::read()與CDataStream::write()的作用是序列化/反序列化原始型別(int,bool,unsigned long等)。為了序列化這些資料型別,這些型別的指標將被轉換為char*。由於這些型別的大小目前已知,它們可以從CDataStream中讀取或者寫入至字元緩衝。兩個用於引用這些函式的巨集被定義為助手。#define WRITEDATA(s, obj) s.write((char*)&(obj), sizeof(obj))
#define READDATA(s, obj) s.read((char*)&(obj), sizeof(obj))
這裡是如何使用這些巨集的例子。下面的函式將序列化一個unsigned long型別。
template<typename Stream> inline void Serialize(Stream& s, unsigned long a, int, int=0) { WRITEDATA(s, a); }把WRITEDATA(s, a)用自身的定義取代,以下是展開以後的函式:
template<typename Stream> inline void Serialize(Stream& s, unsigned long a, int, int=0) { s.write((char*)&(a), sizeof(a)); }
該函式接受一個unsigned long引數a,獲取它的記憶體地址,轉換指標為char*並呼叫函式s.write()。
CDataStream中的操作符 << 和 >>
CDataStream過載了操作符<< 和 >>用於序列化和反序列化。 template<typename T>
CDataStream& operator<<(const T& obj)
{
// Serialize to this stream
::Serialize(*this, obj, nType, nVersion);
return (*this);
}
template<typename T>
CDataStream& operator>>(T& obj)
{
// Unserialize from this stream
::Unserialize(*this, obj, nType, nVersion);
return (*this);
}
標頭檔案serialize.h包含了14個過載後的這兩個全域性函式給14個原始型別(signed和unsigned版本char,short,int,long和long long,以及char,float,double和bool)以及6個過載版本的6個複合型別(string,vector,pair,map,set和CScript)。因此,對於這些型別,你可以簡單地使用以下程式碼來序列化/反序列化資料:
CDataStream ss(SER_GETHASH);
ss<<obj1<<obj2; //序列化
ss>>obj3>>obj4; //反序列化
如果沒有任何實現的型別符合第二個引數obj,則以下泛型T全域性函式將會被呼叫。
template<typename Stream, typename T>
inline void Serialize(Stream& os, const T& a, long nType, int nVersion=VERSION)
{
a.Serialize(os, (int)nType, nVersion);
}
對於該泛型版本,型別T應該用於實現一個成員函式和簽名T::Serialize(Stream, int, int)。它將通過a.Serialize()被呼叫。
怎樣實現一個型別的序列化
在之前的介紹當中,泛型T需要實現以下三個成員函式進行序列化。 unsigned int GetSerializeSize(int nType=0, int nVersion=VERSION) const;
void Serialize(Stream& s, int nType=0, int nVersion=VERSION) const;
void Unserialize(Stream& s, int nType=0, int nVersion=VERSION);
這三個函式將由它們相對應的帶泛型T的全域性函式呼叫。這些全域性函式則由CDataStream中過載的操作符<<和>>呼叫。 一個巨集IMPLEMENT_SERIALIZE(statements)用於定義任意型別的這三個函式的實現。
#define IMPLEMENT_SERIALIZE(statements) \
unsigned int GetSerializeSize(int nType=0, int nVersion=VERSION) const \
{ \
CSerActionGetSerializeSize ser_action; \
const bool fGetSize = true; \
const bool fWrite = false; \
const bool fRead = false; \
unsigned int nSerSize = 0; \
ser_streamplaceholder s; \
s.nType = nType; \
s.nVersion = nVersion; \
{statements} \
return nSerSize; \
} \
template<typename Stream> \
void Serialize(Stream& s, int nType=0, int nVersion=VERSION) const \
{ \
CSerActionSerialize ser_action; \
const bool fGetSize = false; \
const bool fWrite = true; \
const bool fRead = false; \
unsigned int nSerSize = 0; \
{statements} \
} \
template<typename Stream> \
void Unserialize(Stream& s, int nType=0, int nVersion=VERSION) \
{ \
CSerActionUnserialize ser_action; \
const bool fGetSize = false; \
const bool fWrite = false; \
const bool fRead = true; \
unsigned int nSerSize = 0; \
{statements} \
}
以下例子示範怎樣使用該巨集。
#include <iostream>
#include "serialize.h"
using namespace std;
class AClass {
public:
AClass(int xin) : x(xin){};
int x;
IMPLEMENT_SERIALIZE(READWRITE(this->x);)
}
int main() {
CDataStream astream2;
AClass aObj(200); //一個x為200的AClass型別物件
cout<<"aObj="<<aObj.x>>endl;
asream2<<aObj;
AClass a2(1); //另一個x為1的物件
astream2>>a2
cout<<"a2="<<a2.x<<endl;
return 0;
}
這段程式序列化/反序列化AClass物件。它將在螢幕上輸出下面的結果。
aObj=200
a2=200
AClass的這三個序列化/反序列化成員函式可以在一行程式碼中實現: IMPLEMENT_SERIALIZE(READWRITE(this->x);) 巨集READWRITE()的定義如下
#define READWRITE(obj) (nSerSize += ::SerReadWrite(s, (obj), nType, nVersion, ser_action))
該巨集的展開被放在巨集IMPLEMENT_SERIALIZE(statements)的全部三個函式裡。因此,它一次需要完成三件事情:1)返回序列化後資料的大小,2)序列化(寫入)資料至流;3)從流中反序列化(讀取)資料。參考巨集IMPLEMENT_SERIALIZE(statements)中對這三個函式的定義。 想要了解巨集READWRITE(obj)怎樣工作,你首先需要明白它的完整形式當中的nSerSize,s,nType,nVersion和ser_action是怎麼來的。它們全部來自巨集IMPLEMENT_SERIALIZE(statements)的三個函式主體部分:
- nSerSize是一個unsigned int,在三個函式當中初始化為0;
- ser_action是一個物件在三個函式當中均有宣告,但為三種不同型別。它在三個函式當中分別為CSerActionGetSerializeSize、CSerActionSerialize和CSerActionUnserialize;
- s在第一個函式中定義為ser_streamplaceholder型別。它是第一個傳入至另外兩個函式的引數,擁有引數型別Stream;
- nType和nVersion在三個函式中均為傳入引數。
template<typename Stream, typename T>
inline unsigned int SerReadWrite(Stream& s, const T& obj, int nType, int nVersion, CSerActionGetSerializeSize ser_action)
{
return ::GetSerializeSize(obj, nType, nVersion);
}
template<typename Stream, typename T>
inline unsigned int SerReadWrite(Stream& s, const T& obj, int nType, int nVersion, CSerActionSerialize ser_action)
{
::Serialize(s, obj, nType, nVersion);
return 0;
}
template<typename Stream, typename T>
inline unsigned int SerReadWrite(Stream& s, T& obj, int nType, int nVersion, CSerActionUnserialize ser_action)
{
::Unserialize(s, obj, nType, nVersion);
return 0;
}
如你所見,函式::SerReadWrite()被過載為三種版本。取決於最後一個引數,它將會調分別用全域性函式::GetSerialize(),::Serialize()和::Unserialize();這三個函式在前面章節已經介紹。 如果你檢查三種不同版本的::SerReadWrite()的最後一個引數,你會發現它們全部為空型別。這三種類型的唯一用途是區別::SerReadWrite()的三個版本,繼而被巨集IMPLEMENT_SERIALIZE()定義的所有函式使用。