接下来我们来说明一下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",就是在这个函数解析处理的。
这个函数就放在下一章说明吧。就先到这里了。