三十四.区块链网络(5)--消息处理

1.ThreadMessageHandler2

第三个线程调用结构逻辑,就不说明了,跟前两个一样。我们直接看核心代码,Thread..r2函数的代码:

cpp 复制代码
void ThreadMessageHandler(void* parg)
{
    IMPLEMENT_RANDOMIZE_STACK(ThreadMessageHandler(parg));

    loop
    {
        vfThreadRunning[2] = true;
        CheckForShutdown(2);
        try
        {
            ThreadMessageHandler2(parg);
        }
        CATCH_PRINT_EXCEPTION("ThreadMessageHandler()")
        vfThreadRunning[2] = false;
        Sleep(5000);
    }
}

void ThreadMessageHandler2(void* parg)
{
    printf("ThreadMessageHandler started\n");
    SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);
    loop
    {
        // Poll the connected nodes for messages
        vector<CNode*> vNodesCopy;
        CRITICAL_BLOCK(cs_vNodes)
            vNodesCopy = vNodes;
        foreach(CNode* pnode, vNodesCopy)
        {
            pnode->AddRef();

            // Receive messages
            TRY_CRITICAL_BLOCK(pnode->cs_vRecv)
                ProcessMessages(pnode);

            // Send messages
            TRY_CRITICAL_BLOCK(pnode->cs_vSend)
                SendMessages(pnode);

            pnode->Release();
        }

        // Wait and allow messages to bunch up
        vfThreadRunning[2] = false;
        Sleep(100);
        vfThreadRunning[2] = true;
        CheckForShutdown(2);
    }
}

在这个函数可以看到,他也在收发消息,但是收发消息?我们在之前的ThreadSocketHandler2函数也看到收发,这两个有什么区别吗?其实看命名就可以知道,一个是socket,一个是message。

其实前者是网络层,后者是协议层。

真正网络发送的是ThreadSocketHandler2,这是底层收发,纯字节流。负责这部分工作。

ThhreadMessageHandler2这个函数,只负责解析收来的消息,包括发送也是(打包消息)。

我们看到这个函数的代码很少,其实大部分工作都是ProcessMessages,SendMessages这两个函数再做,我们稍后来重点说一下这两个函数。

我们先看代码是怎么开始的:

cpp 复制代码
 IMPLEMENT_RANDOMIZE_STACK(ThreadMessageHandler(parg));

先将线程优先级设置低一级,能在有大量工作任务时,CPU资源又比较紧时,让别的线程优先工作。

然后拷贝一份vNodes用来遍历。

然后给这个节点添加引用标记,表示这个节点正在被使用,因为是多线程的,所以怕被别的线程影响数据。所以标记一下这节点。执行完,会调用pnode->Release();去掉标记(一次)

然后是对这两个函数调用:

cpp 复制代码
       // Receive messages
       TRY_CRITICAL_BLOCK(pnode->cs_vRecv)
           ProcessMessages(pnode);

       // Send messages
       TRY_CRITICAL_BLOCK(pnode->cs_vSend)
           SendMessages(pnode);

其它没有了,其实就是一直在循环执行这两个函数。

2.ProcessMessages

我们先来看消息处理函数:

cpp 复制代码
/ 处理来自节点 pfrom 的所有接收到的消息
bool ProcessMessages(CNode* pfrom)
{
    // 引用节点接收缓冲区(CDataStream 是一个可动态扩展的字节流)
    CDataStream& vRecv = pfrom->vRecv;
    
    // 如果缓冲区为空,直接返回(没有数据可处理)
    if (vRecv.empty())
        return true;
    
    // 打印当前待处理的数据长度(调试用)
    printf("ProcessMessages(%d bytes)\n", vRecv.size());

    //
    // Message format(消息格式说明)
    //  (4) message start     → 魔数 pchMessageStart
    //  (12) command          → 命令字符串(如 "version"、"inv" 等)
    //  (4) size              → 消息体长度
    //  (x) data              → 实际消息内容
    //

    // 进入循环,持续处理缓冲区中的消息(可能一次收到多个消息)
    loop
    {
        // ==================== 1. 查找消息起始标志(魔数) ====================
        
        // 在接收缓冲区中搜索魔数 pchMessageStart 的位置
        CDataStream::iterator pstart = search(vRecv.begin(), vRecv.end(), 
                                               BEGIN(pchMessageStart), END(pchMessageStart));
        
        // 如果剩余数据不足一个完整消息头的大小
        if (vRecv.end() - pstart < sizeof(CMessageHeader))
        {
            // 如果缓冲区数据已经超过一个消息头长度,但还是找不到魔数
            // 说明前面都是无效垃圾数据,清除多余部分,只保留最后不足一个头的数据
            if (vRecv.size() > sizeof(CMessageHeader))
            {
                printf("\n\nPROCESSMESSAGE MESSAGESTART NOT FOUND\n\n");
                vRecv.erase(vRecv.begin(), vRecv.end() - sizeof(CMessageHeader));
            }
            break;   // 退出循环,等待更多数据到来
        }
        
        // 如果魔数不在缓冲区最开头,说明前面有无效/错位的数据,打印跳过的字节数
        if (pstart - vRecv.begin() > 0)
            printf("\n\nPROCESSMESSAGE SKIPPED %d BYTES\n\n", pstart - vRecv.begin());
        
        // 删除魔数前面的所有无效数据,使缓冲区从魔数开始
        vRecv.erase(vRecv.begin(), pstart);

        // ==================== 2. 读取并解析消息头 ====================
        
        // 构造消息头对象,并从缓冲区反序列化读取头部数据
        CMessageHeader hdr;
        vRecv >> hdr;
        
        // 检查消息头是否合法(魔数是否正确、命令是否合理、长度是否合法等)
        if (!hdr.IsValid())
        {
            printf("\n\nPROCESSMESSAGE: ERRORS IN HEADER %s\n\n\n", hdr.GetCommand().c_str());
            continue;   // 无效头,跳过,继续找下一个魔数
        }
        
        // 获取命令名称(字符串形式)
        string strCommand = hdr.GetCommand();

        // ==================== 3. 处理消息体长度 ====================
        
        // 获取消息体声明的长度
        unsigned int nMessageSize = hdr.nMessageSize;
        
        // 如果当前缓冲区剩余数据不足以包含整个消息体
        if (nMessageSize > vRecv.size())
        {
            // 把刚才读出的消息头重新插回缓冲区开头,等待后续数据到达
            //(这是一个"半包"情况,需要继续接收)
            printf("MESSAGE-BREAK 2\n");
            vRecv.insert(vRecv.begin(), BEGIN(hdr), END(hdr));
            Sleep(100);     // 短暂休眠,等待更多数据
            break;
        }

        // ==================== 4. 提取消息体并处理 ====================
        
        // 将完整的一条消息体拷贝到独立的缓冲区 vMsg 中
        CDataStream vMsg(vRecv.begin(), vRecv.begin() + nMessageSize, 
                         vRecv.nType, vRecv.nVersion);
        
        // 从接收缓冲区中移除已处理的消息体(注意:不包含头部,头部已在前面 >> hdr 时移除)
        vRecv.ignore(nMessageSize);

        // ==================== 5. 实际处理这条消息 ====================
        
        bool fRet = false;
        try
        {
            CheckForShutdown(2);   // 检查是否需要关闭节点
            
            // 加锁处理(cs_main 是 Bitcoin 核心的重要全局锁)
            CRITICAL_BLOCK(cs_main)
                fRet = ProcessMessage(pfrom, strCommand, vMsg);  // 真正处理具体消息逻辑
            
            CheckForShutdown(2);
        }
        CATCH_PRINT_EXCEPTION("ProcessMessage()")   // 捕获并打印处理过程中的异常
        
        // 如果消息处理返回失败,打印日志
        if (!fRet)
            printf("ProcessMessage(%s, %d bytes) from %s to %s FAILED\n", 
                   strCommand.c_str(), nMessageSize, 
                   pfrom->addr.ToString().c_str(), addrLocalHost.ToString().c_str());
    }

    // 压缩缓冲区,释放已处理数据的内存(CDataStream 内部优化)
    vRecv.Compact();
    
    return true;
}
3.pchMessageStart

我们知道,收到的网络数据就在节点的, pfrom->vRecv里面,是CDataStream字节流的。

我要需要是把它还原成对应的数据类型。

但从哪里开始的呢?还记得前面的消息头固定标志吗---pchMessageStart。

这是一个固定的4字节的值:

cpp 复制代码
static const char pchMessageStart[4] = { 0xf9, 0xbe, 0xb4, 0xd9 };

所以我们只要找到这值,就能定位到一条消息的开头,然后再读取就可以了。

所以处理消息以下的代码开头,不停的循环的找这个标志,然后获取它的位置pstart。

cpp 复制代码
    loop
    {
        // Scan for message start
        CDataStream::iterator pstart = search(vRecv.begin(), vRecv.end(), BEGIN(pchMessageStart), END(pchMessageStart));

然后,这里我们不仅处理消息,还需要维护一下数据缓冲区,就是这段消息,如果这个标志头,不在这段消息的起始处,那是不是前面的数据我们要把它去掉,我们是用不上的。

以及这段数据找不到这个消息标志,则会进入下面的处理逻辑(即pstart==vRecv.end):

cpp 复制代码
      if (vRecv.end() - pstart < sizeof(CMessageHeader))
      {
          if (vRecv.size() > sizeof(CMessageHeader))
          {
              printf("\n\nPROCESSMESSAGE MESSAGESTART NOT FOUND\n\n");
              vRecv.erase(vRecv.begin(), vRecv.end() - sizeof(CMessageHeader));
          }
          break;
      }

就会删掉数据,但末尾保留一个消息头的数据,这是什么原因呢?这就是因为,怕这段数据的最后一部分只收到了部分消息头的数据,所以还需要后续收到的数据才能凑成一个完整的消息头。

如果有这种情况,虽然这段数据没有消息头,你全删掉了,这条消息就无法同步了。

所以为稳妥,保留一个消息头。然后break跳出循环。

然后是,pstart不在数据的起始点,说明前面有无效的数据,直接删除前面的数据:

cpp 复制代码
     if (pstart - vRecv.begin() > 0)
         printf("\n\nPROCESSMESSAGE SKIPPED %d BYTES\n\n", pstart - vRecv.begin());
     vRecv.erase(vRecv.begin(), pstart);

好,过了这两个判断和处理后,说明获取到一个消息了。我们就先来把这条消息的消息头读取出来:

cpp 复制代码
        // Read header
        CMessageHeader hdr;
        vRecv >> hdr;
        if (!hdr.IsValid())
        {
            printf("\n\nPROCESSMESSAGE: ERRORS IN HEADER %s\n\n\n", hdr.GetCommand().c_str());
            continue;
        }

4.消息格式

从这里就可以看出来,消息的开始是消息头,这里我们之前的代码有展示过,就是这段:

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

三十章有说明过,消息就是这样构造的。那么消息标志又是什么时候添加进去的呢?

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

我们来看,创建消息头,pszCommand就是消息类型,传入CMessageHeader给构造函数。

cpp 复制代码
class CMessageHeader
{
public:
    enum { COMMAND_SIZE=12 };
    char pchMessageStart[sizeof(::pchMessageStart)];
    char pchCommand[COMMAND_SIZE];
    unsigned int nMessageSize;

    CMessageHeader()
    {
        memcpy(pchMessageStart, ::pchMessageStart, sizeof(pchMessageStart));
        memset(pchCommand, 0, sizeof(pchCommand));
        pchCommand[1] = 1;
        nMessageSize = -1;
    }

    CMessageHeader(const char* pszCommand, unsigned int nMessageSizeIn)
    {
        memcpy(pchMessageStart, ::pchMessageStart, sizeof(pchMessageStart));
        strncpy(pchCommand, pszCommand, COMMAND_SIZE);
        nMessageSize = nMessageSizeIn;
    }

构造函数里,创建了消息开头标志,pchMessageStart,即你创建消息头对象时,这个标志就自动加入了。

5.IsValid

读取到消息头后,会调用IsValid函数,对这个消息头的一些数据进行判断,比如消息命令是否对得上,消息数据不能超过最大数。

这是一个初步的判断,防止读取到了错误的消息头。可以防止数据随机正好产生了一个消息标志开头。虽然概率极小。

6.GetCommand

通过了后,调用GetCommand,就是获取消息类型了---"tx","addr"等。

然后是消息大小:nMessageSize

cpp 复制代码
        string strCommand = hdr.GetCommand();

        // Message size
        unsigned int nMessageSize = hdr.nMessageSize;
        if (nMessageSize > vRecv.size())
        {
            // Rewind and wait for rest of message
            ///// need a mechanism to give up waiting for overlong message size error
            printf("MESSAGE-BREAK 2\n");
            vRecv.insert(vRecv.begin(), BEGIN(hdr), END(hdr));
            Sleep(100);
            break;
        }

并且判断一下,如果指示的消息大小,比你现在收到的消息还大,说明消息还没接收完整(tcp分包多次发送).

即跳出当前循环,等待数据接收。

但这里我们发现没有,在跳出之前还有一个操作,即重新插入消息头到缓存区,这是因为,读取消息头时,这句:

cpp 复制代码
 CMessageHeader hdr;
 vRecv >> hdr;

这个操作,已经把消息头从缓冲区移出去了。所以需要重新插入消息头到vRecv的最前面。

7.读取消息

好,到了最后,就可以读取具体的消息数据了,如下:

cpp 复制代码
        // Copy message to its own buffer
        CDataStream vMsg(vRecv.begin(), vRecv.begin() + nMessageSize, vRecv.nType, vRecv.nVersion);
        vRecv.ignore(nMessageSize);

读取到vMsg里面,然后调用ignore删掉vRecv里对应的数据。

7.ProcessMessage

然后是:

cpp 复制代码
       // Process message
       bool fRet = false;
       try
       {
           CheckForShutdown(2);
           CRITICAL_BLOCK(cs_main)
               fRet = ProcessMessage(pfrom, strCommand, vMsg);
           CheckForShutdown(2);
       }
       CATCH_PRINT_EXCEPTION("ProcessMessage()")
       if (!fRet)
           printf("ProcessMessage(%s, %d bytes) from %s to %s FAILED\n", strCommand.c_str(), nMessageSize, pfrom->addr.ToString().c_str(), addrLocalHost.ToString().c_str());
   }

从数据流分解出一条有效的消息,得到strCommand和vMsg后,我们干什么,最终调用另一个函数来处理这条消息--ProcessMessage。

在这个函数里,先打印一下这条消息的类型,和数据大小:

cpp 复制代码
   printf("received: %-12s (%d bytes)  ", strCommand.c_str(), vRecv.size());

然后打印最多前25个字节的数据(依实际情况而定,如果不足25,则打印实际大小,不超过25个字节),以便用来观察调试:

cpp 复制代码
    for (int i = 0; i < min(vRecv.size(), (unsigned int)25); i++)
        printf("%02x ", vRecv[i] & 0xff);
    printf("\n");
  1. nDropMessagesTest

然后这个nDropMessagesTest的值是测试用的,当这个值大于0时,会根据nDropMessagesTest的值(概率)丢弃一段收到消息,模拟节点在比较差的网络环境中(丢包)的健壮性。能否正常运行和处理。

默认设置0,不测试,下面这段代码简单了解可。实际中不使用:

cpp 复制代码
 if (nDropMessagesTest > 0 && GetRand(nDropMessagesTest) == 0)
 {
     printf("dropmessages DROPPING RECV MESSAGE\n");
     return true;
 }

好,以上都是关于测试和调试的代码,接下就是真正的消息处理,我们会看到这样的开头:

cpp 复制代码
    if (strCommand == "version")
    {
        // Can only do this once
        if (pfrom->nVersion != 0)
            return false;

        int64 nTime;
        CAddress addrMe;

就是具体的消息类型处理代码,现在我们可以有选择的了解,因为写法都是一样的。只是每种消息有不同的处理代码,彼此关联性不强。可以单独拿出来看。

在之前,我们了解了如何发送"tx"消息,那么在这里我们就先来了解节点在收到"tx"消息是怎么处理的,相关代码如下:

cpp 复制代码
// 处理收到的 "tx"(交易)消息
else if (strCommand == "tx")
{
    // 用于存放需要递归处理的交易哈希队列(处理孤儿交易时使用)
    vector<uint256> vWorkQueue;
    
    // 保存一份消息体的副本(用于后续中继给其他节点)
    CDataStream vMsg(vRecv);
    
    // 声明一个交易对象
    CTransaction tx;
    
    // 从 vRecv 中反序列化读取交易数据(填充 tx 对象)
    vRecv >> tx;

    // 创建一个交易库存对象(Inventory),类型为 MSG_TX
    CInv inv(MSG_TX, tx.GetHash());
    
    // 标记这个节点已经知道这笔交易,避免重复请求
    pfrom->AddInventoryKnown(inv);

    bool fMissingInputs = false;
    
    // 尝试接受这笔交易(验证交易合法性)
    if (tx.AcceptTransaction(true, &fMissingInputs))
    {
        // 如果是发给自己的交易(钱包相关的),加入钱包
        AddToWalletIfMine(tx, NULL);
        
        // 将这笔交易中继(广播)给其他连接的节点
        RelayMessage(inv, vMsg);
        
        // 从"已请求列表"中删除,因为已经收到并处理了
        mapAlreadyAskedFor.erase(inv);
        
        // 把当前交易哈希加入工作队列(用于后续处理依赖它的孤儿交易)
        vWorkQueue.push_back(inv.hash);

        // ==================== 处理孤儿交易(递归) ====================
        // 递归处理所有依赖于这笔交易的孤儿交易
        for (int i = 0; i < vWorkQueue.size(); i++)
        {
            uint256 hashPrev = vWorkQueue[i];
            
            // 在孤儿交易索引表中查找所有前置交易是 hashPrev 的孤儿交易
            for (multimap<uint256, CDataStream*>::iterator mi = 
                 mapOrphanTransactionsByPrev.lower_bound(hashPrev);
                 mi != mapOrphanTransactionsByPrev.upper_bound(hashPrev);
                 ++mi)
            {
                // 获取孤儿交易的消息体
                const CDataStream& vMsg = *((*mi).second);
                
                CTransaction tx;
                // 从孤儿交易数据中反序列化出一个交易对象
                CDataStream(vMsg) >> tx;
                
                CInv inv(MSG_TX, tx.GetHash());

                // 再次尝试接受这笔孤儿交易
                if (tx.AcceptTransaction(true))
                {
                    printf("   accepted orphan tx %s\n", inv.hash.ToString().substr(0,6).c_str());
                    
                    AddToWalletIfMine(tx, NULL);     // 加入钱包(如果属于自己)
                    RelayMessage(inv, vMsg);         // 中继给其他节点
                    mapAlreadyAskedFor.erase(inv);   // 清理请求记录
                    
                    // 把这个刚接受的孤儿交易也加入工作队列,继续处理它依赖的交易
                    vWorkQueue.push_back(inv.hash);
                }
            }
        }

        // 处理完所有相关孤儿交易后,从孤儿交易池中删除它们
        foreach(uint256 hash, vWorkQueue)
            EraseOrphanTx(hash);
    }
    // 如果交易未被接受,但缺少输入(即依赖的交易还没到),则作为孤儿交易保存
    else if (fMissingInputs)
    {
        printf("storing orphan tx %s\n", inv.hash.ToString().substr(0,6).c_str());
        
        // 将这条交易存入孤儿交易池,等待它依赖的交易到来
        AddOrphanTx(vMsg);
    }
    // 其他情况(交易无效、双花等)则直接丢弃,不做处理
}

代码就不详细解释了,我们大概过一下流程,收到tx后,就会调用:

8.AcceptTransaction
cpp 复制代码
 if (tx.AcceptTransaction(true, &fMissingInputs))
    {

此函数代码:

cpp 复制代码
    bool AcceptTransaction(bool fCheckInputs=true, bool* pfMissingInputs=NULL)
    {
        CTxDB txdb("r");
        return AcceptTransaction(txdb, fCheckInputs, pfMissingInputs);
    }
9.AddToMemoryPool

AcceptTransaction这个函数我们之前有说过,在这里面会调用:

cpp 复制代码
bool CTransaction::AddToMemoryPool()

把这笔tx加入到内存交易池,然后供后续挖矿打包成区块(从内存池挑选交易).

就是这么个过程,另外此函数的注释也做了说明:

cpp 复制代码
bool CTransaction::AddToMemoryPool()
{
    // Add to memory pool without checking anything.  Don't call this directly,
    // call AcceptTransaction to properly check the transaction first.
    CRITICAL_BLOCK(cs_mapTransactions)
    {
        uint256 hash = GetHash();
        mapTransactions[hash] = *this;
        for (int i = 0; i < vin.size(); i++)
            mapNextTx[vin[i].prevout] = CInPoint(&mapTransactions[hash], i);
        nTransactionsUpdated++;
    }
    return true;
}

意思是,不要直接调用这个函数将tx添加到内存池,因为没有做任何验证,先调用AcceptTransaction进行验证。从这段话我们就能知道,AcceptTranasaction函数的作用,和这两个函数的调用关系。

另:关于ProcessMessage函数,tx消息下的孤儿交易,就是在当前节点,引用的交易还未找到,数据库,包括临时池都没有,它会有着相应的处理方法。注意这里不是孤儿块。但可以根据名字了解,即然是孤儿,那都是暂时缺失相应的父亲。需要等待处理。

10.SendMessages

在前面我们了解怎么处理接收到的消息,还需要处理我们要发送的消息,就是通过SendMessages。但是这个函数,只处理部分消息类型,可自行看代码了解:

cpp 复制代码
bool SendMessages(CNode* pto)
{
    // 检查程序是否正在关闭,如果正在关闭则会触发退出流程
    CheckForShutdown(2);

    // 锁住全局区块链和交易相关数据(cs_main)
    // 后面会访问地址、交易池、数据库等共享资源,需要同步
    CRITICAL_BLOCK(cs_main)
    {
        // ===============================
        // 握手检查
        // ===============================

        // 如果还没有收到对方的 version 消息
        // 说明握手尚未完成
        // 此时不要发送任何业务消息
        if (pto->nVersion == 0)
            return true;


        //
        // Message: addr
        //

        // 临时保存准备发送的地址列表
        vector<CAddress> vAddrToSend;

        // 提前分配空间,避免vector不断扩容
        vAddrToSend.reserve(pto->vAddrToSend.size());

        // 遍历等待发送的所有地址
        foreach(const CAddress& addr, pto->vAddrToSend)

            // 如果对方还不知道这个地址
            // 才加入发送列表
            if (!pto->setAddrKnown.count(addr))
                vAddrToSend.push_back(addr);

        // 无论是否发送成功
        // 本次等待发送列表全部清空
        pto->vAddrToSend.clear();

        // 如果确实有新的地址需要发送
        // 构造一个 addr 消息放入发送缓冲区
        if (!vAddrToSend.empty())
            pto->PushMessage("addr", vAddrToSend);


        //
        // Message: inventory
        //

        // 临时保存准备发送的 inv
        vector<CInv> vInventoryToSend;

        // inventory相关数据有自己的锁
        CRITICAL_BLOCK(pto->cs_inventory)
        {
            // 预分配空间
            vInventoryToSend.reserve(pto->vInventoryToSend.size());

            // 遍历所有等待广播的 inventory
            foreach(const CInv& inv, pto->vInventoryToSend)
            {
                // insert 返回 pair<iterator,bool>

                // second==true
                // 表示以前没有发送过

                // second==false
                // 表示已经告诉过对方

                // 因此这里只发送新的 inventory
                if (pto->setInventoryKnown.insert(inv).second)
                    vInventoryToSend.push_back(inv);
            }

            // 清空等待发送队列
            pto->vInventoryToSend.clear();

            // 清空辅助集合
            // 下次重新统计
            pto->setInventoryKnown2.clear();
        }

        // 如果确实有新的 inventory
        // 发送 inv 消息
        if (!vInventoryToSend.empty())
            pto->PushMessage("inv", vInventoryToSend);


        //
        // Message: getdata
        //

        // 保存本次准备请求的数据
        vector<CInv> vAskFor;

        // 当前时间(微秒)
        // mapAskFor 的 key 就是计划发送时间
        int64 nNow = GetTime() * 1000000;

        // 打开交易数据库(只读)
        // 用于判断数据是否已经拥有
        CTxDB txdb("r");

        // mapAskFor 是一个按时间排序的 map
        //
        // key:
        //     请求发送时间
        //
        // value:
        //     要请求的 inventory
        //
        // 这里只处理已经到时间的请求
        while (!pto->mapAskFor.empty() &&
               (*pto->mapAskFor.begin()).first <= nNow)
        {
            // 当前最早需要请求的 inventory
            const CInv& inv = (*pto->mapAskFor.begin()).second;

            printf("sending getdata: %s\n",
                   inv.ToString().c_str());

            // 如果本地还没有这个对象
            // 才真正发送 getdata 请求
            if (!AlreadyHave(txdb, inv))
                vAskFor.push_back(inv);

            // 不管是否请求
            // 都从等待队列中删除
            pto->mapAskFor.erase(pto->mapAskFor.begin());
        }

        // 如果确实需要请求数据
        // 发送 getdata 消息
        if (!vAskFor.empty())
            pto->PushMessage("getdata", vAskFor);

    }

    // 正常结束
    return true;
}

可以看到,只处理了三种消息类型,有些消息,比如"tx",是不经过SendMessages处理的,而是别处代码直接调用PushMessage来发送的。

这是因为有些消息,发送的条件不一样。SendMessages适合延时,批量发送的消息。

关于消息类型的理解,比如"getdata",现在我们都知道收发处的代码,我们如果要了解,现在很方便,比如去ProccessMessage找,关于下面这部分的代码:

cpp 复制代码
   else if (strCommand == "getdata")

我们就能知道其含义和作用。这里就不过多的解释了。