比特币源码学习笔记(二)

g2com 发布在 技术指南 2 8282

第二章

本章继上一章交易创建之后介绍比特币客户端序列化数据的过程。

比特币客户端所有的序列化函数均在seriliaze.h中实现。其中,CDataStream类是数据序列化的核心结构。

bitcoin-code

 

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()和CDataStream::write()是用于执行序列化/反序列化CDataStream对象的低级函数。
 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行)。
该实现表明1)当一段数据被从流中读取之后,该段数据无法被再次读取;2)nReadPos是第一个有效数据的读取位置。
CDataStream::write()非常简单。它将由pch指向的nSize个字符附加到vch的结尾。

宏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在三个函数中均为传入参数。
因此,一旦宏READWRITE()扩展至宏IMPLEMENT_SERIALIZE(),所有它的符号都将被计算,因为它们已经存在于宏IMPLEMENT_SERIALIZE()的主体中。READWRITE(obj)的扩展调用一个全局函数::SerReadWrite(s, (obj), nType, nVersion, ser_action)。这里是这个函数的全部三种版本。
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()定义的所有函数使用。
发文时比特币价格 ¥7173
作者:g2com 版权声明: by nc" sa 作者保留权利。文章为作者独立观点,不代表巴比特立场。

评论:2

您需要登录后才可以回复 登录|注册
    Author Image
    美男子 208 天前

    https://github.com/bitcoin/bitcoin/commit/b019ea17ec7cc37d098982b4f0f4636e424ab4b8
    之前在VC6SP6 seriliaze.h#CDatastream 替换 std::stringstream,
    现在stringstream 比CDatastream 性能好太多了

    三十年河东 三十年河西

    +1
    +1
    我要点评

    vch存有序列化后的数据。它是一个拥有自定义内存分配器的字符容器类型。该内存分配器将由该容器的实现在需要分配/释放内存时调用。该内存分配器会在向操作系统释放内存前清空内存中的数据以防止本机的其他进程访问此数据,从而保证数据存储的安全性。该内存分配器的实现在此不进行讨论,读者可于serialize.h自行查找。

    +1
    +1
    我要点评