三十.区块链网络(1)--网络发送tx

1.网络协议

比特币是采用的P2P网络,因为去中心化,节点对等,这个网络是非常适合比特币的。而这个P2P的实现,在比特币中是采用TCP协议的。

所以我们如果要入手,得了解C++的网络部分,如何构建TCP协议通信。这里调用的是windows API来实现,如果你之前了解,学习起来事半功倍,不了解也没头系,我们会在讲源码的时候,附带说明TCP部分。

我们可以从net.cpp里看到这句:

cpp 复制代码
#include <winsock2.h>

这就是windows 网络API所在头文件。说明是用windows的winsock实现网络通信的。

2.开始

我们从发送比特币交易这里入手,这里肯定是要调用网络广播的。我们循着它找到构建网络的源头。

从最顶层的函数开始:

cpp 复制代码
void CSendDialog::OnButtonSend(wxCommandEvent& event)

在里面调用了SendMoney函数:

cpp 复制代码
    if (!SendMoney(scriptPubKey, nValue, wtx))
        return;

第一个参数是收款人脚本(含有收款人账号),nValue是收款人金额,wtx是当前要构建的tx。

3.RelayWalletTransaction

在SendMoney里当wtx构建好了,就会调用如下函数进行广播:

cpp 复制代码
     wtxNew.RelayWalletTransaction();

然后会传入本地存储有交易索引的数据库对象,供后续相关查找使用:

cpp 复制代码
    void RelayWalletTransaction(CTxDB& txdb);
    void RelayWalletTransaction() { CTxDB txdb("r"); RelayWalletTransaction(txdb); }

看一下真正干活的这个重载广播函数代码:

cpp 复制代码
void CWalletTx::RelayWalletTransaction(CTxDB& txdb)
{
    foreach(const CMerkleTx& tx, vtxPrev)
    {
        if (!tx.IsCoinBase())
        {
            uint256 hash = tx.GetHash();
            if (!txdb.ContainsTx(hash))
                RelayMessage(CInv(MSG_TX, hash), (CTransaction)tx);
        }
    }
    if (!IsCoinBase())
    {
        uint256 hash = GetHash();
        if (!txdb.ContainsTx(hash))
        {
            printf("Relaying wtx %s\n", hash.ToString().substr(0,6).c_str());
            RelayMessage(CInv(MSG_TX, hash), (CTransaction)*this);
        }
    }
}

我们可以看到,新建一笔tx,应该只是广播一笔tx,但是在里面,它调用了好几次RelayMessage来广播tx。这是为什么呢?

是因为,在你广播这笔tx的时候,由于担心你引用的tx,比如可能就是在上一个区块,那么这时候可能别的节点有可能没接收到,没上链。所以它会一同广播,当然,它会有限制的,引用的tx如果离现在的区块不是很近,那么它不会去判断有没有上链,从而广播,而是默认都在链上。

这个设计,主要是为了解决因为网络延迟导致当前tx先于引用tx的问题。

在这个函数前部分代码,即:

cpp 复制代码
    foreach(const CMerkleTx& tx, vtxPrev)
    {
        if (!tx.IsCoinBase())
        {
            uint256 hash = tx.GetHash();
            if (!txdb.ContainsTx(hash))
                RelayMessage(CInv(MSG_TX, hash), (CTransaction)tx);
        }
    }

它就会遍历vtxPrev,这些都是引用的tx,然后判断数据库里有没有这个tx,如果没有,那说明可能没上链,所以它会再广播一次。(当然你的数据库有了,别人的数据库也不一定有,这里的设计只是一道保险,并且能省掉一些事,当然别人如果真的找不到引用的交易,也会有相关的处理方法)

4.AddSupportingTransactions

我们可以看到,是从vtxPrev查找引用的tx,那这个vtxPrev是从哪里构建的呢?

在如下函数构建:

cpp 复制代码
wtxNew.AddSupportingTransactions(txdb);

这个函数,在CreateTransaction创建交易里被调用,也即SendMoney里的CreateTransaction。

我们来看一下这个函数的代码:

cpp 复制代码
void CWalletTx::AddSupportingTransactions(CTxDB& txdb)
{
    vtxPrev.clear();

    const int COPY_DEPTH = 3;
    if (SetMerkleBranch() < COPY_DEPTH)
    {
        vector<uint256> vWorkQueue;
        foreach(const CTxIn& txin, vin)
            vWorkQueue.push_back(txin.prevout.hash);

        // This critsect is OK because txdb is already open
        CRITICAL_BLOCK(cs_mapWallet)
        {
            map<uint256, const CMerkleTx*> mapWalletPrev;
            set<uint256> setAlreadyDone;
            for (int i = 0; i < vWorkQueue.size(); i++)
            {
                uint256 hash = vWorkQueue[i];
                if (setAlreadyDone.count(hash))
                    continue;
                setAlreadyDone.insert(hash);

                CMerkleTx tx;
                if (mapWallet.count(hash))
                {
                    tx = mapWallet[hash];
                    foreach(const CMerkleTx& txWalletPrev, mapWallet[hash].vtxPrev)
                        mapWalletPrev[txWalletPrev.GetHash()] = &txWalletPrev;
                }
                else if (mapWalletPrev.count(hash))
                {
                    tx = *mapWalletPrev[hash];
                }
                else if (!fClient && txdb.ReadDiskTx(hash, tx))
                {
                    ;
                }
                else
                {
                    printf("ERROR: AddSupportingTransactions() : unsupported transaction\n");
                    continue;
                }

                int nDepth = tx.SetMerkleBranch();
                vtxPrev.push_back(tx);

                if (nDepth < COPY_DEPTH)
                    foreach(const CTxIn& txin, tx.vin)
                        vWorkQueue.push_back(txin.prevout.hash);
            }
        }
    }

    reverse(vtxPrev.begin(), vtxPrev.end());
}
5.vtxPrev挑选规则

我们可以看到关键代码:

cpp 复制代码
    const int COPY_DEPTH = 3;
    if (SetMerkleBranch() < COPY_DEPTH)

COPY_DEPTH=3,限制了引用的深度,3个深度,也就是说引用的tx超过了3个深度,它就不会把这个tx加入到vtxPrev里面,注意这个3个深度,并不是前面三个区块的意思,关于是什么,因为涉及到默克尔树,而我们现在重点是网络部分,所以这里的算法现在不分析了。

我们现在只需要大概知道其做什么就行了,在最后面可以看到添加代码:

cpp 复制代码
 int nDepth = tx.SetMerkleBranch();
                vtxPrev.push_back(tx);

这个函数的分析将会放在后续章节,写完默克尔树的时候奉上,因为之前的默克尔树只写了一部分,它不仅仅是对数据的摘要,只有这一个功能,生成默克尔树,还有相应的查找和验证功能。

好,当广播完引用的tx后,才是到我们真正要广播的tx了,即广播我们新建的tx:

cpp 复制代码
    if (!IsCoinBase())
    {
        uint256 hash = GetHash();
        if (!txdb.ContainsTx(hash))
        {
            printf("Relaying wtx %s\n", hash.ToString().substr(0,6).c_str());
            RelayMessage(CInv(MSG_TX, hash), (CTransaction)*this);
        }
    }

但是我们发现,即使是按照SendMoney逻辑,新建的tx,这里还是判断一下是否存在于数据库里再广播,这句:

cpp 复制代码
 if (!txdb.ContainsTx(hash))

这其实也是为了写入成功,避免重复广播。因为RelayWalletTransaction函数可能被多次调用(包括其它地方),所以会加入这个判断。

6.RelayMessage

接下来我们来看广播的核心代码,也是我们越来越接近网络的代码,已经带有消息标志头了--MSG_TX:

cpp 复制代码
RelayMessage(CInv(MSG_TX, hash), (CTransaction)*this);

我们来看一下这个函数的代码:

cpp 复制代码
template<typename T>
void RelayMessage(const CInv& inv, const T& a)
{
    CDataStream ss(SER_NETWORK);
    ss.reserve(10000);
    ss << a;
    RelayMessage(inv, ss);
}

这是一个模板函数,将tx序列化ss,然后调用重载:

cpp 复制代码
RelayMessage(inv, ss);

RelayMessage"重载"函数代码:

cpp 复制代码
template<>
inline void RelayMessage<>(const CInv& inv, const CDataStream& ss)
{
    CRITICAL_BLOCK(cs_mapRelay)
    {
        // Expire old relay messages
        while (!vRelayExpiration.empty() && vRelayExpiration.front().first < GetTime())
        {
            mapRelay.erase(vRelayExpiration.front().second);
            vRelayExpiration.pop_front();
        }

        // Save original serialized message so newer versions are preserved
        mapRelay[inv] = ss;
        vRelayExpiration.push_back(make_pair(GetTime() + 15 * 60, inv));
    }

    RelayInventory(inv);
}

注意到这个函数上的template<>吗,这其实是一个显式模板特化函数,为CDataStream类型单独写一个执行函数,否则会执行最上面的那个模板函数,毕竟const T& a当然也包括CDataStream类型。

这个函数干什么呢?他会把消息存起来,除了广播行为,但是他只缓存15分钟,我们可以看到代码设定:

cpp 复制代码
  mapRelay[inv] = ss;
        vRelayExpiration.push_back(make_pair(GetTime() + 15 * 60, inv));

其中GetTime()+15*60,就是预设的过期时间。

然后这句判断:

cpp 复制代码
 while (!vRelayExpiration.empty() && vRelayExpiration.front().first < GetTime())

当消息缓存里不为空并且,有过期的消息,当前时间已经大于预设的过期时间,那么就进行删除。

这里的front()返回的是最早添加的元素,所以它这里循环判断依次删除。这样里面剩下的都是没过期的元素了。

7. RelayInventory

删除完过期的tx后,接着会广播这条tx了:

cpp 复制代码
 RelayInventory(inv);

但是你发现没,它并没有广播具体的数据ss,只是广播了一个inv,里面标识了这是条tx消息,以及这条tx的哈希值。

其实这是在告诉别的节点,我这里有条这样的tx,你们要不要。然后别的节点确定后(确认没有),就会回复(通过getdata方式),接着才会发送具体tx数据过去。

这样设计,可以节省网络资源。

我们来看一下函数的具体代码:

cpp 复制代码
inline void RelayInventory(const CInv& inv)
{
    // Put on lists to offer to the other nodes
    CRITICAL_BLOCK(cs_vNodes)
        foreach(CNode* pnode, vNodes)
            pnode->PushInventory(inv);
}

这里的意思,给邻居节点都发送一下,vNodes保存了已知的连接上的邻居节点。

这里的关键在这个函数:

cpp 复制代码
 pnode->PushInventory(inv);

代码如下:

cpp 复制代码
   void PushInventory(const CInv& inv)
   {
       CRITICAL_BLOCK(cs_inventory)
           if (!setInventoryKnown.count(inv))
               vInventoryToSend.push_back(inv);
   }

先是setInventoryKnown判断这条消息是否已经发送过,如果没发送,则添加到消息队列vInventoryToSend中。我们可以看到代码执行流程到这里就结束了,并没有网络发送相关。

那这里只是添加消息队例操作。发送方面是在另一个函数中。

8.SendMessages

我们看到接下来的处理是 SendMessages函数中,我们来看这个函数里关于Inventory这一段代码:

cpp 复制代码
        //
        // Message: inventory
        //
        vector<CInv> vInventoryToSend;
        CRITICAL_BLOCK(pto->cs_inventory)
        {
            vInventoryToSend.reserve(pto->vInventoryToSend.size());
            foreach(const CInv& inv, pto->vInventoryToSend)
            {
                // returns true if wasn't already contained in the set
                if (pto->setInventoryKnown.insert(inv).second)
                    vInventoryToSend.push_back(inv);
            }
            pto->vInventoryToSend.clear();
            pto->setInventoryKnown2.clear();
        }
        if (!vInventoryToSend.empty())
            pto->PushMessage("inv", vInventoryToSend);

其中pto就是节点,而pto->vInventoryToSend就是之前节点的添加的消息队例,这段代码的作用是,将pto里的vInventoryToSend复制到局部定义的VInventoryToSend,并去重,我们可以看到这句:

cpp 复制代码
 if (pto->setInventoryKnown.insert(inv).second)
                    vInventoryToSend.push_back(inv);

将这个消息添加到setInventoryKnown里,如果已存在,则失败,就不会执行vInventoryToSend。这是去重式添加到vInventoryToSend。所以从这里可以看出,这什么之前这段代码:

cpp 复制代码
   void PushInventory(const CInv& inv)
   {
       CRITICAL_BLOCK(cs_inventory)
           if (!setInventoryKnown.count(inv))
               vInventoryToSend.push_back(inv);
   }

通过setInventoryKnown判断消息是否发送过,没发送,但只执行了一句代码,并没有把这个未发送的消息添加到setInventoryKnown,是因为这个逻辑在SendMessage函数里处理添加。

9.PushMessage

这样去重生成的vInventoryToSend后,才是要发送的消息(没发送过的),代码如下:

cpp 复制代码
  if (!vInventoryToSend.empty())
      pto->PushMessage("inv", vInventoryToSend);

我们来看一下这个函数的代码:

cpp 复制代码
    template<typename T1>
    void PushMessage(const char* pszCommand, const T1& a1)
    {
        try
        {
            BeginMessage(pszCommand);
            vSend << a1;
            EndMessage();
        }
        catch (...)
        {
            AbortMessage();
            throw;
        }
    }

这里会封装消息,BeginMessage开始,这是封装消息头,pszCommand表示消息命令,也就是消息类型吧,这个我们知道,现在是"inv"。对应vInventoryToSend。

然后是vSend<<a1,中间是具体消息数据,序列化vInventoryTosend用于发送。vSend是CDataStream类型。

EndMessage表示结束收尾,写入消息长度之类的,就是这样一种格式,具体怎么实现,我们需要了解BeginMessage和EndMessage函数以及对vSend进行了什么操作。

这个函数的作用就是序列化打包消息,变成字节流。

10.BeginMessage

我们先来看BeginMessage这个函数:

cpp 复制代码
    void BeginMessage(const char* pszCommand)
    {
        EnterCriticalSection(&cs_vSend);
        if (nPushPos != -1)
            AbortMessage();
        nPushPos = vSend.size();
        vSend << CMessageHeader(pszCommand, 0);
        printf("sending: %-12s ", pszCommand);
    }

主要就是将信息头以一定的格式写入到vSend中。

11.EndMessage

再来看一下EndMessage这个函数:

cpp 复制代码
 void EndMessage()
 {
     extern int nDropMessagesTest;
     if (nDropMessagesTest > 0 && GetRand(nDropMessagesTest) == 0)
     {
         printf("dropmessages DROPPING SEND MESSAGE\n");
         AbortMessage();
         return;
     }

     if (nPushPos == -1)
         return;

     // Patch in the size
     unsigned int nSize = vSend.size() - nPushPos - sizeof(CMessageHeader);
     memcpy((char*)&vSend[nPushPos] + offsetof(CMessageHeader, nMessageSize), &nSize, sizeof(nSize));

     printf("(%d bytes)  ", nSize);
     //for (int i = nPushPos+sizeof(CMessageHeader); i < min(vSend.size(), nPushPos+sizeof(CMessageHeader)+20U); i++)
     //    printf("%02x ", vSend[i] & 0xff);
     printf("\n");

     nPushPos = -1;
     LeaveCriticalSection(&cs_vSend);
 }

这个函数就是计算消息的长度,然后将长度写入进去,注意这里写入的长度并不是在末尾,而是在消息头里,因为构建消息头时,还没有数据,所以无法计算总长度,所以放在EndMessage里计算,然后再补上。

这句就是实际写入:

cpp 复制代码
     memcpy((char*)&vSend[nPushPos] + offsetof(CMessageHeader, nMessageSize), &nSize, sizeof(nSize));

计算vSend中的偏移位置再写入。这里的nPushPos是消息头的起始位置。

现在大概理解即可,在解析数据的时候,再详细说明其结构。

我们看到,到现在,还没涉及到网络发头,只是在打包消息。而真正的发送数据的地方是在ThreadSocketHandler2函数中。

如下代码:

cpp 复制代码
      //
      // Service each socket
      //
      vector<CNode*> vNodesCopy;
      CRITICAL_BLOCK(cs_vNodes)
          vNodesCopy = vNodes;
      foreach(CNode* pnode, vNodesCopy)
      {
          CheckForShutdown(0);
          SOCKET hSocket = pnode->hSocket;

          //
          // Receive
          //
          if (FD_ISSET(hSocket, &fdsetRecv))
          {
              TRY_CRITICAL_BLOCK(pnode->cs_vRecv)
              {
                  CDataStream& vRecv = pnode->vRecv;
                  unsigned int nPos = vRecv.size();

                  // typical socket buffer is 8K-64K
                  const unsigned int nBufSize = 0x10000;
                  vRecv.resize(nPos + nBufSize);
                  int nBytes = recv(hSocket, &vRecv[nPos], nBufSize, 0);
                  vRecv.resize(nPos + max(nBytes, 0));
                  if (nBytes == 0)
                  {
                      // socket closed gracefully
                      if (!pnode->fDisconnect)
                          printf("recv: socket closed\n");
                      pnode->fDisconnect = true;
                  }
                  else if (nBytes < 0)
                  {
                      // socket error
                      int nErr = WSAGetLastError();
                      if (nErr != WSAEWOULDBLOCK && nErr != WSAEMSGSIZE && nErr != WSAEINTR && nErr != WSAEINPROGRESS)
                      {
                          if (!pnode->fDisconnect)
                              printf("recv failed: %d\n", nErr);
                          pnode->fDisconnect = true;
                      }
                  }
              }
          }

          //
          // Send
          //
          if (FD_ISSET(hSocket, &fdsetSend))
          {
              TRY_CRITICAL_BLOCK(pnode->cs_vSend)
              {
                  CDataStream& vSend = pnode->vSend;
                  if (!vSend.empty())
                  {
                      int nBytes = send(hSocket, &vSend[0], vSend.size(), 0);
                      if (nBytes > 0)
                      {
                          vSend.erase(vSend.begin(), vSend.begin() + nBytes);
                      }
                      else if (nBytes == 0)
                      {
                          if (pnode->ReadyToDisconnect())
                              pnode->vSend.clear();
                      }
                      else
                      {
                          printf("send error %d\n", nBytes);
                          if (pnode->ReadyToDisconnect())
                              pnode->vSend.clear();
                      }
                  }
              }
          }
      }

//send 部分里,上面是接收部分。

我们在这里可以看到遍历节点的行为,然后一个个检查,维护,发送,具体发送代码是这句:

cpp 复制代码
    int nBytes = send(hSocket, &vSend[0], vSend.size(), 0);

好,在这里已经看到hSocket这种名称,就是API套接字相关部分,而这个send,正是套接字相关API函数,用来网络发送数据的。

关于这些相关函数,以及整个流程,还有ThreadSocketHandler2这个线程函数,何时被调用。

还有net里关键的CNode节点类,怎么启动,获取,维护,以及他的功能将在后续章节中介绍。

然后会总结整个网络流程。

今天就到这里。谢谢。