问题,网络功能需要两个节点,那就需要连接,那应该怎么连接呢?
在数据库里填写进去就可以了吗?就是存储节点表那里。
我记得是不行的,因为是在表里随机选的,得手动改代码指定连接,具体操作在第三十六章。
但是现在,我们不仅仅是简单的一个连接,我们要修复网络功能。
所以我们现在得详细了解建立连接的整个过程,包括你连接别人,和别人连接你,都是怎么样连接的。
以及如果只有一个IP地址在节点表里,那么,这种随机的方法,连接上的概率有多少。如果是很快,必定连接的,那么可以把IP写数据库,不用自己再写别的代码了。就是第三十六章方法。但是似乎还有一个问题,就是如果同一局域网内,是会过滤的。具体,我们还是要看一下整个代码和流程。然后相应修改设置。
1.LoadAddresses
我们先来看,地址从数据库里加载的过程,在程序初始化时,会调用LoadAddresses函数。
加载地址到mapAddresses之中。然后就是从这里挑选地址连接的。
相关函数,和类型,大部分都定义过了,后续我们看情况补充。
cpp
bool LoadAddresses()
{
return CAddrDB("cr+").LoadAddresses();
}
这个全局函数,直接就是调用地址类CAddrDB里的LoadAddresses实现的。
cpp
// CAddrDB 类的 LoadAddresses 成员函数。
// 返回值 bool:true 表示加载成功,false 表示加载失败。
bool CAddrDB::LoadAddresses()
{
// 进入临界区,锁定 cs_mapAddresses。
// 防止多个线程同时访问或修改 mapAddresses,
// 避免出现并发访问导致的数据竞争。
CRITICAL_BLOCK(cs_mapAddresses)
{
// ==============================
// 第一部分:加载用户提供的地址
// ==============================
// 以文本读取模式打开 addr.txt 文件。
// CAutoFile 是一个自动管理文件资源的封装类,
// 离开作用域时会自动关闭文件。
CAutoFile filein = fopen("addr.txt", "rt");
// 判断文件是否成功打开。
// 如果 filein 有效,则进入文件读取逻辑。
if (filein)
{
try
{
// 定义一个长度为 1000 字节的字符数组。
// 用于临时存放从 addr.txt 中读取的一行文本。
char psz[1000];
// fgets 从文件中读取一行内容。
// 每次最多读取 sizeof(psz)-1 个字符,
// 并自动在末尾添加 '\0'。
// 当读取失败或到达文件末尾时,循环结束。
while (fgets(psz, sizeof(psz), filein))
{
// 使用读取到的字符串构造 CAddress 对象。
// NODE_NETWORK 表示该地址对应的节点支持网络服务。
CAddress addr(psz, NODE_NETWORK);
// 判断解析得到的 IP 地址是否有效。
// 这里以 ip != 0 作为有效性判断条件。
if (addr.ip != 0)
// 将地址加入当前地址数据库。
// AddAddress 负责处理地址的添加逻辑。
AddAddress(*this, addr);
}
}
// 捕获所有异常。
// 如果读取或解析过程中发生异常,则忽略该异常,
// 不再继续处理当前 try 块中的剩余代码。
catch (...) { }
}
// ==============================
// 第二部分:获取数据库游标
// ==============================
// 获取数据库游标。
// Dbc 是 Berkeley DB 的数据库游标类型。
// 游标可以按照数据库中的记录顺序逐条遍历数据。
Dbc* pcursor = GetCursor();
// 判断是否成功获取游标。
// 如果游标获取失败,说明无法遍历数据库。
if (!pcursor)
// 返回 false,表示地址加载失败。
return false;
// loop 是一个循环宏,通常等价于 for(;;),
// 表示无限循环,直到内部遇到 break 或 return。
loop
{
// ==============================
// 第三部分:读取数据库记录
// ==============================
// 创建一个数据流对象,用来保存数据库记录的 Key。
CDataStream ssKey;
// 创建一个数据流对象,用来保存数据库记录的 Value。
CDataStream ssValue;
// 从数据库游标当前位置读取一条记录。
// ssKey 接收记录的键。
// ssValue 接收记录的值。
// ret 接收读取操作的返回状态。
int ret = ReadAtCursor(pcursor, ssKey, ssValue);
// 判断是否已经遍历到数据库末尾。
// DB_NOTFOUND 表示没有找到下一条记录。
if (ret == DB_NOTFOUND)
// 数据库记录已经读取完毕,退出循环。
break;
// 如果返回值不是 0,说明读取过程中出现错误。
// 注意:DB_NOTFOUND 已经在上面单独处理。
else if (ret != 0)
// 返回 false,表示数据库读取失败。
return false;
// ==============================
// 第四部分:反序列化数据库记录
// ==============================
// 定义一个字符串,用于保存数据库记录的类型。
string strType;
// 从 Key 数据流中反序列化出一个字符串。
// 数据库中的记录通常通过 Key 的类型标识区分。
ssKey >> strType;
// 判断当前数据库记录的类型是否为 "addr"。
// 只有地址类型的记录才需要在这里处理。
if (strType == "addr")
{
// 创建一个 CAddress 对象,用于保存反序列化后的地址。
CAddress addr;
// 从 Value 数据流中读取地址数据,
// 并反序列化到 addr 对象中。
ssValue >> addr;
// 将地址插入 mapAddresses 容器。
//
// addr.GetKey():获取地址对应的唯一键。
// addr:作为键对应的地址对象。
//
// make_pair 创建一个键值对。
// insert 将键值对插入 mapAddresses。
mapAddresses.insert(make_pair(addr.GetKey(), addr));
}
}
// ==============================
// 第五部分:调试输出
// ==============================
//// debug print
// 在控制台打印提示信息。
printf("mapAddresses:\n");
// 遍历 mapAddresses 中的所有元素。
//
// PAIRTYPE(vector<unsigned char>, CAddress)
// 表示容器中每个元素都是一个键值对:
// first :vector<unsigned char> 类型的地址键。
// second :CAddress 类型的地址对象。
//
// foreach 是一个遍历宏,类似于现代 C++ 的 range-based for。
foreach(const PAIRTYPE(vector<unsigned char>, CAddress)& item, mapAddresses)
// 调用当前地址对象的 print() 成员函数,
// 将地址信息输出到控制台。
item.second.print();
// 打印分隔线,方便区分调试输出。
printf("-----\n");
}
// 临界区结束,自动释放 cs_mapAddresses 锁。
// 表示地址加载操作完成。
// 返回 true,表示地址加载成功。
return true;
}
2.addr.txt
这个函数会从哪里读取ip地址呢,一个是addr.txt,一个就是CAddrDB关联的数据库文件addr.dat。
先是从addr.txt里读如下:
cpp
CAutoFile filein = fopen("addr.txt", "rt");
fgets按行读取:
cpp
char psz[1000];
while (fgets(psz, sizeof(psz), filein))
{
CAddress addr(psz, NODE_NETWORK);
if (addr.ip != 0)
AddAddress(*this, addr);
}
读到一个地址信息,就将它转换成CAddress类型,然后调用AddAddress将它添加到addr.dat数据库里。接着在后面在遍历addr.dat,一起把它们加载到mapAddresses之中。
我们来看一下转换的构造函数,就知道该怎么填这个IP地址到addr.txt里。他的格式是什么样的。
3.CAddress构造函数
定义如下:
cpp
explicit CAddress(const char* pszIn, uint64 nServicesIn = 0)
{
nServices = nServicesIn; // 1. 设置服务标志
memcpy(pchReserved, pchIPv4, sizeof(pchReserved)); // 2. 把预留字段设为IPv4标记
ip = 0; // 3. IP先清零
port = DEFAULT_PORT; // 4. 端口设为默认端口(早期比特币是8333)
nTime = GetAdjustedTime(); // 5. 记录当前时间
nLastFailed = 0; // 6. 失败次数清零
char psz[100];
if (strlen(pszIn) > ARRAYLEN(psz) - 1) // 7. 字符串太长就直接返回(对象处于半初始化状
态)
return;
strcpy(psz, pszIn); // 8. 复制一份字符串,后面要修改
unsigned int a, b, c, d, e;
if (sscanf(psz, "%u.%u.%u.%u:%u", &a, &b, &c, &d, &e) < 4) // 9. 检查是不是至少是有效的
IPv4格式
return; // 如果连4个数字都解析不出来,直接返回
char* pszPort = strchr(psz, ':'); // 10. 查找冒号位置
if (pszPort)
{
*pszPort++ = '\0'; // 把冒号改成'\0',把IP和端口分开
port = htons(atoi(pszPort)); // 解析端口并转成网络字节序
}
ip = inet_addr(psz); // 11. 把IP字符串转成网络字节序的整数
}
这里有一些其它的设置,我们不管。来看关键的几句:
4.addr.txt填写格式
cpp
if (sscanf(psz, "%u.%u.%u.%u:%u", &a, &b, &c, &d, &e) < 4)
看到中间,那个4个.和最后的:号了吗。说明在addr.txt里指定节点信息,就是:
192.168.1.3:8333
然后换行下一个。
并且这里也可以不正常端口号,就是
192.168.1.3
就会采用默认端口后,根据这句:
cpp
port = DEFAULT_PORT;
char* pszPort = strchr(psz, ':'); // 10. 查找冒号位置
if (pszPort)
{
*pszPort++ = '\0'; // 把冒号改成'\0',把IP和端口分开
port = htons(atoi(pszPort)); // 解析端口并转成网络字节序
}
port的赋值是根据有无冒号来的,如果没有冒号,则是DEFAULT_PORT。
如果有,则是:后面的数字。
5.AddAddress
然后是调用AddAddress函数写入addr.dat,注意这里并没有去重检测,因为函数内只检测了mapAddresses,此时还未加载。
所以addr.txt里的地址,是每次程序启动都会写入覆盖旧记录的。
6.addr.txt在哪里
知道了怎么指定ip地址后,我们该在哪里新建addr.txt文件呢?
cpp
fopen("addr.txt", "rt")
我们根据这句,是在程序同目录的,也就是跟可执行文件exe放在一起。
7.mapAddresses
后面就是读取数据库了:
cpp
// Unserialize
string strType;
ssKey >> strType;
if (strType == "addr")
{
CAddress addr;
ssValue >> addr;
mapAddresses.insert(make_pair(addr.GetKey(), addr));
}
我们只要关注这个mapAddresses就行了,看一下它的定义:
cpp
map<vector<unsigned char>, CAddress> mapAddresses;
这是键值对,值我们知道,就是CAddress格式的ip信息。
键呢,就是:
cpp
addr.GetKey()
具体可以看函数代码,用这个来做唯一标识。
数据就是:
cpp
前 12 字节:pchReserved,IPv4 地址使用的标记字节,固定值为 00 00 00 00 00 00 00 00 00 00 FF FF
接着 4 字节:IP 地址
最后 2 字节:端口
大概了解即可,不必深究。
好,我们以后如果要连接节点,就在程序同目录下新建addr.txt,然后填入ip和端口就行。
之后在程序初始化时调用LoadAddresses即可。这是手动指定的方式。
8.StartNode
加载完了,该怎么连接呢,就是得调用我们之前讲的StartNode函数了。这个函数也放在程序初始化时调用,LoadAddresses后面。
现在我们要进行两个节点连接,在第三十六章,是只要指定一个节点,手动连接另一个节点。
另一个节点,不需要改代码。
那在这里的话,我们建立连接,就是在一个节点b的addr.txt指定另一个节点a的地址就可以了。
然后我们要做的就是,看做为客户端连接服务端的代码,就是在a节点的代码里,看他随机网段挑选IP里,这一段代码我们要详细检查,保证我们定在addr.txt里的IP不会被过滤掉。
这个需要我们理解具体的代码才能实行操作。
9.ThreadOpenConnections2
好,接下来,我们就来看关键的代码,就是在StratNode中的ThreadOpenConnections2函数中,这个函数在第三十三章中有说明,我们来复习一下,这部分代码:
cpp
// 主动建立网络连接的线程函数
// parg 是线程参数,这段代码中没有使用
void ThreadOpenConnections2(void* parg)
{
// 在控制台输出线程启动信息
printf("ThreadOpenConnections started\n");
// 设置最大连接节点数量为 15
const int nMaxConnections = 15;
// loop 是旧版 Bitcoin 使用的无限循环宏,通常等价于 for(;;)
// 这个线程会持续运行,不断寻找可以连接的远程节点
loop
{
// 将第 1 号线程的运行状态设置为 false
// 表示当前线程暂时进入等待阶段
vfThreadRunning[1] = false;
// 休眠 500 毫秒,避免线程频繁占用 CPU
Sleep(500);
// 判断当前是否需要继续等待:
// 条件一:当前连接节点数量已经达到最大连接数 15
// 条件二:当前连接节点数量大于等于已知地址数量
// 只要满足其中一个条件,就继续等待
while (vNodes.size() >= nMaxConnections || vNodes.size() >= mapAddresses.size())
{
// 检查程序是否收到关闭请求
// 参数 1 表示当前线程的编号
CheckForShutdown(1);
// 休眠 2000 毫秒,即 2 秒
// 等待连接数量或地址数量发生变化
Sleep(2000);
}
// 等待结束,将线程运行状态设置为 true
vfThreadRunning[1] = true;
// 再次检查程序是否需要关闭
CheckForShutdown(1);
// ==============================
// 收集所有不同的 Class C 网段
// ==============================
// 定义 IPv4 网段掩码
// 四个字节分别为 255、255、255、0
// 即传统意义上的 Class C 网段掩码,也就是 /24
unsigned char pchIPCMask[4] = { 0xff, 0xff, 0xff, 0x00 };
// 将四个字节的掩码转换为 unsigned int
// 后续通过位与操作提取 IP 地址的前三个字节
// 注意:具体整数值受到机器字节序影响
unsigned int nIPCMask = *(unsigned int*)pchIPCMask;
// 创建一个 unsigned int 类型的 vector
// 用来保存所有不同的 Class C 网段
vector<unsigned int> vIPC;
// 进入地址数据库临界区
// 锁定 cs_mapAddresses,防止其他线程同时修改地址数据库
CRITICAL_BLOCK(cs_mapAddresses)
{
// 预先为 vIPC 分配内存
// 分配容量等于当前地址数据库的元素数量
// 减少后续 push_back 时可能发生的内存重新分配
vIPC.reserve(mapAddresses.size());
// 保存上一次遍历到的网段
// 初始值为 0
unsigned int nPrev = 0;
// 遍历 mapAddresses 中的所有地址记录
// item 是一个键值对:
// first:地址对应的 Key
// second:CAddress 地址对象
foreach(const PAIRTYPE(vector<unsigned char>, CAddress)& item, mapAddresses)
{
// 获取当前记录中的 CAddress 地址对象
// 使用引用,避免复制整个对象
const CAddress& addr = item.second;
// 判断当前地址是否为 IPv4
// 如果不是 IPv4,就跳过当前记录,继续下一次循环
if (!addr.IsIPv4())
continue;
// 提取当前 IP 地址所属的 /24 网段
// addr.ip 是当前 IP 地址
// nIPCMask 是 255.255.255.0 对应的掩码
// 位与操作后得到当前 IP 所属网段
unsigned int ipC = addr.ip & nIPCMask;
// 判断当前网段是否与上一次记录的网段不同
// 如果不同,说明发现了一个新的网段
if (ipC != nPrev)
// 将新网段添加到 vIPC 中
// 同时将 nPrev 更新为当前网段
vIPC.push_back(nPrev = ipC);
}
}
// 临界区结束,自动释放 cs_mapAddresses 锁
// ==============================
// 随机选择网段,防止地址洪泛攻击
// ==============================
// 初始化连接成功标志
// false 表示当前还没有成功建立连接
bool fSuccess = false;
// 获取当前已知的 Class C 网段数量
// 作为本轮最多尝试的网段数量
int nLimit = vIPC.size();
// 只要尚未连接成功,并且剩余尝试次数大于 0,就继续循环
// nLimit-- 表示每次判断时都会将剩余次数减 1
while (!fSuccess && nLimit-- > 0)
{
// 从所有已知的 Class C 网段中随机选择一个
// GetRand(vIPC.size()) 生成一个合法的随机索引
// vIPC[...] 根据随机索引取得对应网段
unsigned int ipC = vIPC[GetRand(vIPC.size())];
// ==============================
// 整理当前网段中的 IP 地址
// ==============================
// 创建一个 map 容器
// Key:unsigned int,表示 IP 地址
// Value:vector<CAddress>,保存该 IP 对应的多个地址对象
// 同一个 IP 可能有不同端口,因此需要使用 vector 保存
map<unsigned int, vector<CAddress> > mapIP;
// 进入地址数据库临界区
// 锁定 cs_mapAddresses,保护地址读取操作
CRITICAL_BLOCK(cs_mapAddresses)
{
// 计算地址连接失败后的基础重试延迟
// 30 * 60 = 1800 秒,即 30 分钟
// << vNodes.size() 表示左移当前连接节点数量位
// 等价于乘以 2 的 vNodes.size() 次方
unsigned int nDelay = ((30 * 60) << vNodes.size());
// 如果计算出的延迟超过 8 小时
// 就将最大延迟限制为 8 小时
if (nDelay > 8 * 60 * 60)
nDelay = 8 * 60 * 60;
// 遍历 mapAddresses 中属于当前 Class C 网段的地址
//
// lower_bound:
// 找到第一个 Key 大于等于指定起始 Key 的元素
//
// CAddress(ipC, 0):
// 构造当前网段的起始地址,端口为 0
//
// GetKey():
// 获取这个地址对应的数据库 Key
//
// upper_bound:
// 找到第一个 Key 大于指定结束 Key 的元素
//
// CAddress(ipC | ~nIPCMask, 0xffff):
// 构造当前网段的结束地址,端口为 65535
//
// 因此,这个 for 循环只遍历当前网段中的地址
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)
{
// 获取当前遍历到的 CAddress 地址对象
const CAddress& addr = (*mi).second;
// 根据上次失败时间、IP 地址以及常数 7777
// 计算一个伪随机化数值
//
// % 20000 将结果限制在 0 到 19999 之间
//
// 这个数值用于随机化不同地址的重试时间
unsigned int nRandomizer = (addr.nLastFailed * addr.ip * 7777U) % 20000;
// 判断当前地址是否已经等待足够长的时间
//
// GetTime():
// 获取当前时间戳
//
// addr.nLastFailed:
// 当前地址上一次连接失败的时间
//
// nDelay * nRandomizer / 10000:
// 计算当前地址需要等待的重试时间
//
// 如果当前时间减去上次失败时间,大于要求的重试时间
// 就允许当前地址重新参与连接选择
if (GetTime() - addr.nLastFailed > nDelay * nRandomizer / 10000)
// 将当前地址加入 mapIP
// addr.ip 作为 map 的 Key
// addr 作为对应的地址对象
//
// 同一个 IP 的不同端口会被放入同一个 vector
mapIP[addr.ip].push_back(addr);
}
}
// 临界区结束,释放地址数据库锁
// ==============================
// 判断当前网段是否有可用地址
// ==============================
// 如果 mapIP 为空,说明当前网段没有符合重试条件的地址
if (mapIP.empty())
// 退出当前网段选择循环
break;
// ==============================
// 随机选择一个 IP 地址
// ==============================
// 获取 mapIP 中第一个元素的迭代器
// map 默认按照 Key 排序,因此 begin() 指向 IP 最小的元素
map<unsigned int, vector<CAddress> >::iterator mi = mapIP.begin();
// 将迭代器向后移动一个随机距离
// GetRand(mapIP.size()) 生成随机位置
// advance 将迭代器移动到对应位置
// 最终随机选中一个 IP 地址
advance(mi, GetRand(mapIP.size()));
// ==============================
// 尝试连接选中的 IP
// ==============================
// 遍历当前选中 IP 对应的所有地址
// 一个 IP 可能有多个端口,因此会依次尝试
foreach(const CAddress& addrConnect, (*mi).second)
{
// 判断当前地址是否应该跳过
//
// 条件一:
// addrConnect.ip == addrLocalHost.ip
// 目标 IP 是自己的本地 IP,不应该连接自己
//
// 条件二:
// !addrConnect.IsIPv4()
// 目标地址不是 IPv4,跳过
//
// 条件三:
// FindNode(addrConnect.ip)
// 检查该 IP 是否已经存在于当前连接节点列表中
// 如果已经存在,就跳过,避免重复连接
if (addrConnect.ip == addrLocalHost.ip || !addrConnect.IsIPv4() || FindNode(addrConnect.ip))
continue;
// 尝试与目标地址建立网络连接
// ConnectNode 成功时返回 CNode 指针
// 失败时返回 NULL
CNode* pnode = ConnectNode(addrConnect);
// 判断连接是否失败
if (!pnode)
// 连接失败,跳过当前地址
// 继续尝试当前 IP 对应的下一个端口
continue;
// 将当前节点标记为正常的网络节点
pnode->fNetworkNode = true;
// ==============================
// 向远程节点广播自己的地址
// ==============================
// 判断本地地址是否可以被公网路由访问
// 如果可以,就向远程节点公布自己的地址
if (addrLocalHost.IsRoutable())
{
// 创建一个地址列表
// 用于存放准备发送给远程节点的地址
vector<CAddress> vAddrToSend;
// 将本地节点自己的地址加入发送列表
vAddrToSend.push_back(addrLocalHost);
// 向远程节点发送 addr 消息
// 告诉对方自己的网络地址
pnode->PushMessage("addr", vAddrToSend);
}
// ==============================
// 请求远程节点提供更多地址
// ==============================
// 向远程节点发送 getaddr 消息
// 请求对方提供它所知道的其他网络节点地址
pnode->PushMessage("getaddr");
////// should the one on the receiving end do this too?
// 开发者留下的疑问:
// 接收连接的一方是否也应该执行类似的订阅操作?
// ==============================
// 向远程节点发送频道订阅信息
// ==============================
// 定义订阅消息的跳数参数,初始化为 0
const unsigned int nHops = 0;
// 遍历本地节点的所有频道订阅状态
// nChannel 表示当前频道编号
// vfSubscribe 保存每个频道是否已经订阅的状态
for (unsigned int nChannel = 0; nChannel < pnodeLocalHost->vfSubscribe.size(); nChannel++)
// 判断本地节点是否已经订阅当前频道
// 如果为 true,就向远程节点发送订阅消息
if (pnodeLocalHost->vfSubscribe[nChannel])
// 发送 subscribe 消息
// 第一个参数:消息类型
// 第二个参数:频道编号
// 第三个参数:跳数参数
//
// 告诉远程节点本地节点订阅了哪个频道
pnode->PushMessage("subscribe", nChannel, nHops);
// ==============================
// 当前连接尝试成功
// ==============================
// 设置连接成功标志
// 表示本轮已经成功建立一个网络连接
fSuccess = true;
// 当前 IP 已经连接成功
// 不需要继续尝试该 IP 对应的其他端口
break;
}
}
// 当前一轮节点选择和连接尝试结束
// 回到最外层 loop,继续下一轮节点发现和连接工作
}
}
这个函数的核心任务是:从本地地址数据库中选择合适的远程节点,主动建立网络连接,并向对方请求更多节点地址,同时向对方广播自己的地址和订阅信息。、
关于订阅信息,已经废弃,无视即可。
10.挑选网段
我们先来看网段定义规则,所谓网段不是固定的,看你怎么设置。
比如下面这个:
cpp
// 定义 IPv4 网段掩码
// 四个字节分别为 255、255、255、0
// 即传统意义上的 Class C 网段掩码,也就是 /24
unsigned char pchIPCMask[4] = { 0xff, 0xff, 0xff, 0x00 };
/24网段,就是按前面三个来算网段,意思,就是128.117.112.1,128.117.112.18,前三个一样的算同一网段。
/这里的24,指的就是24位,一个数字8个字节。正好前三个段。
那么如果是/16,那就是前两位 120.118.1.18 120.118.0.27,也算同一个网段,如果是/24,那这两个ip地址就不是同一网段。因为前三位数字,是不一样的。
好,我们这里就是/24 子网掩码255、255、255.0的网段。
他是按前三位分网段的。
11.挑选IP流程算法
我们看关键是怎么挑选IP的就行。
vIPC--收集网段
cpp
vector<unsigned int> vIPC;
这个数据是用来存储网段的,怎么存储的呢?就是根据mapAddresses里的地址来存储。
比如,有两个地址,192.168.1.108 192.168.1.03 它就会收集对应的网段192.168.1.0存在里面了。
注意了,不管你的mapAddresses有多少个这样的网段地址,它只会收集一个,就是192.168.1.0。
除非你是另一个网段,比如220.33.0.88,那么就又会收集一个网段220.33.0.0在vIPC里面。
好,这个情况我们要重视,即网段分类,实际是根据你mapAddresses地址来的,而不是在225X225X225这个多个网段里选择。这个区别很重要。
即,你的mapAddresses有多少个网段,产生多少个网段,就在这些网段里面选择IP。
如果是这样,那么你的addr.txt即使只有一条ip地址,也会被选中连接。我之前没细思考代码。大概以为是/24所有的网段。
所以按实际,我们只有一条192.168.1.105,那么实际这里的vIPC里只有一个元素,对应着我们那一条网段。
ipC 随机网段
收集网段成功后看这一句:
cpp
unsigned int ipC = vIPC[GetRand(vIPC.size())];
它就会从vIPC里随机挑选一个网段给ipC.
注意这个算法GetRand获取随机数,它返回随机值范围是0到参数减1。
所以如果你只有一个地址,那么vIPC.size()就是1,所以返回0。而vIPC0,就是唯一的那个元素。意思是只能从这个网段里面选择,
mapIP--收集IP
随机出网段后,然后遍历该网段的地址。
先看怎么遍历的:
cpp
// 因此,这个 for 循环只遍历当前网段中的地址
for (map<vector<unsigned char>, CAddress>::iterator mi = mapAddresses.lower_bound(CAddress(ipC, 0).GetKey());
mi != mapAddresses.upper_bound(CAddress(ipC | ~nIPCMask, 0xffff).GetKey());
这个lower_bound,根据参数,指向大于或等于该参数的位置迭代器,比如说是2174,如果没有2174,2175,那就指向2176。
那么upper_bound差不多,是指向大于的那个参数(没有等于),比如参数是3213。如果没有3214,3215,3216,那就是指向3217。(其实按这个逻辑,用两个lower_bound也能实现范围遍历,只不过不利于阅读)
那么这里实际的代码就是遍历对应网段ipC里的所有地址,在mapAddresses中。
这里面遍历逻辑正常是因为内部已经排好序了。
遍历的所有地址放进这里:
cpp
mapIP[addr.ip].push_back(addr);
同样的我只有一条地址,那么就是只执行一次,得到那一条192.168.1.105。
所以mapIP只有一个元素。
随机IP
然后是这句:
cpp
map<unsigned int, vector<CAddress> >::iterator mi = mapIP.begin();
// 将迭代器向后移动一个随机距离
// GetRand(mapIP.size()) 生成随机位置
// advance 将迭代器移动到对应位置
// 最终随机选中一个 IP 地址
advance(mi, GetRand(mapIP.size()));
毫无意外,我们这里实际就是advance(mi,0),mi向前移动0位,也就是mapIP0。
就是我的们那个192.168.1.105。
然后就是最终的函数,进行连接:
cpp
CNode* pnode = ConnectNode(addrConnect);
好,到这里,我们知道了,如果你在addr.txt里填地址,他会一次就连接了。这里的随机,并不会把地址给筛选掉。
所以整个函数,我们只要注释掉里面的这句就行了:
cpp
// pnode->PushMessage("getaddr"); //临时注释 暂不获取对方存储的所有节点地址即对方的mapAddresses
这个功能,我们暂时不做,后续开放。因为我们现在就是几个局域地址。后续出于学习目的可以去实现它。不过那也是用局域网地址演示一下。因为这里有个问题。
即,这个函数里,我为什么不注释掉这个功能呢:
cpp
if (addrLocalHost.IsRoutable())
{
// Advertise our address
vector<CAddress> vAddrToSend;
vAddrToSend.push_back(addrLocalHost);
pnode->PushMessage("addr", vAddrToSend);
}
跟订阅功能是一样的原因。
因为我们用的就是局域网地址,而这个IsRoutable一旦识别到你的地址是192.168开头,会返回false。所以这里我不用注释,里面的代码不会执行。
订阅功能在节点初始化时,有这句:
cpp
vfSubscribe.assign(256, false);
也没有设置为true的地方,所以也不会被执行。而且这个功能后面本身也是废弃的。不管是现在和未来,都不需要去担心它。
好,还剩两个地方要解决,一个是关于IsRoutable函数,他会造成另一个问题(无法添加局域网地址)。
另一个是ConnectNode连接节点里,会进行getblocks操作,这个是节点同步区块,就是把我们之前的单节点区块同步给别人。即b节点获取a节点上的区块进行同步。
这个功能是我们重点要实现的,但不是现在,我们先把连接节点完成。
篇幅有限,下一章中测试连接。