三十六.区块链网络(7)--修复net与测试例子

现在进行编译,我们在main()函数,调用一下StartNode()函数,然后编译。

cpp 复制代码
int main()
{
    StartNode();
}

出现很多编译时的错误。

第一条还是printf.

1.printf

在stdio.h报, "printf": 不是 "`global namespace'" 的成员;

这是因为之前这句:

cpp 复制代码
// ==================== 解决 printf 冲突 ====================
#undef printf
#define printf ::printf
// =======================================================

stdio.h 是库的头文件,代码肯定不会错的。

这是由于加载stdio.h代码后,然后宏定义生效,把原来stdio.h里的printf都改为了::printf。

所以我们现在要去掉上面两句代码。

然后手动给所有的提示printf不明确的地方加上::,以解决两者冲突的问题。

但是我删掉后,并没有出现报错提示,可能之前是别的错误引起这个printf不明确。

现在删掉正常了。那这样最好。

2.ToString

我们来解决别的错误,CTransaction提示未定义的标识符ToString:

cpp 复制代码
  void print() const
  {
      printf("%s", ToString().c_str());
  }

这是因为我们没在CTransaction里写ToString函数,把相关函数代码复制过来就行。

然后把ToString里下级调用ToString补上就行,就是给CTxIn和CTxOut,COutPoint添加ToString函数。如有其它函数或变量,根据提示补上即可。

3.警告弃用函数

然后就是一些错误警告,就是gethostbyname之类的老函数有隐患已弃用之类的。

我们在文件最前面加上这个,取消警告:

cpp 复制代码
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#define _CRT_SECURE_NO_WARNINGS
3.CWalletTx

接着:

"std::pair<const uint256,CWalletTx>::second"使用未定义的 class"CWalletTx"

报到库文件里去了,应该是pair传入了 CWalletTx只有声明,没有定义。

我们来看一下,具体是哪里使用,采取直接注释,如果比较重要我们就完成CWalletTx的定义。

这个CWalletTx牵扯的似乎有些多,不能简单注释。

我们直接恢复CWalletTx吧,一次性解决。

在源码中找到CWalletTx类的定义,复制到main.h中,将它的父类CMerkleTx定义也一起复制过来。

然后补全CTransaction类中的IsCoinBase和GetCredit函数,因为上面两个子类调用了,最上级的父类CTransaction里的这两个函数。

CTxOut也要添加相应的GetCredit函数。

这里的CBlock类,以及其下包含的类,我们现在都不一次性复制,是因为要最小化修复。

选择用到什么加上什么。到最后它们依赖都补充的差不多了,我们再一次性复制。如果不完整的话。

好,现在编译已经通过了。接下来来解决链接时的问题(obj)。

4.htons

无法解析的外部符号 htons---util.obj

这个htons是属于#include <winsock2.h>,因为我把这个头文件放在net.cpp。

导致其它的cpp没有。

所以我们把它放回到headers.h里面,最上面,如下:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS   //设置不报安全警告
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <winsock2.h>
#include<windows.h>
#include <direct.h>
...其它头文件

这样其它文件都能认到。

5.ws2_32.lib

但是还是报有些函数:

无法解析的外部符号 __imp_accept,函数 "void __cdecl ThreadSocketHandler2(void *)" (?ThreadSocketHandler2@@YAXPEAX@Z) 中引用了该符号。

还是winsock2.h里定义的accept函数,说明根据头文件,找不到函数代码。

是因为,如果使用winsock,在编译器里要指定加载相关的lib,然后根据lib找到dll函数代码。

winsock2.h 的accpet只有声明相关。

系统不会帮你自动加载,所以我们显式加载指定就可以了。

就是这个ws2_32.lib库,headers.h里如下:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS   //设置不报安全警告
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")     
#include<windows.h>
#include <direct.h>

好,现在编译和链接都成功了,我们来看运行时会不会异常:

好,StartNode运行时不报异常,获取到IP了。获取到局域网IP。

但外网失败,应该是网络的原因,这个先不急着解决。

还有其它很多信息没显示出来,这是正常的,因为我去掉了一些功能。

所以这个StartNode是不完整的。

但网络功能已经正常了,接下来,我们可以根据情况,单独测试例子,以及开放相应的功能。

6.StartNode

从之前的报错信息来看,StartNode函数正常执行到了GetMyExternalIP函数这里了,而这里报错,并不会返回false,而是还是会往下执行,再往下就是三个线程了。

那么这三个线程应该是被启动了。

这里的GetMyExternalIP我们暂时不要,就把它注释了,然后在三个线程后加上语句输出,看是否执行到位,如下:

cpp 复制代码
 /*   if (GetMyExternalIP(addrLocalHost.ip))
    {
        addrIncoming = addrLocalHost;
        CWalletDB().WriteSetting("addrIncoming", addrIncoming);
    }*/

    //// Get addresses from IRC and advertise ours   IRC功能暂不使用
    //if (_beginthread(ThreadIRCSeed, 0, NULL) == -1)
    //    printf("Error: _beginthread(ThreadIRCSeed) failed\n"); 

    //
    // Start threads
    //
    if (_beginthread(ThreadSocketHandler, 0, new SOCKET(hListenSocket)) == -1)
    {
        strError = "Error: _beginthread(ThreadSocketHandler) failed";
        printf("%s\n", strError.c_str());
        return false;
    }

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

    if (_beginthread(ThreadMessageHandler, 0, NULL) == -1)
    {
        strError = "Error: _beginthread(ThreadMessageHandler) failed";
        printf("%s\n", strError.c_str());
        return false;
    }
    cout << "三个线程已启动" << endl;
    return true;

输出如下:

我们可以看到主程序退出了,下面的thshandler输出,是我在第一个线程ThreadSocketHandler里添加的测试语句,看函数有没有被执行。

cpp 复制代码
void ThreadSocketHandler(void* parg)
{
    while (true)
    {
        cout << "thshandler" << endl;
        Sleep(1000 * 5);
    }
7.主程序

只输出了一下就退出了,就是就是主程序任务完成了,所以会退出,我们把main()的代码改成如下,不让主程序退出就行:

cpp 复制代码
int main()
{
    StartNode();

    while (true)
    {
        Sleep(1000 * 60);
    }
}

这样运行后,第一次报错,反DB数据库相关的吧,然后第二次,却正常了,接着一直正常:

关于第一次db数据报错的问题,我想可能由于r+模式打开,由于我之前并没有调用load之类的函数创建对应数据库,或者要写入东西都数据库,写完后释放啥的(释放报错),然后第二次因为已经写入,所以不会有这个操作。又或者对数据库的操作由于某种情况,后面不再操作。

所以导致第一次有这个现象。这个问题我们先放着,因为现在也找不到。问题没出现了。

我们正常的测试就行,一个功能一个功能的测试,如果真有问题,测试到那个功能的,应该是能发现的。

8.ThreadSocketHandler2

我们看一下ThreadSocketHandler2函数,在此之前,我们先分析整理一下:

即做为一个p2p,应该有服务端的socket部分,和做为客户端的socket部分,就是服务端的socket部分在哪里创建,然后在哪里处理接收别人的连接,客户端的socket又在哪里创建,然后在哪里去连接别人。

在这里的话,服务端的socket创建是在StartNode函数开头处完成。

然后在ThreadSocketHandler2接收别人的连接,并且ThreadSocketHandler2函数统一处理所有的socket数据(服务端和客户端),你也可以看成处理所有节点的收发数据(因为是p2p)。

而客户端socket的创建是在ThreadOpenConnections2函数完成的,连接也是在这个函数里。

ThreadMessageHandler2就是处理消息。

它们通过共享vNodes协同工作,即所有连接的节点都放到vNodes里。这样就可以很方便的统一处理消息。

好,我们回到ThreadSocketHandler2函数,当这个函数启动时,意味我们已经可以接收别人的连接了。

9.连接流程

由于节点刚启动,vNodes是空的,所以在等待别人连接时(假设自己不去连接别人),如果有一个节点连接你,就会进入下面的处理代码:

cpp 复制代码
    //
    // Accept new connections
    //
    if (FD_ISSET(hListenSocket, &fdsetRecv))
    {
        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);
            pnode->AddRef();
            CRITICAL_BLOCK(cs_vNodes)
                vNodes.push_back(pnode);
        }
    }

获取连接上的socket,然后依此构建node,加入到vNodes里面。

pnode->AddRef给这个节点标记一下,表明正在被使用。不要delete。降为0会被delete。

流程简图就是下面这样:

cpp 复制代码
节点A启动
   ↓
创建 hListenSocket + listen() //StartNode函数里
   ↓
启动 ThreadSocketHandler2
   ↓
循环中:select() 监听 hListenSocket
   ↓
节点B 调用 connect() → 连 A
   ↓
A 的 select() 返回,FD_ISSET(hListenSocket) == true
   ↓
执行 accept() → 创建新 socket
   ↓
new CNode(...) → 加入 vNodes
   ↓
下次循环开始正常收发消息
10.ThreadOpenConnections2

在上面的流程中,我们看到,需要节点b连接,那这个连接代码在哪里呢?

在第二个线程ThreadOpenConnections2函数中。

但是这个函数连接节点,是先从海量节点中(加载到mapAddresses中),建立网段集合,再随机从网段中挑选一个ip。

然后才连接,而我们测试在局域网,只有两个节点。即我们将这个节点的IP写进,数据库,或对应的文本中,加载到mapAddresses中里也只有一个节点,由于这个随机机制,不一定能连接上。

这样我们很难测试,用它原来的代码的话。

所以我们在这个函数前,自写一个手动连接我们节点的代码。

在此之前,我们先来了解原代码,连接上了做了什么事,然后我们手动写的代码连接上了,也要增加这部分的写法。

关键的代码在这里:

cpp 复制代码
                CNode* pnode = ConnectNode(addrConnect);
                if (!pnode)
                    continue;
                pnode->fNetworkNode = true;

                if (addrLocalHost.IsRoutable())
                {
                    // Advertise our address
                    vector<CAddress> vAddrToSend;
                    vAddrToSend.push_back(addrLocalHost);
                    pnode->PushMessage("addr", vAddrToSend);
                }

                // Get as many addresses as we can
                pnode->PushMessage("getaddr");

                ////// should the one on the receiving end do this too?
                // Subscribe our local subscription list
                const unsigned int nHops = 0;
                for (unsigned int nChannel = 0; nChannel < pnodeLocalHost->vfSubscribe.size(); nChannel++)
                    if (pnodeLocalHost->vfSubscribe[nChannel])
                        pnode->PushMessage("subscribe", nChannel, nHops);

选中ip后,是调用ConnectNode函数进行连接的,在这之后的代码就是连接上了所做的事。

11.连接成功

客户端连接成功会干什么呢?

cpp 复制代码
      if (addrLocalHost.IsRoutable())
      {
          // Advertise our address
          vector<CAddress> vAddrToSend;
          vAddrToSend.push_back(addrLocalHost);
          pnode->PushMessage("addr", vAddrToSend);
      }

判断自身是否可路由地址,即有没有外网地址,如果有的话,就可以告诉对方节点,我的外网地址。通过发送"addr"消息。这里获取外网地址被我们注释了,所以这里可以忽略。

然后发送地址请求,即把你知道的其他节点地址尽量多发给我一些:

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

接着就是订阅消息,已弃用,忽略即可。

好,现在我们连接上后,主要的事是"getaddr"这个消息,但是我们的地址库现在也没有节点。

所以也暂时搁置。

12.第一次握手

为什么要了解上面这些消息呢,是因为两个节点建立连接时,第一次不是发送这些消息。

在这之前,第一次通信是互相发送version,确认一下版本信息协议等以便更好通信。

但是我们没看到发送version的地方,这是因为这个是在别的地方中完成的,从ConnectNode函数里找。

我们在ConnectNode函数,找到如下语句:

cpp 复制代码
 CNode* pnode = new CNode(hSocket, addrConnect, false);

当连接上后,就会根据这个socket new一个新节点。

13.version

我们看这个类的构造函数:

cpp 复制代码
    CNode(SOCKET hSocketIn, CAddress addrIn, bool fInboundIn = false)
    {
        nServices = 0;
        hSocket = hSocketIn;
        vSend.SetType(SER_NETWORK);
        vRecv.SetType(SER_NETWORK);
        nPushPos = -1;
        addr = addrIn;
        nVersion = 0;
        fClient = false; // set by version message
        fInbound = fInboundIn;
        fNetworkNode = false;
        fDisconnect = false;
        nRefCount = 0;
        nReleaseTime = 0;
        vfSubscribe.assign(256, false);

        // Push a version message
        /// when NTP implemented, change to just nTime = GetAdjustedTime()
        int64 nTime = (fInbound ? GetAdjustedTime() : GetTime());
        PushMessage("version", VERSION, nLocalServices, nTime, addr);
    }

最后一句看到了没,是先发送的version信息,而服务端也会new node,也就是说双方连接上后,就是先互相发送"version"消息。

13.version处理

那我们来看接收到version,会做什么,我们只需要到消息处理函数,看相应的"version"处理代码就行,如下:

cpp 复制代码
if (strCommand == "version")
{
    // Can only do this once
    if (pfrom->nVersion != 0)
        return false;                    // 防止同一个连接重复处理 version 消息(握手只能做一次)

    int64 nTime;
    CAddress addrMe;

    // 从接收缓冲区中解析对方发来的 version 消息字段
    vRecv >> pfrom->nVersion >> pfrom->nServices >> nTime >> addrMe;

    if (pfrom->nVersion == 0)
        return false;                    // 对方版本号无效,拒绝

    // ====================== 版本协商 ======================
    // 双方取较小的版本号作为后续通信使用的版本
    pfrom->vSend.SetVersion(min(pfrom->nVersion, VERSION));
    pfrom->vRecv.SetVersion(min(pfrom->nVersion, VERSION));

    // ====================== 判断对方类型 ======================
    // 如果对方不提供 NODE_NETWORK 服务,则认为是轻客户端(只同步区块头)
    pfrom->fClient = !(pfrom->nServices & NODE_NETWORK);
    
    if (pfrom->fClient)
    {
        // 对轻客户端只收发区块头,节省带宽
        pfrom->vSend.nType |= SER_BLOCKHEADERONLY;
        pfrom->vRecv.nType |= SER_BLOCKHEADERONLY;
    }

    // ====================== 时间同步 ======================
    // 记录对方时间,用于全网时间校准(防止时间偏差过大)
    AddTimeData(pfrom->addr.ip, nTime);

    // ====================== 请求区块链数据 ======================
    // 只向第一个连接的非轻客户端节点请求区块(避免重复请求)
    static bool fAskedForBlocks;                     // 静态变量,只执行一次
    if (!fAskedForBlocks && !pfrom->fClient)
    {
        fAskedForBlocks = true;
        // 请求从创世区块到当前最新区块的更新
        pfrom->PushMessage("getblocks", CBlockLocator(pindexBest), uint256(0));
    }

    // 打印调试信息:对方认为的我们的地址(有助于判断 NAT)
    printf("version addrMe = %s\n", addrMe.ToString().c_str());
}
14.连接测试

好,我们现在就先了解这里,做一个小例子,测试一下连接。

我们现在需要两台电脑,一台当客户端,在其下ThreadOpenConnections2函数开头处添加如下代码:指定IP连接(注意服务端不用)

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

    static int flag = 0;
    if (flag == 0)
    {
        CAddress addrConnect = CAddress("192.168.1.101", 8333);
        CNode* pnode = ConnectNode(addrConnect);
        if (pnode)
        {
            pnode->fNetworkNode = true;
            cout << "连接服务端成功!" << endl;
         //以下暂时不执行,不请求其它消息

   /*         if (addrLocalHost.IsRoutable())
            {
                vector<CAddress> vAddrToSend;
                vAddrToSend.push_back(addrLocalHost);
                pnode->PushMessage("addr", vAddrToSend);
            }

            pnode->PushMessage("getaddr");*/
            
     
        }
        flag = 1;
    }

我们利用ConnectNode函数来连接结点,这里我们直接指定IP地址。不能采取默认的方法。

因为默认算法,IP是随机抽取的。不能保证抽到我们另一台的IP。

我们只需要调用ConnectNode即可,这个函数里面,如果节点连接成功,会自动将其添加到vNodes里面,不需要我们在外面手动维护。

好,注意上面的局域地址,192.168.1.101,填你自己的被连接的那个节点地址。

注意,被连接的节点代码,需要将上面的添加的代码去掉("服务端")。

(注,这里的因为手动指定了连接,所以区分客户端和服务端,事实上,一个节点是不定的,对等的,可以是服务端也可以是客户端)

我们先运行服务端,再运行客户端。

运行结果:

客户端:

服务端:

好,连接测试成功,在下一章中,我们将详细的了解这个过程,并解释输出的信息。

然后我们将了解,是怎么通信,分享区块,验证的。

相关推荐
LedgerNinja1 小时前
WEEX API 稳定性实战:限频、重试与风控的工程化清单
运维·区块链
IvorySQL6 小时前
PG 日报|修复高 IO 并发场景,解决预读机制耗尽本地缓冲区缺陷
数据库·人工智能·sql·postgresql·区块链
北冥you鱼2 天前
OpenZeppelin Contracts 完全指南:从入门到精通,构建安全的智能合约
安全·区块链·智能合约
财迅通Ai3 天前
科源制药领投企业航脑科技布局梳理:北大实验室、EXROBOT与工作犬脑电课题
人工智能·科技·区块链·科源制药
ZDGJ60993 天前
外盘与国内期货交易品种核心区
区块链
维克兜率天4 天前
【维克】量化交易的法律边界:程序化交易和“拔网线“的区别
区块链
Web3_Daisy4 天前
什么是 Robinhood Chain?
大数据·人工智能·web3·区块链
黄焖鸡能干四碗4 天前
IT数据架构规划设计方案(PPT文件)
大数据·网络·数据库·人工智能·架构·区块链
cmes_love5 天前
CME、LME、CBOT、NYMEX等交易所外盘期货tick和分钟历史行情数据下载和分析
数据库·区块链