三十三.区块链网络(4)--节点维护和数据收发

接下来我们来说明一下StartNode中的三个线程函数。这三个函数发射成功后,就会一直执行的。

1.ThreadSocketHandler

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

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

    loop
    {
        vfThreadRunning[0] = true;
        CheckForShutdown(0);
        try
        {
            ThreadSocketHandler2(parg);
        }
        CATCH_PRINT_EXCEPTION("ThreadSocketHandler()")
        vfThreadRunning[0] = false;
        Sleep(5000);
    }
}
2.IMPLEMENT_RANDOMIZE_STACK

我们先来看一下这个宏定义是干什么,这一种防攻击的手段,在于将函数的栈地址变得随机,不可预测,增加攻击难度。当然后来的版本不采用了,现代系统主要靠 ASLR(地址空间布局随机化)。

我们来看一下这个宏定义的实现原理,可以根据兴趣研究一下代码。不感兴趣跳过即可。

cpp 复制代码
// Randomize the stack to help protect against buffer overrun exploits
#define IMPLEMENT_RANDOMIZE_STACK(ThreadFn)                         \
    {                                                               \
        static char nLoops;                                         \
        if (nLoops <= 0)                                            \
            nLoops = GetRand(50) + 1;                               \
        if (nLoops-- > 1)                                           \
        {                                                           \
            ThreadFn;                                               \
            return;                                                 \
        }                                                           \
    }

就是随机次数递归调用自身的函数,这样直到递归结束后,才是真正调用该函数。每一次调用会分配地址,那么最终的函数里面的局部变量等地址相对就不是固定的。可以一定程度的防止攻击,类似于单机游戏作弊那种。

但是这里会有疑问了,假如递归调用了十几次,那么递归结束后,那函数不也还是被调用了十几次吗?其实不会,注意到最后的那个return吗?在递归中,会直接return,这是个宏定义,所以这个return是在ThreadSocketHandler里面。所以return后面的代码,即真正的代码只会被执行一次。

3.ThreadSocketHandler2

真正执行代码后,我们会看到循环调用ThreadSocketHandler2函数,每sleep 5000;

这就是干活的函数,这个函数其实之前我们见过,在发送tx那一章中,我们发现tx一直没发送,只是在打包构建,最终写进了vSend里面。然后我们最终找到是在ThreadSocketHandler2函数里面去发送。

那么现在对上了,这个线程函数是StartNode里启动的,而StartNode是在程序启动就会被调用,现在整个过程我们都知道了,完美闭环。关于程序怎么启动main函数(涉及到UI框架封装),怎么调用StartNode我们后面再讲。

现在我们说一下ThreadSocketHandler2函数,这是个处理节点收发消息的函数,不管是你做为服务端,客户端连接你后收发消息,还是你做为客户端连接对面节点的收发消息,都统一在这个函数里处理。只是在连接的时候是分开的也必须分开(不同的socket),目前我们只看了服务端连接代码,把获得的socket放进vNodes里。客户端的连接成功后也是把socket放在vNodes里,所以是可以统一处理的,只要遍历vNodes就行。

来看一下这个函数的完整代码:

cpp 复制代码
void ThreadSocketHandler2(void* parg)
{
    printf("ThreadSocketHandler started\n");   // 线程启动提示

    // 从参数中取出监听socket(上层通过 new SOCKET() 传递而来)
    SOCKET hListenSocket = *(SOCKET*)parg;

    list<CNode*> vNodesDisconnected;   // 存放已断开但还未彻底释放的节点
    int nPrevNodeCount = 0;            // 记录上一次的节点数量,用于检测变化

    loop   // 无限主循环
    {
        // =============================================
        // 第一部分:断开连接与节点清理
        // =============================================
        CRITICAL_BLOCK(cs_vNodes)          // 加锁保护全局节点列表 vNodes
        {
            // ---------- 处理重复IP连接 ----------
            map<unsigned int, CNode*> mapFirst;   // 用于记录每个IP第一个出现的节点

            foreach(CNode* pnode, vNodes)         // 遍历所有节点
            {
                if (pnode->fDisconnect)           // 已标记断开的节点跳过
                    continue;

                unsigned int ip = pnode->addr.ip; // 获取节点IP

                // 如果同一个IP已存在连接,且当前节点IP更大,则断开较小的那个
                if (mapFirst.count(ip) && addrLocalHost.ip < ip)
                {
                    CNode* pnodeExtra = mapFirst[ip];

                    if (pnodeExtra->GetRefCount() > (pnodeExtra->fNetworkNode ? 1 : 0))
                        swap(pnodeExtra, pnode);

                    if (pnodeExtra->GetRefCount() <= (pnodeExtra->fNetworkNode ? 1 : 0))
                    {
                        printf("(%d nodes) disconnecting duplicate: %s\n", 
                               vNodes.size(), pnodeExtra->addr.ToString().c_str());

                        if (pnodeExtra->fNetworkNode && !pnode->fNetworkNode)
                        {
                            pnode->AddRef();
                            swap(pnodeExtra->fNetworkNode, pnode->fNetworkNode);
                            pnodeExtra->Release();
                        }
                        pnodeExtra->fDisconnect = true;   // 标记为需要断开
                    }
                }
                mapFirst[ip] = pnode;   // 更新map
            }

            // ---------- 清理不再使用的节点 ----------
            vector<CNode*> vNodesCopy = vNodes;   // 复制一份,避免边遍历边修改

            foreach(CNode* pnode, vNodesCopy)
            {
                // 如果节点可以断开,且收发缓冲区都为空
                if (pnode->ReadyToDisconnect() && pnode->vRecv.empty() && pnode->vSend.empty())
                {
                    // 从全局节点列表中删除
                    vNodes.erase(remove(vNodes.begin(), vNodes.end(), pnode), vNodes.end());
                    
                    pnode->Disconnect();                    // 关闭socket

                    // 设置延迟释放时间(给其他线程5分钟缓冲)
                    pnode->nReleaseTime = max(pnode->nReleaseTime, GetTime() + 5 * 60);
                    
                    if (pnode->fNetworkNode)
                        pnode->Release();                   // 减少引用计数

                    vNodesDisconnected.push_back(pnode);    // 放入待删除列表
                }
            }

            // ---------- 真正删除内存中的节点 ----------
            list<CNode*> vNodesDisconnectedCopy = vNodesDisconnected;

            foreach(CNode* pnode, vNodesDisconnectedCopy)
            {
                if (pnode->GetRefCount() <= 0)   // 没有线程再使用该节点
                {
                    bool fDelete = false;

                    // 尝试获取所有相关锁,确保安全删除
                    TRY_CRITICAL_BLOCK(pnode->cs_vSend)
                     TRY_CRITICAL_BLOCK(pnode->cs_vRecv)
                      TRY_CRITICAL_BLOCK(pnode->cs_mapRequests)
                       TRY_CRITICAL_BLOCK(pnode->cs_inventory)
                        fDelete = true;

                    if (fDelete)
                    {
                        vNodesDisconnected.remove(pnode);
                        delete pnode;   // 释放对象内存
                    }
                }
            }
        }   // cs_vNodes 锁在这里释放

        // 节点数量有变化时刷新界面
        if (vNodes.size() != nPrevNodeCount)
        {
            nPrevNodeCount = vNodes.size();
            MainFrameRepaint();
        }


        // =============================================
        // 第二部分:使用 select() 监控所有socket
        // =============================================
        struct timeval timeout;
        timeout.tv_sec  = 0;
        timeout.tv_usec = 50000;          // 设置50毫秒超时

        struct fd_set fdsetRecv;          // 接收事件集合
        struct fd_set fdsetSend;          // 发送事件集合

        FD_ZERO(&fdsetRecv);              // 清空接收集合
        FD_ZERO(&fdsetSend);              // 清空发送集合

        SOCKET hSocketMax = 0;            // 记录最大的socket编号

        // 把监听socket加入接收集合(用于检测新连接)
        FD_SET(hListenSocket, &fdsetRecv);
        hSocketMax = max(hSocketMax, hListenSocket);

        // 把所有已连接节点的socket加入监控
        CRITICAL_BLOCK(cs_vNodes)
        {
            foreach(CNode* pnode, vNodes)
            {
                FD_SET(pnode->hSocket, &fdsetRecv);     // 监控接收
                hSocketMax = max(hSocketMax, pnode->hSocket);

                // 只有待发送缓冲区有数据时,才监控发送事件
                TRY_CRITICAL_BLOCK(pnode->cs_vSend)
                    if (!pnode->vSend.empty())
                        FD_SET(pnode->hSocket, &fdsetSend);
            }
        }

        vfThreadRunning[0] = false;                     // 标记线程进入等待状态
        int nSelect = select(hSocketMax + 1, &fdsetRecv, &fdsetSend, NULL, &timeout);  // 调用select等待事件
        vfThreadRunning[0] = true;                      // 恢复运行状态

        CheckForShutdown(0);                            // 再次检查是否需要退出

        // select失败处理
        if (nSelect == SOCKET_ERROR)
        {
            int nErr = WSAGetLastError();
            printf("select failed: %d\n", nErr);
            
            // 错误恢复:把所有socket加入集合
            for (int i = 0; i <= hSocketMax; i++)
            {
                FD_SET(i, &fdsetRecv);
                FD_SET(i, &fdsetSend);
            }
            Sleep(timeout.tv_usec/1000);
        }

        RandAddSeed();   // 为随机数生成器补充随机熵


        // =============================================
        // 第三部分:接受新的传入连接
        // =============================================
        if (FD_ISSET(hListenSocket, &fdsetRecv))   // 如果监听socket有事件
        {
            struct sockaddr_in sockaddr;
            int len = sizeof(sockaddr);
            
            SOCKET hSocket = accept(hListenSocket, (struct sockaddr*)&sockaddr, &len);  // 接受新连接
            
            CAddress addr(sockaddr);   // 构造对方地址对象

            if (hSocket == INVALID_SOCKET)
            {
                if (WSAGetLastError() != WSAEWOULDBLOCK)
                    printf("ERROR ThreadSocketHandler accept failed: %d\n", WSAGetLastError());
            }
            else
            {
                printf("accepted connection from %s\n", addr.ToString().c_str());
                
                CNode* pnode = new CNode(hSocket, addr, true);  // true表示入站连接
                pnode->AddRef();                                // 增加引用计数
                
                CRITICAL_BLOCK(cs_vNodes)
                    vNodes.push_back(pnode);                    // 加入全局节点列表
            }
        }


        // =============================================
        // 第四部分:为每个节点处理收发数据
        // =============================================
        vector<CNode*> vNodesCopy;
        CRITICAL_BLOCK(cs_vNodes)
            vNodesCopy = vNodes;          // 复制列表,减少加锁时间

        foreach(CNode* pnode, vNodesCopy) // 遍历每个节点
        {
            CheckForShutdown(0);
            SOCKET hSocket = pnode->hSocket;

            // ---------- 接收数据 ----------
            if (FD_ISSET(hSocket, &fdsetRecv))   // 如果该socket有数据可读
            {
                TRY_CRITICAL_BLOCK(pnode->cs_vRecv)
                {
                    CDataStream& vRecv = pnode->vRecv;
                    unsigned int nPos = vRecv.size();

                    const unsigned int nBufSize = 0x10000;   // 64KB缓冲
                    vRecv.resize(nPos + nBufSize);
                    
                    int nBytes = recv(hSocket, &vRecv[nPos], nBufSize, 0);  // 实际接收数据
                    
                    vRecv.resize(nPos + max(nBytes, 0));   // 调整实际接收长度

                    if (nBytes == 0)
                    {
                        if (!pnode->fDisconnect)
                            printf("recv: socket closed\n");
                        pnode->fDisconnect = true;
                    }
                    else if (nBytes < 0)
                    {
                        int nErr = WSAGetLastError();
                        if (nErr != WSAEWOULDBLOCK && nErr != WSAEMSGSIZE && 
                            nErr != WSAEINTR && nErr != WSAEINPROGRESS)
                        {
                            if (!pnode->fDisconnect)
                                printf("recv failed: %d\n", nErr);
                            pnode->fDisconnect = true;
                        }
                    }
                }
            }

            // ---------- 发送数据 ----------
            if (FD_ISSET(hSocket, &fdsetSend))   // 如果该socket可以发送
            {
                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();
                        }
                    }
                }
            }
        }

        Sleep(10);   // 每轮循环结束后短暂休眠,防止CPU占用过高
    }
}

我们来看一些注释:

cpp 复制代码
 //
 // Disconnect nodes
 //
 {代码...}

 // Delete disconnected nodes
   {代码...}
  
 //
 // Find which sockets have data to receive
 //
 {代码...}


 //
 // Accept new connections
 //
   {代码...}
//
// Receive
//
   {代码...}
//
// Send
//
   {代码...}

根据注释我们可以大概知道,这个函数不仅处理收发消息,还负责维护节点,哪些节点失联了,清理可以断开的节点。

然后他会检查哪些socket有事情做了,就是需要收发消息。需要连接新的节点等。

增加新的节点(被动,通过accept)。

接着最后就是Receive和Send。收发消息。

然后这些写在循环语句里,不停的做着这些事。

这些代码就不详细解释了,目前大概理解即可,有需要可自行看代码注释。后面有需要再说明。

4.ThreadOpenConnections

然后我们来看第二个线程函数:

cpp 复制代码
if (_beginthread(ThreadOpenConnections, 0, NULL) == -1)
{
    strError = "Error: _beginthread(ThreadOpenConnections) failed";
    printf("%s\n", strError.c_str());
    return false;
}

代码:

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

    loop
    {
        vfThreadRunning[1] = true;
        CheckForShutdown(1);
        try
        {
            ThreadOpenConnections2(parg);
        }
        CATCH_PRINT_EXCEPTION("ThreadOpenConnections()")
        vfThreadRunning[1] = false;
        Sleep(5000);
    }
}

写法一样,干活的是另写了一个函数。

5.ThreadOpenConnections2

在前面我们看到了做为服务端连接节点,把socket添加到vNodes的过程,但并没有见到做为客户端连接的代码,而ThreadOpenConnections2就负责这样的事,即ThreadOpenConnections线程函数,就是负责当节点数少于一定的数量,就主动从mapAddresses找节点连接,然后维持节点上限。

但是我们看这个函数据代码,并不是很简单的遍历连接,还有很多其它的代码,他挑选节点有着一定的算法,目的是为了防范IP攻击。

以及将节点分类,并相应处理的逻辑,包括哪些节点连接过,哪些节点优先级较低不稳定等。

我们来看一下具体代码:

cpp 复制代码
// ==================== 主动对外建立连接的线程 ====================
void ThreadOpenConnections2(void* parg)
{
    printf("ThreadOpenConnections started\n");   // 线程启动提示信息

    // 最多主动维护的连接数量(早期比特币版本设置为15)
    const int nMaxConnections = 15;

    loop   // 无限循环,等价于 while(true)
    {
        // ====================== 1. 等待与休眠逻辑 ======================
        vfThreadRunning[1] = false;      // 标记线程1(OpenConnections)暂时空闲
        Sleep(500);                      // 每次循环先休眠500毫秒

        // 如果当前已连接节点数量已达到上限,或者地址池里没有更多地址,就等待
        while (vNodes.size() >= nMaxConnections || vNodes.size() >= mapAddresses.size())
        {
            CheckForShutdown(1);         // 检查程序是否要退出
            Sleep(2000);                 // 每2秒检查一次
        }

        vfThreadRunning[1] = true;       // 标记线程开始工作
        CheckForShutdown(1);             // 再次检查退出信号


        // ====================== 2. 构建 Class C 列表(防攻击设计) ======================
        // 定义 Class C 子网掩码:255.255.255.0 (只取前3个字节)
        unsigned char pchIPCMask[4] = { 0xff, 0xff, 0xff, 0x00 };
        unsigned int nIPCMask = *(unsigned int*)pchIPCMask;

        vector<unsigned int> vIPC;       // 用来保存所有唯一的 Class C 网络地址

        // 加锁保护地址映射表
        CRITICAL_BLOCK(cs_mapAddresses)
        {
            vIPC.reserve(mapAddresses.size());   // 预分配内存,提高效率
            unsigned int nPrev = 0;

            // 遍历整个地址数据库
            foreach(const PAIRTYPE(vector<unsigned char>, CAddress)& item, mapAddresses)
            {
                const CAddress& addr = item.second;

                if (!addr.IsIPv4())              // 只处理IPv4地址
                    continue;

                // 取出 Class C 前缀(a.b.c.0)
                unsigned int ipC = addr.ip & nIPCMask;

                // 利用 map 已经排序的特性,去重并记录每个 Class C
                if (ipC != nPrev)
                    vIPC.push_back(nPrev = ipC);
            }
        }


        // ====================== 3. 核心选址和连接尝试 ======================
        bool fSuccess = false;           // 是否成功建立了一个新连接
        int nLimit = vIPC.size();        // 防止死循环,最多尝试这么多 Class C

        // 只要没有成功连接,并且还有可尝试的 Class C,就继续循环
        while (!fSuccess && nLimit-- > 0)
        {
            // 随机选择一个 Class C 网络
            unsigned int ipC = vIPC[GetRand(vIPC.size())];

            // 用于存放该 Class C 下所有可用 IP 的地址列表
            map<unsigned int, vector<CAddress> > mapIP;

            CRITICAL_BLOCK(cs_mapAddresses)
            {
                // 动态计算等待时间:连接越多,惩罚等待时间越长(防止频繁失败)
                unsigned int nDelay = ((30 * 60) << vNodes.size());   // 30分钟左移当前连接数位
                if (nDelay > 8 * 60 * 60)
                    nDelay = 8 * 60 * 60;      // 最大等待8小时

                // 在地址数据库中查找属于当前 Class C 的所有地址
                for (map<vector<unsigned char>, CAddress>::iterator mi = 
                     mapAddresses.lower_bound(CAddress(ipC, 0).GetKey());
                     mi != mapAddresses.upper_bound(CAddress(ipC | ~nIPCMask, 0xffff).GetKey());
                     ++mi)
                {
                    const CAddress& addr = (*mi).second;

                    // 计算一个随机因子,失败越多次的地址越难被选中
                    unsigned int nRandomizer = (addr.nLastFailed * addr.ip * 7777U) % 20000;

                    // 只选择距离上次失败时间足够久的地址
                    if (GetTime() - addr.nLastFailed > nDelay * nRandomizer / 10000)
                        mapIP[addr.ip].push_back(addr);
                }
            }

            if (mapIP.empty())      // 该 Class C 没有可用地址,跳过
                break;

            // 在当前 Class C 的 IP 中随机选择一个 IP
            map<unsigned int, vector<CAddress> >::iterator mi = mapIP.begin();
            advance(mi, GetRand(mapIP.size()));

            // 尝试该 IP 的所有端口(早期比特币一个 IP 可能有多个端口记录)
            foreach(const CAddress& addrConnect, (*mi).second)
            {
                // 跳过以下情况:
                if (addrConnect.ip == addrLocalHost.ip ||     // 自己
                    !addrConnect.IsIPv4() ||                  // 非IPv4
                    FindNode(addrConnect.ip))                 // 已经连接过了
                    continue;

                // 尝试建立 TCP 连接
                CNode* pnode = ConnectNode(addrConnect);
                if (!pnode)          // 连接失败,继续尝试下一个
                    continue;

                // 标记为我们主动发起的连接(NetworkNode)
                pnode->fNetworkNode = true;

                // ====================== 连接成功后的初始化操作 ======================
                if (addrLocalHost.IsRoutable())
                {
                    // 把自己(本地节点)的地址告诉对方
                    vector<CAddress> vAddrToSend;
                    vAddrToSend.push_back(addrLocalHost);
                    pnode->PushMessage("addr", vAddrToSend);
                }

                // 请求对方给我们尽可能多的其他节点地址
                pnode->PushMessage("getaddr");

                // 订阅消息通道(早期比特币的 P2P 订阅机制)
                const unsigned int nHops = 0;
                for (unsigned int nChannel = 0; nChannel < pnodeLocalHost->vfSubscribe.size(); nChannel++)
                    if (pnodeLocalHost->vfSubscribe[nChannel])
                        pnode->PushMessage("subscribe", nChannel, nHops);

                fSuccess = true;     // 成功建立连接,跳出外层循环
                break;
            }
        }   // while (!fSuccess && nLimit-- > 0)

    }   // end loop
}

6.防止IP攻击

如果你的节点,连接选的都是造假者的IP,那么你这个节点就容易被忽悠,然后你又传播造假的数据,会壮大造假者的力量。

这样我们在挑选节点的时候,需要一种能控制的的方法。

这个方法就是避免同一网段的IP,比如192.168.1.0,192.168.1.2,前三位一致,就会被筛选掉,只保留一个放在集合中(vIPC--去重网段),然后再根据vIPC获取IP地址。这样保证不会选到大量同一网段的地址。

为什么攻击者会大量使用同一网段呢?这是因为成本和资源的问题,所以基本都是这样。

好,这部分对应的筛选代码如下:

cpp 复制代码
        // ====================== 2. 构建 Class C 列表(防攻击设计) ======================
        // 定义 Class C 子网掩码:255.255.255.0 (只取前3个字节)
        unsigned char pchIPCMask[4] = { 0xff, 0xff, 0xff, 0x00 };
        unsigned int nIPCMask = *(unsigned int*)pchIPCMask;

        vector<unsigned int> vIPC;       // 用来保存所有唯一的 Class C 网络地址

        // 加锁保护地址映射表
        CRITICAL_BLOCK(cs_mapAddresses)
        {
            vIPC.reserve(mapAddresses.size());   // 预分配内存,提高效率
            unsigned int nPrev = 0;

            // 遍历整个地址数据库
            foreach(const PAIRTYPE(vector<unsigned char>, CAddress)& item, mapAddresses)
            {
                const CAddress& addr = item.second;

                if (!addr.IsIPv4())              // 只处理IPv4地址
                    continue;

                // 取出 Class C 前缀(a.b.c.0)
                unsigned int ipC = addr.ip & nIPCMask;

                // 利用 map 已经排序的特性,去重并记录每个 Class C
                if (ipC != nPrev)
                    vIPC.push_back(nPrev = ipC);
            }
        }

我们来看这两句:

cpp 复制代码
        // ====================== 2. 构建 Class C 列表(防攻击设计) ======================
        // 定义 Class C 子网掩码:255.255.255.0 (只取前3个字节)
        unsigned char pchIPCMask[4] = { 0xff, 0xff, 0xff, 0x00 };
        unsigned int nIPCMask = *(unsigned int*)pchIPCMask;

因为IP地址是4位的,并且每位不超过255,那么它其实也可以用一个unsigned int来描述,正好是4字节的。

上面两句代码就是做的这个事,将255.255.255.0这个IP地址,转换成int类型描述,即nIPCMask。

然后就是从地址库里取出地址,生成相应vIPC网段集合。

cpp 复制代码
    // Taking advantage of mapAddresses being in sorted order,
    // with IPs of the same class C grouped together.
    unsigned int ipC = addr.ip & nIPCMask;
    if (ipC != nPrev)
        vIPC.push_back(nPrev = ipC);

将取出的地址addr.ip跟nIPCMask进行按位与运行:addr.ip&nIPCMask,这样如果192.168.1.22和这个255.255.255.0进行二进制按位与计算,它就只会保留前三个字节的数据,抽象的理解是这三个192.168.1,最终ipC是192.168.1.0。

然后把它存进nPrev里,并在下一次比较nPrecv,如果下一次还是192.168.1.0则不进行添加。

就是这么个原理。

6.建立网段集合ip

接下来我们来看下挑选IP的代码:

cpp 复制代码
bool fSuccess = false;                    // 初始化成功标志为 false,表示当前还未成功连接

int nLimit = vIPC.size();                 // 设置尝试次数上限为 vIPC(IP 段列表)的大小,防止无限循环

while (!fSuccess && nLimit-- > 0)         // 只要还没成功,并且还有尝试次数,就继续循环(nLimit 每次循环后减1)
{
    // Choose a random class C
    unsigned int ipC = vIPC[GetRand(vIPC.size())];   
    // 从 vIPC 中随机选择一个 Class C 的网络段(例如 192.168.1.0)

    // Organize all addresses in the class C by IP
    map<unsigned int, vector<CAddress> > mapIP;      
    // 创建一个 map,用于按 IP 地址组织当前 Class C 网段内的所有节点地址

    CRITICAL_BLOCK(cs_mapAddresses)                   // 进入临界区,锁定 mapAddresses,防止多线程并发修改
    {
        unsigned int nDelay = ((30 * 60) << vNodes.size());  
        // 计算基础延迟时间:30分钟左移当前连接节点数(节点越多,延迟越长)

        if (nDelay > 8 * 60 * 60)                     
            nDelay = 8 * 60 * 60;                     // 延迟时间上限设为 8 小时,防止溢出或过大

        // 遍历 mapAddresses 中属于当前 Class C 网段的所有地址
        for (map<vector<unsigned char>, CAddress>::iterator mi = 
             mapAddresses.lower_bound(CAddress(ipC, 0).GetKey()); 
             mi != mapAddresses.upper_bound(CAddress(ipC | ~nIPCMask, 0xffff).GetKey()); 
             ++mi)
        {
            const CAddress& addr = (*mi).second;      // 获取当前迭代到的地址对象

            // 生成一个随机因子,用于错开尝试时间,避免集中失败
            unsigned int nRandomizer = (addr.nLastFailed * addr.ip * 7777U) % 20000;

            // 判断该地址是否已经过了足够的冷却时间(失败越久、随机因子越大,越容易被选中)
            if (GetTime() - addr.nLastFailed > nDelay * nRandomizer / 10000)
                mapIP[addr.ip].push_back(addr);        // 将符合条件的地址按 IP 放入 mapIP
        }
    }

    if (mapIP.empty())                            // 如果当前 Class C 网段内没有符合条件的地址,则放弃,重新进入更上级的循环sleep一会儿然后重新随机网段。
        break;           

这段代码做的事就是,随机选择一个网段,然后根据这个网段,从mapAddresses,查找对应的IP。找到的IP放在mapIP里,也就是说建立该网段的IP集合。

我们来看,这个关键的语句:

cpp 复制代码
        for (map<vector<unsigned char>, CAddress>::iterator mi = mapAddresses.lower_bound(CAddress(ipC, 0).GetKey());
                 mi != mapAddresses.upper_bound(CAddress(ipC | ~nIPCMask, 0xffff).GetKey());

这就是遍历那个网段的逻辑,因为mapAddress里面是有序排列的,所以找到对应的起始map.Addresses.lower_bound,然后结束.upper_bound,按顺序遍历即可。

然后找到这个网段的IP,添加进去也是有条件的,如下:

cpp 复制代码
    if (GetTime() - addr.nLastFailed > nDelay * nRandomizer / 10000)
                    mapIP[addr.ip].push_back(addr);
            

当前面时间减去这个ip上次连接失败的时间,就是节点连接失败后的冷却时间,要满足要求,才能被重新加入,那这个要求有一定的随机性,但也遵循着一定的规则,就是后面那个nDelay*nRandomizer/10000算法。

当然如果这个ip从未被连接或者从未失败过,那addr.nLastFailed默认初始化的时候为0.

那么此时GetTime()就是一个非常大的数字,也会被加入到mapIP。

而关于这样会重复连接的问题,这个不属于这里的工作,会在连接时进行判断。

注意:关于这里的mapIP的key和value,value是个vector数组,因为同一个IP可能有不同的端口,所以有的可能会对应多个CAddress。所以是这样定义的:

cpp 复制代码
     // Organize all addresses in the class C by IP
     map<unsigned int, vector<CAddress> > mapIP;
7.mapIP.empty

然后这样收集后,我们竟然发现mapIP里面一个也没有,就是这样的IP一个也没找到。

则会放弃这次尝试,直接跳出来,如下:

cpp 复制代码
   if (mapIP.empty())
       break;

回到函数的开头,即下面这个循环:

cpp 复制代码
void ThreadOpenConnections2(void* parg)
{
    printf("ThreadOpenConnections started\n");

    // Initiate network connections
    const int nMaxConnections = 15;
    loop
    {
        // Wait
        vfThreadRunning[1] = false;
        Sleep(500);
        while (vNodes.size() >= nMaxConnections || vNodes.size() >= mapAddresses.size())
        {
            CheckForShutdown(1);
            Sleep(2000);
        }
        vfThreadRunning[1] = true;
        CheckForShutdown(1);

即跳出while循环,从loop处重新开始。

8.进行连接

直到mapIP对应网段收集IP成功后,就从里面挑一个IP尝试进行连接,如下:

cpp 复制代码
     // 在当前 Class C 的 IP 中随机选择一个 IP
            map<unsigned int, vector<CAddress> >::iterator mi = mapIP.begin();
            advance(mi, GetRand(mapIP.size()));

            // 尝试该 IP 的所有端口(早期比特币一个 IP 可能有多个端口记录)
            foreach(const CAddress& addrConnect, (*mi).second)
            {
                // 跳过以下情况:
                if (addrConnect.ip == addrLocalHost.ip ||     // 自己
                    !addrConnect.IsIPv4() ||                  // 非IPv4
                    FindNode(addrConnect.ip))                 // 已经连接过了
                    continue;

                // 尝试建立 TCP 连接
                CNode* pnode = ConnectNode(addrConnect);
                if (!pnode)          // 连接失败,继续尝试下一个
                    continue;

                // 标记为我们主动发起的连接(NetworkNode)
                pnode->fNetworkNode = true;

                // ====================== 连接成功后的初始化操作 ======================
                if (addrLocalHost.IsRoutable())
                {
                    // 把自己(本地节点)的地址告诉对方
                    vector<CAddress> vAddrToSend;
                    vAddrToSend.push_back(addrLocalHost);
                    pnode->PushMessage("addr", vAddrToSend);
                }

                // 请求对方给我们尽可能多的其他节点地址
                pnode->PushMessage("getaddr");

                // 订阅消息通道(早期比特币的 P2P 订阅机制)
                const unsigned int nHops = 0;
                for (unsigned int nChannel = 0; nChannel < pnodeLocalHost->vfSubscribe.size(); nChannel++)
                    if (pnodeLocalHost->vfSubscribe[nChannel])
                        pnode->PushMessage("subscribe", nChannel, nHops);

                fSuccess = true;     // 成功建立连接,跳出外层循环
                break;
            }
        }   // while (!fSuccess && nLimit-- > 0)

    }   // end loop

先是通过:

cpp 复制代码
advance(mi, GetRand(mapIP.size()));,

advance将迭代器向前移动n个位置。由于这个位置是随机传入,所以达到了随机挑选IP的效果。

然后遍历该IP下的所有CAddress(如果有多个端口的情况),只要有一个成功就退出。

cpp 复制代码
  foreach(const CAddress& addrConnect, (*mi).second)
            {

该函数后面的代码,自己看注释吧,我们说一下几个关键的地方:

cpp 复制代码
        // 尝试建立 TCP 连接
                CNode* pnode = ConnectNode(addrConnect);

这里是调用ConnectNode函数实现连接的,这个函数在三十一章里面讲过,就是在这里被调用的。

然后是:

cpp 复制代码
   if (addrLocalHost.IsRoutable())
                {
                    // 把自己(本地节点)的地址告诉对方
                    vector<CAddress> vAddrToSend;
                    vAddrToSend.push_back(addrLocalHost);
                    pnode->PushMessage("addr", vAddrToSend);
                }

我们发送消息的,标记"addr",记住这个消息标志,后面解析消息的时候能对上。

而在这里我们发送给其它的节点的IP是addrLocalHost,那么在这里我们就知道为什么之前要获取外网地址了吧,我们总不能告诉其它节点的局域网地址吧,那对它们没用。

然后记住这个消息标记----getaddr,请求对方节点分享他知道的节点,到时候我们可以在消息处理处,看到会怎么处理。

cpp 复制代码
  // Get as many addresses as we can
  pnode->PushMessage("getaddr");

然后是:

cpp 复制代码
 fSuccess = true;
 break;

到这里设置fSucces为真,表示连接上了,结束while循环,否则一直在continue,如果没连接上的话。fSucces也不会被设置。

好连接成功后,就回到了loop循环:

cpp 复制代码
    loop
    {
        // Wait
        vfThreadRunning[1] = false;
        Sleep(500);
        while (vNodes.size() >= nMaxConnections || vNodes.size() >= mapAddresses.size())
        {
            CheckForShutdown(1);
            Sleep(2000);
        }

看到这里的另一个while循环了吗?这里在判断连接上限,一旦连接的节点数量大于等于nMaxConnections,最大连接也就是15个,它就是会Sleep(2000)秒,然后一直循环监控。

是在这里控制连接上限的,从而不会一直连接节点。

9.异常抓取和恢复

理解完整个函数后,我们发现这个函数本身会一直执行和循环,但为什么上级还要加个循环调用呢:

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

    loop
    {
        vfThreadRunning[1] = true;
        CheckForShutdown(1);
        try
        {
            ThreadOpenConnections2(parg);
        }
        CATCH_PRINT_EXCEPTION("ThreadOpenConnections()")
        vfThreadRunning[1] = false;
        Sleep(5000);
    }
}

这是为了防止ThreadOpenConnections2函数异常退出,然后我们能捕获异常,并重新启动该函数。

10.网段随机

我们再来思考一下为什么要这样设计,每一次随机一个网段,然后再从该网段下随机选一个IP。这样做的原因是,如果有10个网段,那么1到9,每个网段只有100个地址,而攻击者在第10个网段有1000个地址,如果纯粹随机IP的话,那么攻击者有2分之一的概率被选中,而这样如果根据网段随机的话,那么,攻击者只有10分之一的概率被选中。

这就是设计的巧妙之处,vIPC里为什么只是网段的集合,而不是去重网段IP的集合。这样虽然会将同一网段的大量的IP去掉,但也会误伤正常网段的IP。

所以这种设计是比较合理的方法,能尽量防止伪造者的攻击,增加伪造者的成本,又能尽量降低对正常节点的影响。

11.ThreadMessageHandler

StartNode启动的最后一个线程函数,ThreadMessageHandler。这个是主要用来处理接收到的消息。涉及到拆分解析消息,比如之前讲的消息标志"tx","getaddr",就是在这个函数解析处理的。

这个函数就放在下一章说明吧。就先到这里了。