五十一.网络(1)--加载节点地址和连接

问题,网络功能需要两个节点,那就需要连接,那应该怎么连接呢?

在数据库里填写进去就可以了吗?就是存储节点表那里。

我记得是不行的,因为是在表里随机选的,得手动改代码指定连接,具体操作在第三十六章。

但是现在,我们不仅仅是简单的一个连接,我们要修复网络功能。

所以我们现在得详细了解建立连接的整个过程,包括你连接别人,和别人连接你,都是怎么样连接的。

以及如果只有一个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节点上的区块进行同步。

这个功能是我们重点要实现的,但不是现在,我们先把连接节点完成。

篇幅有限,下一章中测试连接。

相关推荐
OxYGC1 小时前
[Web3] 基础第7篇: 说透比特币的基本框架与原理
web3·区块链·以太坊·比特币
混凝土拌意大利面3 小时前
WEB3核心技术的一种实现:时间长河共识算法(TRC, Time-River Consensus)
web3·区块链·共识算法
OxYGC1 天前
[Web3] 基础第5篇: 共识算法与 P2P 通信协议
web3·区块链·以太坊·比特币
Bczheng16 天前
四十六.创建一笔交易(9)--第三个区块 添加tx到钱包
区块链
2301_776045237 天前
理解Layer 1区块链协议
区块链
幻智星科技7 天前
零知识证明三大核心特性解析
区块链·零知识证明
2301_776045238 天前
山寨币与比特币之间的关系
大数据·人工智能·区块链
JF_Ma9 天前
1000个量化策略第29例:经典红利低波策略
区块链
AlanAshley9 天前
实时湖仓与区块链:数据架构的信任革命
架构·区块链