现在进行编译,我们在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,填你自己的被连接的那个节点地址。
注意,被连接的节点代码,需要将上面的添加的代码去掉("服务端")。
(注,这里的因为手动指定了连接,所以区分客户端和服务端,事实上,一个节点是不定的,对等的,可以是服务端也可以是客户端)
我们先运行服务端,再运行客户端。
运行结果:
客户端:

服务端:

好,连接测试成功,在下一章中,我们将详细的了解这个过程,并解释输出的信息。
然后我们将了解,是怎么通信,分享区块,验证的。