九、初始化和启动模块(8)
6. 第八步:加载钱包
这一步是一个条件编译语句:
// ********************* 第八步:加载钱包
#ifdef ENABLE_WALLET
if (!CWallet::InitLoadWallet())
return false;
#else
LogPrintf("No wallet support compiled in!\n");
#endif
与第五步:验证钱包数据库的完整性的编译条件相同,ENABLE_WALLET
宏定义默认为1,即打开钱包功能,除非在编译时输入关闭钱包命令参数。
这个部分最主要是调用CWallet::InitLoadWallet()
函数,这个函数的声明在wallet/wallet.h的1107行,实现在wallet/wallet.cpp的4133-4149行:
bool CWallet::InitLoadWallet()
{
if (gArgs.GetBoolArg("-disablewallet", DEFAULT_DISABLE_WALLET)) {
LogPrintf("Wallet disabled!\n");
return true;
}
for (const std::string& walletFile : gArgs.GetArgs("-wallet")) {
CWallet * const pwallet = CreateWalletFromFile(walletFile);
if (!pwallet) {
return false;
}
vpwallets.push_back(pwallet);
}
return true;
}
该函数首先判断是否在启动程序时添加了-disablewallet
参数命令,如果有该命令,就不用加载钱包了。
然后如果没有该命令,就使用一个for循环语句加载钱包:for循环开始是遍历-wallet
参数数组后面的元素,一次赋值给walletFile
变量;然后用该变量当作输入调用CreateWalletFromFile()
函数来加载钱包;
///////////////////////////////////////////////////////
CreateWalletFromFile()
函数是加载钱包的核心函数,该函数在wallet/wallet.h的1106行声明,在wallet/wallet.cpp的3940-4131行实现,这个函数是初始化钱包的一系列操作,在发生错误是会返回一个钱包的例子或者一个空指针。
///////////////////////////////////////////////////////
并把加载的钱包拷贝添加到vpwallets
(是一个vector
类变量)容器的后面。
********************************************
第八步总结:
这一步很简单,就是用一个条件编译语句,判断如果启动钱包功能,则加载钱包。
********************************************
7. 第九步:数据目录维护
// **********************第九步: 数据目录维护
// 如果打开了修剪模式,则在任何钱包重新扫描后,不设置NODE_NETWORK 并执行初始区块存储的修剪。
if (fPruneMode) {
LogPrintf("Unsetting NODE_NETWORK on prune mode\n");
nLocalServices = ServiceFlags(nLocalServices & ~NODE_NETWORK);
if (!fReindex) {
uiInterface.InitMessage(_("Pruning blockstore..."));
PruneAndFlush();
}
}
if (chainparams.GetConsensus().vDeployments[Consensus::DEPLOYMENT_SEGWIT].nTimeout != 0) {
// 只有在有合理的开始时间的情况下才告知具有隔离见证的权利。
// 这允许我们通过将代码结束时间设置为0来合并没有定义的软分叉的代码。
// 请注意,不需要设置NODE_WITNESS:
// 不设置它的唯一的缺点是,在激活之后,没有升级的节点将会从你那里获取。
nLocalServices = ServiceFlags(nLocalServices | NODE_WITNESS);
// 如果定义了一个软分叉,则只关注提供隔离见证能力的其它节点。
nRelevantServices = ServiceFlags(nRelevantServices | NODE_WITNESS);
}
这部分的代码分为两部分:
①修剪区块存储数据
这是在第一个if判断语句中实现,判断条件是:是否打开了修剪模式(由fPruneMode
的值确定)?在打开了修剪模式的情况下,在日志文件中输出“在修剪模式下不设置NODE_NETWORK
”日志;然后判断是否重建链状态和块索引(由fReindex
的值确定)?如果没有重索引,则会进行区块存储的修剪。修剪功能由PruneAndFlush()
函数实现,该函数声明在validation.h的299行,实现在validation.cpp的1998-2003行,这个函数主要是修剪区块文件并刷新状态到磁盘。
②区块隔离见证的部署
第二个if判断语句中两个赋值语句,主要都是关于隔离见证部署问题的。
有关隔离见证的讲解可以参考:
********************************************
第九步总结:
这个部分是关于数据目录的维护的,牵涉到比特币的区块数据结构:修剪区块存储数据和隔离见证在区块中的部署。其中此版本的比特币核心是默认使用隔离见证的。
********************************************
8. 第十步:导入数据块
// ********************第十步:导入数据块
if (!CheckDiskSpace())
return false;
// 要么安装处理程序当同源链激活新区块的时候通知我们,要么直接设置fHaveGenesis为true。
// 没有锁定,因为这发生在任何后台线程开始之前。
if (chainActive.Tip() == nullptr) {
uiInterface.NotifyBlockTip.connect(BlockNotifyGenesisWait);
} else {
fHaveGenesis = true;
}
if (gArgs.IsArgSet("-blocknotify"))
uiInterface.NotifyBlockTip.connect(BlockNotifyCallback);
std::vector<fs::path> vImportFiles;
for (const std::string& strFile : gArgs.GetArgs("-loadblock")) {
vImportFiles.push_back(strFile);
}
threadGroup.create_thread(boost::bind(&ThreadImport, vImportFiles));
// 等待生成块被处理
{
boost::unique_lock<boost::mutex> lock(cs_GenesisWait);
while (!fHaveGenesis) {
condvar_GenesisWait.wait(lock);
}
uiInterface.NotifyBlockTip.disconnect(BlockNotifyGenesisWait);
}
①检查磁盘可用空间
这是由CheckDiskSpace()
函数检查,该函数的声明在validation.h的255行,实现在validation.cpp的3430-3439行,这里该函数的作用是:检查磁盘可用空间,如果空闲空间小于nMinDiskSpace
(默认为50MB),则会报错。
②通知激活的新区块
这是个if-else语句,判断条件是:Tip()
函数返回的该链的激活块的索引,如果是一个空指针,则表示该区块没有被同步,会连接到BlockNotifyGenesisWait
;否则fHaveGenesis
直接设置成true。
③最佳块改变时的操作
此时会有一个参数的设置值的判断,该参数是:-blocknotify
,该参数在帮助文件中的解释是:当最佳区块改变时执行命令。如果最佳区块被更改,则连接回叫区块通知BlockNotifyCallback
函数。
④从外部导入区块数据
这在一个for循环中进行,由参数-loadblock
的文件中依次导入区块数据到vector类容器变量vImportFiles
中。然后创建一个线程用来捆绑该容器。-loadblock
参数在帮助文件中的解释为:在启动时从外部blk000???.dat文件导入块,即导入的块数据在外部的blk000???.dat样式的文件中。
⑤等待处理生成块
当有需要处理的生成块时(fHaveGenesis = false
),使通知处于等待状态。当处理完生成块后断开BlockNotifyGenesisWait
的连接。
********************************************
第十步总结:
此部分是节点导入数据块,使区块更新最新认证区块。首先会检查磁盘空间是否可容下新数据块;然后收到有新区块产生的通知;当最佳区块受到攻击改变时会有回叫通知的操作;然后从外部导入新的数据块;当有生成块需要处理时,会使通知处于等待状态,直到处理完,然后断开通知的连接。
********************************************
9. 第十一步:启动节点服务
// ***************第十一步:启动节点服务(start node)
// 调试日志打印
LogPrintf("mapBlockIndex.size() = %u\n", mapBlockIndex.size());
LogPrintf("nBestHeight = %d\n", chainActive.Height());
if (gArgs.GetBoolArg("-listenonion", DEFAULT_LISTEN_ONION))
StartTorControl(threadGroup, scheduler);
Discover(threadGroup);
// 使用UPnP映射端口
MapPort(gArgs.GetBoolArg("-upnp", DEFAULT_UPNP));
CConnman::Options connOptions;
connOptions.nLocalServices = nLocalServices;
connOptions.nRelevantServices = nRelevantServices;
connOptions.nMaxConnections = nMaxConnections;
connOptions.nMaxOutbound = std::min(MAX_OUTBOUND_CONNECTIONS, connOptions.nMaxConnections);
connOptions.nMaxAddnode = MAX_ADDNODE_CONNECTIONS;
connOptions.nMaxFeeler = 1;
connOptions.nBestHeight = chainActive.Height();
connOptions.uiInterface = &uiInterface;
connOptions.m_msgproc = peerLogic.get();
connOptions.nSendBufferMaxSize = 1000*gArgs.GetArg("-maxsendbuffer", DEFAULT_MAXSENDBUFFER);
connOptions.nReceiveFloodSize = 1000*gArgs.GetArg("-maxreceivebuffer", DEFAULT_MAXRECEIVEBUFFER);
connOptions.nMaxOutboundTimeframe = nMaxOutboundTimeframe;
connOptions.nMaxOutboundLimit = nMaxOutboundLimit;
for (const std::string& strBind : gArgs.GetArgs("-bind")) {
CService addrBind;
if (!Lookup(strBind.c_str(), addrBind, GetListenPort(), false)) {
return InitError(ResolveErrMsg("bind", strBind));
}
connOptions.vBinds.push_back(addrBind);
}
for (const std::string& strBind : gArgs.GetArgs("-whitebind")) {
CService addrBind;
if (!Lookup(strBind.c_str(), addrBind, 0, false)) {
return InitError(ResolveErrMsg("whitebind", strBind));
}
if (addrBind.GetPort() == 0) {
return InitError(strprintf(_("Need to specify a port with -whitebind: '%s'"), strBind));
}
connOptions.vWhiteBinds.push_back(addrBind);
}
for (const auto& net : gArgs.GetArgs("-whitelist")) {
CSubNet subnet;
LookupSubNet(net.c_str(), subnet);
if (!subnet.IsValid())
return InitError(strprintf(_("Invalid netmask specified in -whitelist: '%s'"), net));
connOptions.vWhitelistedRange.push_back(subnet);
}
if (gArgs.IsArgSet("-seednode")) {
connOptions.vSeedNodes = gArgs.GetArgs("-seednode");
}
if (!connman.Start(scheduler, connOptions)) {
return false;
}
这一步为start node
,我们在第六步:网络初始化最开始的注释也了解了,第六步是基本的网络初始化,而这一步是真正的网络连接。
9-1 打印调试日志
最开始会打印两个调试日志,在日志文件中打印块索引映射的元素个数(mapBlockIndex.size
)和活动链的区块高度(nBestHeight
)。
9-2 自动创建Tor隐藏服务
if (gArgs.GetBoolArg("-listenonion", DEFAULT_LISTEN_ONION))
StartTorControl(threadGroup, scheduler);
上面的代码表示读取是否有设置-listenonion
,如果有设置则执行StartTorControl()
函数,该函数是与Tor沟通的功能中的启动Tor控制的函数;Tor是第二代洋葱路由(onion routing)的一种实现,用户通过Tor可以在因特网上进行匿名交流。如果没有设置-listenonion
参数,则默认不打开该功能,则不执行StartTorControl()
函数。
9-3 发现线程
Discover(threadGroup);
该段代码主要是Discover()
发现线程函数,该函数在这里面主要是发现与本地节点相连接的其他节点。
9-4 映射upnp设备
// Map ports with UPnP
MapPort(gArgs.GetBoolArg("-upnp", DEFAULT_UPNP));
该部分就是检测-upnp
参数,如果有设置会用MapPort()
来映射upnp设备。UPnP 是各种各样的智能设备、无线设备和个人电脑等实现遍布全球的对等网络的结构。
9-5 初始化connOptions
对象,以启动节点服务
第十一步剩下的代码都是和connOptions
有关的参数设置,connOptions
是CConnman
类中Options
结构体对象(CConnman
类在net.h的119行)。
这里要单独说说CConnman
类,根据它的命名可以知道它和“Connman”有关,而在百度百科中“Connman”的简介是:
ConnMan(Connection Manager)是一个开源项目,在Linux操作系统中提供一个后台进程,来管理网络连接。ConnMan设计小巧,并且尽可能的减小资源消耗,因此他能很容易的集成进其他平台。
可以知道该类是关于管理网络连接的后台进程,该类中的结构体Options
就是管理网络连接的选项参数了;而connOptions
是Options
的对象,即connOptions
就是管理网络连接选项参数的初始化。在设置参数时的很多值在前面的过程中已经提到过,而且有相应的赋值,并且那些没提到过的参数使用默认值。但这些网络连接参数设置完成,也代表着节点的网络连接完成,所以把该部分叫做“start node”。
********************************************
第十一步总结:
该部分是启动节点服务阶段,也是真正的网络连接完成阶段。在该部分开始前会打印两个调试日志,分别是索引映射的元素个数和活动链的区块高度;然后是创建Tor隐藏服务、发现与本地节点相连接的其他节点、映射upnp设备;最后是管理网络连接选项参数的设定。
********************************************
10. 第十二步:完成
// ***************第十二步:完成
SetRPCWarmupFinished();
uiInterface.InitMessage(_("Done loading"));
#ifdef ENABLE_WALLET
for (CWalletRef pwallet : vpwallets) {
pwallet->postInitProcess(scheduler);
}
#endif
return !fRequestShutdown;
}
10.1 标记RPC准备工作完成
在开始会出现SetRPCWarmupFinished()
函数,该函数在/server.h中声明,在/server.cpp中实现。在声明中对该函数的解释为:
/* Mark warmup as done. RPC calls will be processed from now on. /
/标记准备工作的完成。从现在开始将处理RPC调用。*/
该函数的实现为:
void SetRPCWarmupFinished()
{
LOCK(cs_rpcWarmup);
assert(fRPCInWarmup);
fRPCInWarmup = false;
}
也就是锁定cs_rpcWarmup
,把fRPCInWarmup
设置成false
。
10.2 显示加载完成
uiInterface.InitMessage(_("Done loading")
这行代码功能就是在界面显示"Done loading"(加载完成),写入日志文件中。
10.3 重新接受钱包交易
postInitProcess()
函数在wallet.h的1113行定义,对该函数的注释为:
- 电子钱包初始化后设置
- 为钱包提供注册重复任务并完成初始化后任务的机会
该部分的功能是把钱包交易中的交易添加到内存池中。
********************************************
第十二步总结:
该部分是整个初始化的最后步骤,标志着初始化的完成。主要包含RPC开始工作、显示加载完成和重新接收钱包的交易工作。至此初始化最重要的函数AppInitMain()
就全部完成了。
********************************************
(十五)继续看bitcoind.cpp中main函数前面的最后部分,它在bitcoind.cpp的170-186行
catch (const std::exception& e) {
PrintExceptionContinue(&e, "AppInit()");
} catch (...) {
PrintExceptionContinue(nullptr, "AppInit()");
}
if (!fRet)
{
Interrupt(threadGroup);
threadGroup.join_all();
} else {
WaitForShutdown(&threadGroup);
}
Shutdown();
return fRet;
}
1. 捕捉并抛出异常
catch (const std::exception& e) {
PrintExceptionContinue(&e, "AppInit()");
} catch (...) {
PrintExceptionContinue(nullptr, "AppInit()");
}
这部分其实try/catch()/catch(...))
语句的抛出异常部分,这是处理会有异常程序的良好的编程习惯。主要是对初始化过程中的异常进行捕捉并处理。
2. 初始化失败的处理方式
我们知道fRet = AppInitMain(threadGroup, scheduler);
而且当初始化函数AppInitMain()
正常成功时,会返回true
,此处用if/else
语句是当初始化未顺利完成(返回false,并且不是异常)时,会打开一个中断线程Interrupt(threadGroup);
;然后会有个threadGroup.join_all();
命令,该命令在这里被故意忽略了,因为我们没有重新测试所有的启动失败情况,以确保它们在启动的情况下不会因某些线程阻塞等待另一个线程而导致挂起。
3. 等待关闭消息
这是在else
部分的WaitForShutdown()
函数控制的,该函数在bitcoind.cpp的43-57行定义,主要是循环检测关闭命令。
4. 关闭程序
Shutdown();
这个很简单,就是关闭AppInit()
程序。
***************************************
到这里bitcoind.cpp中的main函数就全部介绍完了,主要是比特币客户端的初始化工作。也代表着初始化和启动阶段的完成,剩下阶段主要是分析各个比特币运行时的模块的运行机理,并会尽量结合代码简单介绍下工作流程。