从这一步的名字也可以看出这一步主要的内容就是导入区块,也就是从外部文件中导入区块信息,并加载到内存,更新区块索引等信息,具体代码如下。
// ********************************************************* Step 11: import blocks
if (!CheckDiskSpace() && !CheckDiskSpace(0, true))
return false;
// Either install a handler to notify us when genesis activates, or set fHaveGenesis directly.
// No locking, as this happens before any background thread is started.
if (chainActive.Tip() == nullptr) {
uiInterface.NotifyBlockTip_connect(BlockNotifyGenesisWait);
} else {
fHaveGenesis = true;
}
if (gArgs.IsArgSet("-blocknotify"))
uiInterface.NotifyBlockTip_connect(BlockNotifyCallback);
首先来看前面部分,第一行先检查磁盘空间,主要是看GetBlocksDir()
和GetDataDir()
目录下的磁盘空间是否大于nMinDiskSpace
目前也就是50MB
,但是这一句并没有十分看懂检查的意义,因为这一步似乎并没有创建新的文件。接下来判断chainActive.Tip()
是否为空,也就是判断Genesis
区块是否已经加载了,如果没有那么链接一个信号BlockNotifyGenesisWait
,到Genesis
加载成功后触发该信号;如果Genesis
已经加载了,那么就直接将fHaveGenesis
变量设置为true
。接下来是判断参数-blocknotify
是否设置了,该参数的解释如下
Execute command when the best block changes (%s in cmd is replaced by block hash).
当最佳区块(即区块头)变化时,执行该参数指定的命令,参数中的
%s
会被替换为区块哈希值。
如果设置了该参数,那么将BlockNotifyCallback
加入到信号中,以便在有新区块到来时进行回调,该函数的实现比较简单,如下
static void BlockNotifyCallback(bool initialSync, const CBlockIndex *pBlockIndex)
{
if (initialSync || !pBlockIndex)
return;
std::string strCmd = gArgs.GetArg("-blocknotify", "");
if (!strCmd.empty()) {
boost::replace_all(strCmd, "%s", pBlockIndex->GetBlockHash().GetHex());
std::thread t(runCommand, strCmd);
t.detach(); // thread runs free
}
}
主要就是替换%s
,然后创建一个线程去执行指令。
std::vector<fs::path> vImportFiles;
for (const std::string& strFile : gArgs.GetArgs("-loadblock")) {
vImportFiles.push_back(strFile);
}
threadGroup.create_thread(boost::bind(&ThreadImport, vImportFiles));
// Wait for genesis block to be processed
{
WAIT_LOCK(g_genesis_wait_mutex, lock);
// We previously could hang here if StartShutdown() is called prior to
// ThreadImport getting started, so instead we just wait on a timer to
// check ShutdownRequested() regularly.
while (!fHaveGenesis && !ShutdownRequested()) {
g_genesis_wait_cv.wait_for(lock, std::chrono::milliseconds(500));
}
uiInterface.NotifyBlockTip_disconnect(BlockNotifyGenesisWait);
}
if (ShutdownRequested()) {
return false;
}
接下来再判断-loadblock
是否设置了,该参数的解释如下
Imports blocks from external blk000??.dat file on startup.
启动时从外部文件导入区块。
该参数指定了导入区块的目录路径,指定多个路径,首先将所有的路径放入vImportFiles
变量中,然后创建一个线程去执行ThreadImport
函数,来从路径中导入所有的区块。最后,如果Genesis
区块还没有创建成功,那么就等待该区块创建,直到该区块创建成功。而在上述等待的过程中,有可能用户已经请求终止运行了,所以在等待结束之后判断用户是否请求终止运行了,如果是那么直接退出。第11步也就到此结束了。
到这一步为止,所有的初始化、缓存和索引等信息就已经全部完成了,现在就开始启动节点,开始节点之间的通信了。
// ********************************************************* Step 12: start node
int chain_active_height;
debug print
{
LOCK(cs_main);
LogPrintf("mapBlockIndex.size() = %u\n", mapBlockIndex.size());
chain_active_height = chainActive.Height();
}
LogPrintf("nBestHeight = %d\n", chain_active_height);
if (gArgs.GetBoolArg("-listenonion", DEFAULT_LISTEN_ONION))
StartTorControl();
Discover();
// Map ports with UPnP
if (gArgs.GetBoolArg("-upnp", DEFAULT_UPNP)) {
StartMapPort();
}
这几步比较简单,就是将当前最新区块的高度放在变量chain_active_height
中,然后判断是否启用了洋葱路由协议和UPnP协议,其中Discover()
是查找本机的ip地址和主机名。接下来就是一堆参数的设置,其中每个参数的含义如注释所示。
CConnman::Options connOptions;
connOptions.nLocalServices = nLocalServices; // 启用的服务标识
connOptions.nMaxConnections = nMaxConnections; // 最大连接数
connOptions.nMaxOutbound = std::min(MAX_OUTBOUND_CONNECTIONS, connOptions.nMaxConnections); // 向外发出的最多连接数
connOptions.nMaxAddnode = MAX_ADDNODE_CONNECTIONS; // 最大添加节点数
connOptions.nMaxFeeler = 1;
connOptions.nBestHeight = chain_active_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.m_added_nodes = gArgs.GetArgs("-addnode"); // 添加节点地址列表
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);
}
connOptions.vSeedNodes = gArgs.GetArgs("-seednode");
// Initiate outbound connections unless connect=0
connOptions.m_use_addrman_outgoing = !gArgs.IsArgSet("-connect");
if (!connOptions.m_use_addrman_outgoing) {
const auto connect = gArgs.GetArgs("-connect");
if (connect.size() != 1 || connect[0] != "0") {
connOptions.m_specified_outgoing = connect;
}
}
if (!connman.Start(scheduler, connOptions)) {
return false;
}
在上一步的最后,调用了一个connman.Start(scheduler, connOptions)
函数,其中scheduler
是线程调度的结构体,主要包含周期性的执行某一个函数,后一个参数connOptions
则是节点之间连接的相关信息。此函数的实现在net.cpp line 2266
,除了创建节点之间的连接之外,还创建了几个新的线程,分别是:
ThreadSocketHandler
:从socket中发送和接受消息。ThreadDNSAddressSeed
:从DNS种子中去寻找其他节点地址。ThreadOpenAddedConnections
:处理从参数传入的节点的连接。ThreadOpenConnections
:自动寻找节点进行连接。ThreadMessageHandler
:处理节点之间的消息。其中最重要的是ThreadMessageHandler
这个线程,因为节点之间消息的传输包括交易、区块等信息都是在此线程中进行处理,我们将在后面等章节来详细介绍其中的过程。
虽然上一步中有些主要的内容还没有介绍,但是我们先看最后一步的内容。
// ********************************************************* Step 13: finished
SetRPCWarmupFinished();
uiInterface.InitMessage(_("Done loading"));
g_wallet_init_interface.Start(scheduler);
return true;
首先通过一个函数去修改了一个标志变量fRPCInWarmup
将其值改为false
,接下来在最后一步调用g_wallet_init_interface.Start(scheduler)
去重复的刷新钱包,该函数的实现如下
void WalletInit::Start(CScheduler& scheduler) const
{
for (const std::shared_ptr<CWallet>& pwallet : GetWallets()) {
pwallet->postInitProcess();
}
// Run a thread to flush wallet periodically
scheduler.scheduleEvery(MaybeCompactWalletDB, 500);
}
在函数MaybeCompactWalletDB
中不断的去查询钱包的状态,如果有任何的变化泽及时更新钱包的状态。
在之前的十三个步骤当中,主要介绍了可执行程序bitcoind
所有的命令行参数,以及这些参数的设置和运用,同时还包括了索引和缓存的创建、创世区块Genesis
的创建、节点的启动等内容,这些步骤所处理的仅仅只是非核心的功能,但同时也是一个完整的系统必不可少的功能。这些基本步骤的介绍就到此结束,后续的文章将围绕Step 12
最后创建的几个线程来展开分析,核心功能为节点之间进行通信的过程。