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

发布时间:2026/7/25 2:24:41
三十六.区块链网络(7)--修复net与测试例子 现在进行编译我们在main()函数调用一下StartNode()函数然后编译。int main() { StartNode(); }出现很多编译时的错误。第一条还是printf.1.printf在stdio.h报 printf: 不是 global namespace 的成员;这是因为之前这句// 解决 printf 冲突 #undef printf #define printf ::printf // stdio.h 是库的头文件代码肯定不会错的。这是由于加载stdio.h代码后然后宏定义生效把原来stdio.h里的printf都改为了::printf。所以我们现在要去掉上面两句代码。然后手动给所有的提示printf不明确的地方加上::以解决两者冲突的问题。但是我删掉后并没有出现报错提示可能之前是别的错误引起这个printf不明确。现在删掉正常了。那这样最好。2.ToString我们来解决别的错误CTransaction提示未定义的标识符ToStringvoid print() const { printf(%s, ToString().c_str()); }这是因为我们没在CTransaction里写ToString函数把相关函数代码复制过来就行。然后把ToString里下级调用ToString补上就行就是给CTxIn和CTxOutCOutPoint添加ToString函数。如有其它函数或变量根据提示补上即可。3.警告弃用函数然后就是一些错误警告就是gethostbyname之类的老函数有隐患已弃用之类的。我们在文件最前面加上这个取消警告:#define _WINSOCK_DEPRECATED_NO_WARNINGS #define _CRT_SECURE_NO_WARNINGS3.CWalletTx接着“std::pairconst 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里面最上面如下#define _CRT_SECURE_NO_WARNINGS //设置不报安全警告 #define _WINSOCK_DEPRECATED_NO_WARNINGS #include winsock2.h #includewindows.h #include direct.h ...其它头文件这样其它文件都能认到。5.ws2_32.lib但是还是报有些函数无法解析的外部符号 __imp_accept函数 void __cdecl ThreadSocketHandler2(void *) (?ThreadSocketHandler2YAXPEAXZ) 中引用了该符号。还是winsock2.h里定义的accept函数说明根据头文件找不到函数代码。是因为如果使用winsock在编译器里要指定加载相关的lib,然后根据lib找到dll函数代码。winsock2.h 的accpet只有声明相关。系统不会帮你自动加载所以我们显式加载指定就可以了。就是这个ws2_32.lib库headers.h里如下#define _CRT_SECURE_NO_WARNINGS //设置不报安全警告 #define _WINSOCK_DEPRECATED_NO_WARNINGS #include winsock2.h #pragma comment(lib, ws2_32.lib) #includewindows.h #include direct.h好现在编译和链接都成功了我们来看运行时会不会异常好StartNode运行时不报异常获取到IP了。获取到局域网IP。但外网失败应该是网络的原因这个先不急着解决。还有其它很多信息没显示出来这是正常的因为我去掉了一些功能。所以这个StartNode是不完整的。但网络功能已经正常了接下来我们可以根据情况单独测试例子以及开放相应的功能。6.StartNode从之前的报错信息来看StartNode函数正常执行到了GetMyExternalIP函数这里了而这里报错并不会返回false,而是还是会往下执行再往下就是三个线程了。那么这三个线程应该是被启动了。这里的GetMyExternalIP我们暂时不要就把它注释了然后在三个线程后加上语句输出看是否执行到位如下/* 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里添加的测试语句看函数有没有被执行。void ThreadSocketHandler(void* parg) { while (true) { cout thshandler endl; Sleep(1000 * 5); }7.主程序只输出了一下就退出了就是就是主程序任务完成了所以会退出我们把main()的代码改成如下不让主程序退出就行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是空的所以在等待别人连接时假设自己不去连接别人),如果有一个节点连接你就会进入下面的处理代码// // 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。流程简图就是下面这样节点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中里也只有一个节点由于这个随机机制不一定能连接上。这样我们很难测试用它原来的代码的话。所以我们在这个函数前自写一个手动连接我们节点的代码。在此之前我们先来了解原代码连接上了做了什么事然后我们手动写的代码连接上了也要增加这部分的写法。关键的代码在这里CNode* pnode ConnectNode(addrConnect); if (!pnode) continue; pnode-fNetworkNode true; if (addrLocalHost.IsRoutable()) { // Advertise our address vectorCAddress 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.连接成功客户端连接成功会干什么呢if (addrLocalHost.IsRoutable()) { // Advertise our address vectorCAddress vAddrToSend; vAddrToSend.push_back(addrLocalHost); pnode-PushMessage(addr, vAddrToSend); }判断自身是否可路由地址即有没有外网地址如果有的话就可以告诉对方节点我的外网地址。通过发送addr消息。这里获取外网地址被我们注释了所以这里可以忽略。然后发送地址请求即把你知道的其他节点地址尽量多发给我一些// Get as many addresses as we can pnode-PushMessage(getaddr);接着就是订阅消息已弃用忽略即可。好现在我们连接上后主要的事是getaddr这个消息但是我们的地址库现在也没有节点。所以也暂时搁置。12.第一次握手为什么要了解上面这些消息呢是因为两个节点建立连接时第一次不是发送这些消息。在这之前第一次通信是互相发送version,确认一下版本信息协议等以便更好通信。但是我们没看到发送version的地方这是因为这个是在别的地方中完成的从ConnectNode函数里找。我们在ConnectNode函数找到如下语句CNode* pnode new CNode(hSocket, addrConnect, false);当连接上后就会根据这个socket new一个新节点。13.version我们看这个类的构造函数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“处理代码就行如下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连接注意服务端不用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()) { vectorCAddress vAddrToSend; vAddrToSend.push_back(addrLocalHost); pnode-PushMessage(addr, vAddrToSend); } pnode-PushMessage(getaddr);*/ } flag 1; }我们利用ConnectNode函数来连接结点这里我们直接指定IP地址。不能采取默认的方法。因为默认算法IP是随机抽取的。不能保证抽到我们另一台的IP。我们只需要调用ConnectNode即可这个函数里面如果节点连接成功会自动将其添加到vNodes里面不需要我们在外面手动维护。好注意上面的局域地址192.168.1.101填你自己的被连接的那个节点地址。注意被连接的节点代码需要将上面的添加的代码去掉(“服务端”)。注这里的因为手动指定了连接所以区分客户端和服务端事实上一个节点是不定的对等的可以是服务端也可以是客户端我们先运行服务端再运行客户端。运行结果客户端服务端好连接测试成功在下一章中我们将详细的了解这个过程并解释输出的信息。然后我们将了解是怎么通信分享区块验证的。