我们解决上一章的两个问题:
在LoadAddresses加载addr.txt,把里面的地址添加到addr.dat数据库里。
会调用AddAddress函数进行添加。
1.IsRoutable
而这个函数,开头这句:
cpp
bool AddAddress(CAddrDB& addrdb, const CAddress& addr)
{
if (!addr.IsRoutable())
return false;
我们要暂时注释,因为如果是192.168的地址,他就会返回false,被屏蔽了。无法添加到数据库里,因为这个数据库里的地址是要分享给其他节点的,你不可能分享局域网地址。
2.ConnectNode
然后在是连接上面,选中IP后,就会调用ConnectNode进行连接。
如果连接成功,它就会依此创建一个节点:
cpp
// Add node
CNode* pnode = new CNode(hSocket, addrConnect, false);
这个CNode构造函数代码我们来看一下:
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信息过去。
3.getblocks
这里还没问题,然后我们来看对方收到version会怎么处理:
cpp
if (strCommand == "version")
{
// Can only do this once
if (pfrom->nVersion != 0)
return false;
int64 nTime;
CAddress addrMe;
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));
pfrom->fClient = !(pfrom->nServices & NODE_NETWORK);
if (pfrom->fClient)
{
pfrom->vSend.nType |= SER_BLOCKHEADERONLY;
pfrom->vRecv.nType |= SER_BLOCKHEADERONLY;
}
AddTimeData(pfrom->addr.ip, nTime);
// Ask the first connected node for block updates
static bool fAskedForBlocks;
if (!fAskedForBlocks && !pfrom->fClient)
{
fAskedForBlocks = true;
pfrom->PushMessage("getblocks", CBlockLocator(pindexBest), uint256(0));
}
printf("version addrMe = %s\n", addrMe.ToString().c_str());
}
看到最后的,getblocks消息了吗?我们这里不是轻客户端,然后也是第一次连接。所以会发送getblocks消息请求块同步。
然后做为服务端,同样的,在ThreadSocketHandler2函数里,这部分代码:
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);
}
}
接收到一个客户端连接后,他同样会为这个ip新建一个节点。也同样会发送version,然后对方发送getblocks请求同步区块。
也就是说,当两个节点连接上后,开始会自动互相发送版本号,然后各自请求同步区块,至于谁同步谁,那当然看后面的处理算法。
好,因为后续功能未修复,所以我们得取消同步,注释掉CNode构造函数里发送version语句是可以的。但我们还是希望有个版本号沟通。所以我们选择注释掉发送getblocks消息,即在version消息处理处,这句:
cpp
pfrom->PushMessage("getblocks", CBlockLocator(pindexBest), uint256(0));
4.测试连接
注释掉之后,我们现在有区块的节点,就称之为a结点吧,然后把项目复制到另一台电脑。
只复制项目,相关的区块数据文件不要复制过去。
取名为b节点,我们在b节点的addr.txt填上a节点的ip地址和端口。
然后先运行a节点,接着运行b节点,我们来测试一下连接。
主函数代码如下:
cpp
void Init2()
{
LoadAddresses(); //加载地址库--mapAddresses
LoadBlockIndex(); //加载区块索引--mapBlockIndex等
LoadWallet(); //加载钱包账号 tx--mapPubKeys、mapWallet等
ReacceptWalletTransactions();//将钱包未确认交易重新加入池子
StartNode();//启动网络节点
}
int main()
{
Init2();
while (true)
{
Sleep(1000 * 60);
}
}
先启动a节点,输出如下:
cpp
sending: version (46 bytes)
dbenv.open strAppDir=c:\test\btc
mapAddresses:
-----
LoadBlockIndex(): hashBestChain=00000eca18a942 height=5
fGenerateBitcoins = 0
nTransactionFee = 0
addrLocalHost = 192.168.2.101:8333
bound to addrLocalHost = 192.168.2.101:8333
ThreadMessageHandler started
ThreadSocketHandler started
ThreadOpenConnections started
RandAddSeed() got 120 bytes of performance data
在b节点建立addr.txt,填入192.168.2.103:8333
启动后如下:
cpp
sending: version (46 bytes)
dbenv.open strAppDir=c:\test\btc
mapAddresses:
-----
00000b60c6943717840ec4df7c4fc9386d9c81babf3d293761e7c31a6ae04e6b
merkleroot
a564aab2c6e83f5f36b08a0a9a7a30c1fa76f662f806a4b5c431b77497e5a31d
00000b60c6943717840ec4df7c4fc9386d9c81babf3d293761e7c31a6ae04e6b
0x74C52104B5BD4D544AB52348D991EEF12FF2869CD2F19578DC72AE668445A9050EFAF5793A4907615C8876F674CBC2B2FE225837832D0654957A9191C0BE9C6604 OP_CHECKSIG
CBlock(hash=00000b60c69437, ver=1, hashPrevBlock=00000000000000, hashMerkleRoot=a564aa, nTime=1792561819, nBits=1f000fff, nNonce=272822, vtx=1)
CTransaction(hash=a564aa, ver=1, vin.size=1, vout.size=1, nLockTime=0)
CTxIn(COutPoint(000000, -1), coinbase 04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73)
CTxOut(nValue=50.00000000, scriptPubKey=0x74C52104B5BD4D544AB523)
vMerkleTree: a564aa
AddToBlockIndex: new best=00000b60c69437 height=0
fGenerateBitcoins = 0
nTransactionFee = 0
addrLocalHost = 192.168.2.106:8333
bound to addrLocalHost = 192.168.2.106:8333
ThreadMessageHandler started
ThreadSocketHandler started
ThreadOpenConnections started
RandAddSeed() got 120 bytes of performance data
这里输出创世块信息,是因为创世块是写死的,每一个节点运行,如果没有都会固定生成这个块。
5.解决错误
好,我们看后面没有连接上,我们需要调试一下b节点的代码,看问题出在哪。
我们直接在b节点,Init2()后面,加上这句:
cpp
cout<<mapAddressese.size()<<endl;
发现大小为0,可见addr.txt里的地址没有写入到数据库。应该不是读数据库问题。
我在AddAddress函数开头增加输出,并没有被执行。
奇怪没读到addr.txt文件?
在fets后,加输出也没被执行。
最终定位:
cpp
CAutoFile filein=fopen("addr.txt","rt")
filein为假。没找到目录。
我直接运行x64 debug下的exe,就正常建立连接了。因为我的文件放在这个目录下。
6.找到目录
难道调试模式输出的不是这个目录?
这是因为EXE所在目录不等于调试时的工作目录。
我们可以改工作目录设置,也可以放到正确的工具目录,就是把addr.txty放到项目根目录,跟.sln同级目录下。
7.输出信息
这次编译运行后,成功建立连接:
cpp
sending: version (46 bytes)
dbenv.open strAppDir=c:\test\btc
mapAddresses:
CAddress(192.168.2.101:8333)
-----
LoadBlockIndex(): hashBestChain=00000b60c69437 height=0
fGenerateBitcoins = 0
nTransactionFee = 0
addrLocalHost = 192.168.2.106:8333
bound to addrLocalHost = 192.168.2.106:8333
1
ThreadSocketHandler started
ThreadOpenConnections started
ThreadMessageHandler started
RandAddSeed() got 120 bytes of performance data
trying 192.168.2.101:8333
connected 192.168.2.101:8333
sending: version (46 bytes)
ProcessMessages(66 bytes)
received: version (46 bytes) 71 11 01 00 01 00 00 00 00 00 00 00 cb 37 f4 6a 00 00 00 00 00 00 00 00 00
Added time data, samples 2, ip 6502a8c0, offset -1 (+0 minutes)
version addrMe = 192.168.2.106:53807
服务端a节点后续输出:
cpp
sending: version (46 bytes)
dbenv.open strAppDir=c:\test\btc
mapAddresses:
-----
LoadBlockIndex(): hashBestChain=00000eca18a942 height=5
fGenerateBitcoins = 0
nTransactionFee = 0
addrLocalHost = 192.168.2.101:8333
bound to addrLocalHost = 192.168.2.101:8333
ThreadMessageHandler started
ThreadSocketHandler started
RandAddSeed() got 120 bytes of performance data
ThreadOpenConnections started
accepted connection from 192.168.2.106:53807
sending: version (46 bytes)
ProcessMessages(66 bytes)
received: version (46 bytes) 71 11 01 00 01 00 00 00 00 00 00 00 cc 37 f4 6a 00 00 00 00 01 00 00 00 00
Added time data, samples 2, ip 6a02a8c0, offset +1 (+0 minutes)
version addrMe = 192.168.2.101:8333
好,现在已经能正常建立连接了,并互发了版本号。
这里是怎么判断掉线的呢,没有其他的设置,就是默认的处理方式。
当一个节点关闭时,socket本身就会发消息。
然后在ThreadSocketHandler2函数中
cpp
if (nBytes == 0)
{
// socket closed gracefully
if (!pnode->fDisconnect)
printf("recv: socket closed\n");
pnode->fDisconnect = true;
}
else if (nBytes < 0)
{
// socket error
int nErr = WSAGetLastError();
if (nErr != WSAEWOULDBLOCK && nErr != WSAEMSGSIZE && nErr != WSAEINTR && nErr != WSAEINPROGRESS)
{
if (!pnode->fDisconnect)
printf("recv failed: %d\n", nErr);
pnode->fDisconnect = true;
}
}
recv就会返回0,这是正常关闭,完成了关闭流程,比如调用相应关闭函数等。
如果小于0,异常关闭,大概是直接结束进程之类的。
比如我窗口关闭b节点,a节点会有这个输出:
cpp
recv failed: 10054
disconnecting node 192.168.2.106:53807