從零開始學習比特幣開發(四)--網路初始化,載入區塊鏈和錢包,匯入區塊啟動節點
寫在前面:
本篇文章接續
從零開始學習區塊鏈技術(三)-接入比特幣網路的關鍵步驟解析、建立比特幣錢包,以及重要rpc指令
從零開始學習區塊鏈技術(二)–如何接入比特幣網路以及其原理分析
以及從零開始學習區塊鏈技術(一)–從原始碼編譯比特幣
如果這篇文章看不明白,請務必先閱讀之前的文章。
第6步,網路初始化(src/init.cpp::AppInitMain()
)
-
生成智慧指標物件 g_connman,型別為
CConnman
。g_connman = std::unique_ptr<CConnman>(new CConnman(GetRand(std::numeric_limits<uint64_t>::max()), GetRand(std::numeric_limits<uint64_t>::max()))); CConnman& connman = *g_connman;
-
生成智慧指標物件 peerLogic,型別為
PeerLogicValidation
。peerLogic.reset(new PeerLogicValidation(&connman, scheduler, gArgs.GetBoolArg("-enablebip61", DEFAULT_ENABLE_BIP61)));
PeerLogicValidation 繼承了 CValidationInterface、NetEventsInterface 兩個類。實現 CValidationInterface 這個類可以訂閱驗證過程中產生的事件。實現 NetEventsInterface 這個類可以處理訊息網路訊息。
-
註冊各種驗證處理器,即訊號處理器,在傳送訊號時會呼叫這些處理器。
RegisterValidationInterface(peerLogic.get());
方法具體實現如下:
void RegisterValidationInterface(CValidationInterface* pwalletIn) { g_signals.m_internals->UpdatedBlockTip.connect(boost::bind(&CValidationInterface::UpdatedBlockTip, pwalletIn, _1, _2, _3)); g_signals.m_internals->TransactionAddedToMempool.connect(boost::bind(&CValidationInterface::TransactionAddedToMempool, pwalletIn, _1)); g_signals.m_internals->BlockConnected.connect(boost::bind(&CValidationInterface::BlockConnected, pwalletIn, _1, _2, _3)); g_signals.m_internals->BlockDisconnected.connect(boost::bind(&CValidationInterface::BlockDisconnected, pwalletIn, _1)); g_signals.m_internals->TransactionRemovedFromMempool.connect(boost::bind(&CValidationInterface::TransactionRemovedFromMempool, pwalletIn, _1)); g_signals.m_internals->ChainStateFlushed.connect(boost::bind(&CValidationInterface::ChainStateFlushed, pwalletIn, _1)); g_signals.m_internals->Broadcast.connect(boost::bind(&CValidationInterface::ResendWalletTransactions, pwalletIn, _1, _2)); g_signals.m_internals->BlockChecked.connect(boost::bind(&CValidationInterface::BlockChecked, pwalletIn, _1, _2)); g_signals.m_internals->NewPoWValidBlock.connect(boost::bind(&CValidationInterface::NewPoWValidBlock, pwalletIn, _1, _2)); }
靜態變數 g_signals 在程式啟動前生成,m_internals 在第4a 步應用程式初始化過程中生成。
-
根據命令列引數
-uacomment
,處理追加到使用者代理的字串。std::vector<std::string> uacomments; for (const std::string& cmt : gArgs.GetArgs("-uacomment")) { if (cmt != SanitizeString(cmt, SAFE_CHARS_UA_COMMENT)) return InitError(strprintf(_("User Agent comment (%s) contains unsafe characters."), cmt)); uacomments.push_back(cmt); }
-
構造並檢查版本字串長度是否大於
version
訊息中版本的最大長度。strSubVersion = FormatSubVersion(CLIENT_NAME, CLIENT_VERSION, uacomments); if (strSubVersion.size() > MAX_SUBVERSION_LENGTH) { return InitError(strprintf(_("Total length of network version string (%i) exceeds maximum length (%i). Reduce the number or size of uacomments."), strSubVersion.size(), MAX_SUBVERSION_LENGTH)); }
-
如果指定了
onlynet
引數,則設定可以對接進行連線的型別,比如:ipv4、ipv6、onion。if (gArgs.IsArgSet("-onlynet")) { std::set<enum Network> nets; for (const std::string& snet : gArgs.GetArgs("-onlynet")) { enum Network net = ParseNetwork(snet); if (net == NET_UNROUTABLE) return InitError(strprintf(_("Unknown network specified in -onlynet: '%s'"), snet)); nets.insert(net); } for (int n = 0; n < NET_MAX; n++) { enum Network net = (enum Network)n; if (!nets.count(net)) SetLimited(net); } }
上面的程式碼首先把
-onlynet
引數指定的只允許對外連線的網路型別加入集合中,然後進行 for 遍歷,如果當前的型別不在允許的集合中,則呼叫SetLimited
方法,設定這些型別為受限的。 -
獲取是否允許進行 DNS 查詢,是否進行代理隨機
fNameLookup = gArgs.GetBoolArg("-dns", DEFAULT_NAME_LOOKUP); bool proxyRandomize = gArgs.GetBoolArg("-proxyrandomize", DEFAULT_PROXYRANDOMIZE);
兩者預設都為真。
-
處理網路代理。
如果指定了
-proxy
,且不等於 0,即指定了代理地址,進行下面的處理:- 呼叫
Lookup
方法,根據指定的代理,通過 DNS查詢,發現代理伺服器的地址。 - 生成 proxyType 物件。
- 設定 IPv4、IPv6、Tor 網路的代理。
- 設定命名(域名)代理。
- 設定不限制連線到 Tor 網路。
具體程式碼如下:
std::string proxyArg = gArgs.GetArg("-proxy", ""); SetLimited(NET_ONION); if (proxyArg != "" && proxyArg != "0") { CService proxyAddr; if (!Lookup(proxyArg.c_str(), proxyAddr, 9050, fNameLookup)) { return InitError(strprintf(_("Invalid -proxy address or hostname: '%s'"), proxyArg)); } proxyType addrProxy = proxyType(proxyAddr, proxyRandomize); if (!addrProxy.IsValid()) return InitError(strprintf(_("Invalid -proxy address or hostname: '%s'"), proxyArg)); SetProxy(NET_IPV4, addrProxy); SetProxy(NET_IPV6, addrProxy); SetProxy(NET_ONION, addrProxy); SetNameProxy(addrProxy); SetLimited(NET_ONION, false); // by default, -proxy sets onion as reachable, unless -noonion later }
- 呼叫
-
處理洋蔥網路。 如果指定了
onion
引數,則處理洋蔥網路的相關設定。如果指定了
-onion
,且不等於空字串,即指定了洋蔥代理地址,進行下面的處理:- 如果引數等於 0,設定洋蔥網路受限,即不可達。否則,進行下面的處理。
- 呼叫
Lookup
方法,根據指定的代理,通過 DNS查詢,發現代理伺服器的地址。 - 生成 proxyType 物件。
- 設定 Tor 網路的代理。
- 設定不限制連線到 Tor 網路。
具體程式碼如下:
std::string onionArg = gArgs.GetArg("-onion", ""); if (onionArg != "") { if (onionArg == "0") { // Handle -noonion/-onion=0 SetLimited(NET_ONION); // set onions as unreachable } else { CService onionProxy; if (!Lookup(onionArg.c_str(), onionProxy, 9050, fNameLookup)) { return InitError(strprintf(_("Invalid -onion address or hostname: '%s'"), onionArg)); } proxyType addrOnion = proxyType(onionProxy, proxyRandomize); if (!addrOnion.IsValid()) return InitError(strprintf(_("Invalid -onion address or hostname: '%s'"), onionArg)); SetProxy(NET_ONION, addrOnion); SetLimited(NET_ONION, false); } }
-
處理通過
-externalip
引數設定的外部 IP地址。獲取並遍歷所有指定的外部地址,進行如下處理:呼叫
Lookup
方法進行DNS 查詢。如果成功則呼叫AddLocal
方法,儲存新的地址。否則,丟擲初始化錯誤。for (const std::string& strAddr : gArgs.GetArgs("-externalip")) { CService addrLocal; if (Lookup(strAddr.c_str(), addrLocal, GetListenPort(), fNameLookup) && addrLocal.IsValid()) AddLocal(addrLocal, LOCAL_MANUAL); else return InitError(ResolveErrMsg("externalip", strAddr)); }
-
如果設定了
maxuploadtarget
引數,則設定最大出站限制。if (gArgs.IsArgSet("-maxuploadtarget")) { nMaxOutboundLimit = gArgs.GetArg("-maxuploadtarget", DEFAULT_MAX_UPLOAD_TARGET)*1024*1024; }
第7步,載入區塊鏈(src/init.cpp::AppInitMain()
)
首先,計算快取的大小。包括:區塊索引資料庫、區塊狀態資料庫、記憶體中 UTXO 集。程式碼如下:
fReindex = gArgs.GetBoolArg("-reindex", false);
bool fReindexChainState = gArgs.GetBoolArg("-reindex-chainstate", false);
// cache size calculations
int64_t nTotalCache = (gArgs.GetArg("-dbcache", nDefaultDbCache) << 20);
nTotalCache = std::max(nTotalCache, nMinDbCache << 20); // total cache cannot be less than nMinDbCache
nTotalCache = std::min(nTotalCache, nMaxDbCache << 20); // total cache cannot be greater than nMaxDbcache
int64_t nBlockTreeDBCache = std::min(nTotalCache / 8, nMaxBlockDBCache << 20);
nTotalCache -= nBlockTreeDBCache;
int64_t nTxIndexCache = std::min(nTotalCache / 8, gArgs.GetBoolArg("-txindex", DEFAULT_TXINDEX) ? nMaxTxIndexCache << 20 : 0);
nTotalCache -= nTxIndexCache;
int64_t nCoinDBCache = std::min(nTotalCache / 2, (nTotalCache / 4) + (1 << 23)); // use 25%-50% of the remainder for disk cache
nCoinDBCache = std::min(nCoinDBCache, nMaxCoinsDBCache << 20); // cap total coins db cache
nTotalCache -= nCoinDBCache;
nCoinCacheUsage = nTotalCache; // the rest goes to in-memory cache
int64_t nMempoolSizeMax = gArgs.GetArg("-maxmempool", DEFAULT_MAX_MEMPOOL_SIZE) * 1000000;
LogPrintf("Cache configuration:\n");
LogPrintf("* Using %.1fMiB for block index database\n", nBlockTreeDBCache * (1.0 / 1024 / 1024));
if (gArgs.GetBoolArg("-txindex", DEFAULT_TXINDEX)) {
LogPrintf("* Using %.1fMiB for transaction index database\n", nTxIndexCache * (1.0 / 1024 / 1024));
}
然後,只要載入標誌為真且沒有收到關閉系統的請求,即進行以下 while 迴圈。
-
進行 do … while 迴圈處理:
-
呼叫
UnloadBlockIndex
方法,解除安裝區塊相關的索引。 -
重置指向活躍 CCoinsView 的全域性智慧指標變數 pcoinsTip。
-
重置指向 coins 資料庫的全域性智慧指標變數 pcoinsdbview。
-
重置 CCoinsViewErrorCatcher 的智慧指標靜態變數 pcoinscatcher。
-
重置指向活動區塊樹的全域性智慧指標變數 pblocktree。
-
如果
-reset
引數為真,那麼:呼叫活躍區塊樹 pblocktree 的
WriteReindexing
方法,儲存重寫索引標誌。如果當前處於修剪模式,呼叫CleanupBlockRevFiles
方法,清除特定區塊的資料檔案。if (fReset) { pblocktree->WriteReindexing(true); //If we're reindexing in prune mode, wipe away unusable block files and all undo data files if (fPruneMode) CleanupBlockRevFiles(); }
-
如果收到結束請求,則退出迴圈。
if (ShutdownRequested()) break;
-
呼叫
LoadBlockIndex
方法,載入區塊索引。if (!LoadBlockIndex(chainparams)) { strLoadError = _("Error loading block database"); break; }
-
如果區塊索引成功載入,則檢查是否包含區塊。
if (!mapBlockIndex.empty() && !LookupBlockIndex(chainparams.GetConsensus().hashGenesisBlock)) { return InitError(_("Incorrect or no genesis block found. Wrong datadir for network?")); }
-
如果指定有修剪,但又沒有處於修剪模式,則退出迴圈。
if (fHavePruned && !fPruneMode) { strLoadError = _("You need to rebuild the database using -reindex to go back to unpruned mode. This will redownload the entire blockchain"); break; }
-
如果不重建索引,呼叫
LoadGenesisBlock
載入創世區塊。如果失敗,則退出迴圈。if (!fReindex && !LoadGenesisBlock(chainparams)) { strLoadError = _("Error initializing block database"); break; }
-
生成兩個智慧指標物件。
pcoinsdbview.reset(new CCoinsViewDB(nCoinDBCache, false, fReset || fReindexChainState)); pcoinscatcher.reset(new CCoinsViewErrorCatcher(pcoinsdbview.get()));
兩個變數的含義見前面說明。
-
升級資料庫格式。
if (!pcoinsdbview->Upgrade()) { strLoadError = _("Error upgrading chainstate database"); break; }
-
重放區塊,用來處理資料庫不一致。
if (!ReplayBlocks(chainparams, pcoinsdbview.get())) { strLoadError = _("Unable to replay blocks. You will need to rebuild the database using -reindex-chainstate."); break; }
-
當系統走到這一步時,硬碟上的 coinsdb 資料庫已經片於一致狀態了。現在建立指向活躍 CCoinsView 的全域性智慧指標變數 pcoinsTip。
pcoinsTip.reset(new CCoinsViewCache(pcoinscatcher.get()));
-
第8步,開始索引(src/init.cpp::AppInitMain()
)
如果指定了 -txindex
引數,則生成交易索引物件 g_txindex,型別為 TxIndex
;然後呼叫其 Start
方法,開始建立索引。
if (gArgs.GetBoolArg("-txindex", DEFAULT_TXINDEX)) {
g_txindex = MakeUnique<TxIndex>(nTxIndexCache, false, fReindex);
g_txindex->Start();
}
start
方法處理如下:
-
首先,呼叫
RegisterValidationInterface
方法註冊TxIndex
為MainSignalsInstance
上各種事件的訊號處理器,在傳送訊號時會呼叫這些處理器。RegisterValidationInterface(this);
-
然後,呼叫
Init
方法升級交易索引從老的資料庫到新的資料庫。TxIndex
子類過載了這個方法,會呼叫m_db->MigrateData(*pblocktree, chainActive.GetLocator())
方法來升級資料庫。然後,呼叫父類
BaseIndex
的同名方法進行處理。在父類的Init
方法中,首先會呼叫ReadBestBlock
方法從資料庫中讀取 Key 為B
的區塊做為定位器(可能是所有沒有分叉的區塊)。然後,呼叫FindForkInGlobalIndex
方法,找到活躍區塊鏈上的分叉前的最後一區塊索引(從這個區塊產生了分叉)。如果這個索引對應的區塊和活躍區塊鏈的頂端區塊是相同的,設定同步完成標誌為真。 -
啟動一個執行緒,執行緒執行的真正方法為
BaseIndex::ThreadSync
。執行緒的主要作用在於當沒有同步完成時,通過讀取活躍區塊鏈的下一個區塊來進行同步,並把沒有分叉的區塊以 Key 為B
寫入資料庫中。
第9步,載入錢包(src/init.cpp::AppInitMain()
)
呼叫錢包介面物件的 Open
方法,開始載入錢包。具體方法在 wallet/init.cpp
檔案中。內容如下:
bool WalletInit::Open() const
{
if (gArgs.GetBoolArg("-disablewallet", DEFAULT_DISABLE_WALLET)) {
LogPrintf("Wallet disabled!\n");
return true;
}
for (const std::string& walletFile : gArgs.GetArgs("-wallet")) {
std::shared_ptr<CWallet> pwallet = CWallet::CreateWalletFromFile(walletFile, fs::absolute(walletFile, GetWalletDir()));
if (!pwallet) {
return false;
}
AddWallet(pwallet);
}
return true;
}
首先檢查是否禁止錢包,如果禁止直接返回。否則遍歷所有錢包,呼叫 CWallet::CreateWalletFromFile
方法,要根據錢包檔案生成錢包物件,如果成功生成錢包,則呼叫 AddWallet
方法把錢包加入 vpwallets
集合中。
第10步,資料目錄維護(src/init.cpp::AppInitMain()
)
如果當前為修剪模式,本地服務去掉 NODE_NETWORK
標誌,然後如果不需要索引則呼叫 PruneAndFlush
函式,修剪並重新整理到硬碟中。
if (fPruneMode) {
LogPrintf("Unsetting NODE_NETWORK on prune mode\n");
nLocalServices = ServiceFlags(nLocalServices & ~NODE_NETWORK);
if (!fReindex) {
uiInterface.InitMessage(_("Pruning blockstore..."));
PruneAndFlush();
}
}
第11步,匯入區塊(src/init.cpp::AppInitMain()
)
-
呼叫
CheckDiskSpace
函式,檢查硬碟空間是否足夠。如果沒有足夠的硬碟空間,則退出。
-
檢查最佳區塊鏈頂端指示指標是否為空。
如果頂端打針為空,UI介面進行通知。如果不空,則設定有創世區塊,即
fHaveGenesis
設為真。if (chainActive.Tip() == nullptr) { uiInterface.NotifyBlockTip_connect(BlockNotifyGenesisWait); } else { fHaveGenesis = true; }
-
如果指定了
blocknotify
引數,設定介面通知為BlockNotifyCallback
。 -
遍歷引數
loadblock
指定要載入的區塊檔案,放進向量變數vImportFiles
集合中。然後呼叫threadGroup.create_thread
方法,建立一個執行緒。執行緒執行的函式為ThreadImport
,引數為要載入的區塊檔案。std::vector<fs::path> vImportFiles; for (const std::string& strFile : gArgs.GetArgs("-loadblock")) { vImportFiles.push_back(strFile); } threadGroup.create_thread(boost::bind(&ThreadImport, vImportFiles));
-
獲取
cs_GenesisWait
鎖,等待創世區塊被處理完成。{ WaitableLock lock(cs_GenesisWait); // 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()) { condvar_GenesisWait.wait_for(lock, std::chrono::milliseconds(500)); } uiInterface.NotifyBlockTip_disconnect(BlockNotifyGenesisWait); }
第12步,啟動節點(src/init.cpp::AppInitMain()
)
-
獲取活躍區塊鏈的當前排程。
chain_active_height = chainActive.Height();
-
如果指定了監聽洋蔥網路
-listenonion
,呼叫StartTorControl
函式,開始 Tor 控制。程式碼如下所示:
void StartTorControl() { assert(!gBase); #ifdef WIN32 evthread_use_windows_threads(); #else evthread_use_pthreads(); #endif gBase = event_base_new(); if (!gBase) { LogPrintf("tor: Unable to create event_base\n"); return; } torControlThread = std::thread(std::bind(&TraceThread<void (*)()>, "torcontrol", &TorControlThread)); }
libevent預設情況下是單執行緒,每個執行緒有且僅有一個event_base。為了儲存多執行緒下是安全的,首先需要呼叫
evthread_use_pthreads
、evthread_use_windows_threads
等兩個方法,前面是 linux 下的,後面是 windows 下的。在處理完多執行緒設定後,呼叫
event_base_new
方法,建立一個預設的 event_base。最後,啟動一個 Tor 控制執行緒。具體呼叫
std::thread
方法,建立一個執行緒,執行緒的具體執行方法為std::bind
返回的繫結函式。標準繫結函式的第一個引數為要執行的函式,此處為TraceThread
,第二個引數為執行緒的名字torcontrol
,第三個引數為執行緒要執行的真正方法,此處為TorControlThread
函式,後面兩個引數都會做為引數,傳遞到第一個函式。TraceThread
函式,呼叫RenameThread
方法,把執行緒名字設定為bitcoin-torcontrol
,然後執行傳遞進來的TorControlThread
函式。後者會生成一個 Tor 控制器,然後呼叫event_base_dispatch
方法,分發事件。程式碼如下:static void TorControlThread() { TorController ctrl(gBase, gArgs.GetArg("-torcontrol", DEFAULT_TOR_CONTROL)); event_base_dispatch(gBase); }
TorController
建構函式中會做幾件重要的事情:-
首先,呼叫
event_new
方法生成一個 event 物件,event 物件的回撥函式為reconnect_cb
。 -
然後,呼叫
TorControlConnection::Connect
方法連線到 Tor 控制器。這個方法又會做幾件事情:
-
解析 Tor 控制器的地址。
-
呼叫
bufferevent_socket_new
方法,基於套接字生成一個 bufferevent。 -
設定 bufferevent 的回撥方法,包括:讀取回調函式為
TorControlConnection::readcb
,寫入回撥函式為空,事件回撥函式為TorControlConnection::eventcb
,同時指定 bufferevent 啟用讀寫標誌。 -
設定
TorControlConnection
連線、斷開連線的兩個指標函式分別為:TorController::connected_cb
和TorController::disconnected_cb
。 -
呼叫
bufferevent_socket_connect
方法,連線到前面生成的 bufferevent。方法在連線成功後,會立即呼叫事件回撥函式
TorControlConnection::eventcb
。
-
-
呼叫
Discover
函式,開始發現本節點的地址。3. 呼叫Discover
函式,開始發現本節點的地址。3. 呼叫Discover
函式,開始發現本節點的地址。方法內首先判斷是否已經處理過。如果沒有,那麼開始發現本節點的地址。具體處理分為 windows 和 linux,下面主要講述 linux 下的處理。方法內首先判斷是否已經處理過。如果沒有,那麼開始發現本節點的地址。具體處理分為 windows 和 linux,下面主要講述 linux 下的處理。
呼叫
getifaddrs
方法,查詢系統所有的網路介面的資訊,包括乙太網卡介面和迴環介面等。本方法返回一個如下的結構體:呼叫getifaddrs
方法,查詢系統所有的網路介面的資訊,包括乙太網卡介面和迴環介面等。本方法返回一個如下的結構體:
struct ifaddrs { struct ifaddrs *ifa_next; /* 列表中的下一個條目 */ char *ifa_name; /* 介面的名稱 */ unsigned int ifa_flags; /* 來自 SIOCGIFFLAGS 的標誌 */ struct sockaddr *ifa_addr; /* 介面的地址 */ struct sockaddr *ifa_netmask; /* 介面的網路掩碼 */ union { struct sockaddr *ifu_broadaddr; /* 介面的廣播地址 */ struct sockaddr *ifu_dstaddr; /* 點對點的目標地址 */ } ifa_ifu; #define ifa_broadaddr ifa_ifu.ifu_broadaddr #define ifa_dstaddr ifa_ifu.ifu_dstaddr void *ifa_data; /* Address-specific data */ };
如果可以獲取介面資訊,則遍歷每一個介面,進行如下處理:
- 如果介面地址為空,則處理下一個。
- 如果不是介面標誌不是 IFF_UP ,則處理下一個。
- 如果介面名稱是 lo 或 lo0,則處理下一個。
- 如果介面是 tcp,TCP 等,則生成 IP 地址物件,然後呼叫
AddLocal
方法,儲存本地地址。 - 如果介面是 IPV6,則則生成 IP 地址物件,然後呼叫
AddLocal
方法,儲存本地地址。
程式碼如下所示:
if (getifaddrs(&myaddrs) == 0) { for (struct ifaddrs* ifa = myaddrs; ifa != nullptr; ifa = ifa->ifa_next) { if (ifa->ifa_addr == nullptr) continue; if ((ifa->ifa_flags & IFF_UP) == 0) continue; if (strcmp(ifa->ifa_name, "lo") == 0) continue; if (strcmp(ifa->ifa_name, "lo0") == 0) continue; if (ifa->ifa_addr->sa_family == AF_INET) { struct sockaddr_in* s4 = (struct sockaddr_in*)(ifa->ifa_addr); CNetAddr addr(s4->sin_addr); if (AddLocal(addr, LOCAL_IF)) LogPrintf("%s: IPv4 %s: %s\n", __func__, ifa->ifa_name, addr.ToString()); } else if (ifa->ifa_addr->sa_family == AF_INET6) { struct sockaddr_in6* s6 = (struct sockaddr_in6*)(ifa->ifa_addr); CNetAddr addr(s6->sin6_addr); if (AddLocal(addr, LOCAL_IF)) LogPrintf("%s: IPv6 %s: %s\n", __func__, ifa->ifa_name, addr.ToString()); } } freeifaddrs(myaddrs); }
-
-
如果指定了
upnp
引數,則呼叫StartMapPort
函式,開始進行埠對映。if (gArgs.GetBoolArg("-upnp", DEFAULT_UPNP)) { StartMapPort(); }
-
生成選項物件,並進行初始化。
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;
上面的程式碼基本就是設定本地支援的服務、最大連線數、最大出站數、最大節點數、最大費率、活躍區塊鏈的高度、節點邏輯驗證器、傳送的最大緩衝值、接收的最大緩衝值、連線的節點數等。
-
如果指定了
-bind
引數,則處理繫結引數。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); }
遍歷所有的繫結地址,呼叫
Lookup
方法,進行 DNS查詢。如果可以找到對應 IP地址,把生成的CService
物件放入選項物件的vBinds
屬性中。 -
如果指定了
-whitebind
引數,則處理繫結引數。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); }
遍歷所有的繫結地址,呼叫
Lookup
方法,進行 DNS查詢。如果可以找到對應 IP地址,且對應的埠號不等於0,把生成的CService
物件放入選項物件的vWhiteBinds
屬性中。 -
如果指定了
-whitelist
引數,則處理白名單列表。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); }
遍歷白名單列表,呼叫
LookupSubNet
方法,查詢對應的子網掩碼,如果對應的子網掩碼是有效的,那麼放入選項物件的vWhitelistedRange
屬性中。 -
取得引數
seednode
指定的值,放入選項物件的vSeedNodes
屬性中。connOptions.vSeedNodes = gArgs.GetArgs("-seednode");
-
呼叫
CConnman
物件的Start
方法,初始所有的出站連線。本方法非常非常重要,因為它啟動了一個重要的流程,即底層的 P2P 網路建立和訊息處理流程。
具體分析如下:
-
呼叫
Init
方法,根據選項物件設定物件的屬性,不細說,程式碼如下:void Init(const Options& connOptions) { nLocalServices = connOptions.nLocalServices; nMaxConnections = connOptions.nMaxConnections; nMaxOutbound = std::min(connOptions.nMaxOutbound, connOptions.nMaxConnections); nMaxAddnode = connOptions.nMaxAddnode; nMaxFeeler = connOptions.nMaxFeeler; nBestHeight = connOptions.nBestHeight; clientInterface = connOptions.uiInterface; m_msgproc = connOptions.m_msgproc; nSendBufferMaxSize = connOptions.nSendBufferMaxSize; nReceiveFloodSize = connOptions.nReceiveFloodSize; { LOCK(cs_totalBytesSent); nMaxOutboundTimeframe = connOptions.nMaxOutboundTimeframe; nMaxOutboundLimit = connOptions.nMaxOutboundLimit; } vWhitelistedRange = connOptions.vWhitelistedRange; { LOCK(cs_vAddedNodes); vAddedNodes = connOptions.m_added_nodes; } }
-
接下來,使用鎖初始一些比較重要的屬性。
{ LOCK(cs_totalBytesRecv); nTotalBytesRecv = 0; } { LOCK(cs_totalBytesSent); nTotalBytesSent = 0; nMaxOutboundTotalBytesSentInCycle = 0; nMaxOutboundCycleStartTime = 0; }
-
再接下來,獲取節點繫結的本地地址和埠,並生成對應的套接字,接受別的節點的請求。
if (fListen && !InitBinds(connOptions.vBinds, connOptions.vWhiteBinds)) { if (clientInterface) { clientInterface->ThreadSafeMessageBox( _("Failed to listen on any port. Use -listen=0 if you want this."), "", CClientUIInterface::MSG_ERROR); } return false; }
`InitBinds` 方法,接收 `-bind` 和 `-whitebind` 引數生成的集合,並解析各個地址,生成套接字,並進行監聽。具體分析如下: - 首先,處理`-bind` 地址集合。 for (const auto& addrBind : binds) { fBound |= Bind(addrBind, (BF_EXPLICIT | BF_REPORT_ERROR)); } - 然後,處理 `-whitebind` 地址集合。 for (const auto& addrBind : whiteBinds) { fBound |= Bind(addrBind, (BF_EXPLICIT | BF_REPORT_ERROR | BF_WHITELIST)); } - 如果,兩個引數都沒有指定,則使用下面程式碼進行處理。 if (binds.empty() && whiteBinds.empty()) { struct in_addr inaddr_any; inaddr_any.s_addr = INADDR_ANY; struct in6_addr inaddr6_any = IN6ADDR_ANY_INIT; fBound |= Bind(CService(inaddr6_any, GetListenPort()), BF_NONE); fBound |= Bind(CService(inaddr_any, GetListenPort()), !fBound ? BF_REPORT_ERROR : BF_NONE); } 從以上程式碼可以看出來,三種情況下,處理基本相同,都是呼叫 `Bind` 方法來處理。下面,我們進進入這個方法一控究竟。這個方法的主體是呼叫 `BindListenPort` 方法進行處理。下面我們開始講解這個方法。 - 首先,生成一個通用的網路地址 sockaddr 物件,型別為 sockaddr_storage,它的長度是 128個位元組。 - 然後,呼叫 `addrBind.GetSockAddr((struct sockaddr*)&sockaddr, &len)` 方法來設定網路地址 sockaddr。 `GetSockAddr` 方法內部根據地址是 IPV4 或 IPV6,分別進行處理。 如果是 IPV4,則生成 sockaddr_in 地址物件,然後呼叫 `memset` 把結構體所佔記憶體用0填充,然後呼叫 `GetInAddr` 方法來設定地址物件的地址欄位,最後設定地址型別為 AF_INET 和埠號。 如果是 IPV6,則生成 sockaddr_in6 地址物件,然後呼叫 `memset` 把結構體所佔記憶體用0填充,然後呼叫 `GetIn6Addr` 方法來設定地址物件的地址欄位,最後設定地址型別為 AF_INET6 和埠號。 - 再然後,呼叫 `CreateSocket(addrBind)` 方法生成套接字物件。 方法處理如下: - 首先,生成一個通用的網路地址 sockaddr 物件,型別為 sockaddr_storage,然後,呼叫 `addrBind.GetSockAddr((struct sockaddr*)&sockaddr, &len)` 方法來設定網路地址 sockaddr。具體分析詳見上面。 - 然後,生成套接字。 socket(((struct sockaddr*)&sockaddr)->sa_family, SOCK_STREAM, IPPROTO_TCP) - 再然後,對套接字進行一些檢查和處理,不詳述。 - 套接字生成之後,接下來把套接字繫結到指定的地址上,並監聽入站請求。 if (::bind(hListenSocket, (struct sockaddr*)&sockaddr, len) == SOCKET_ERROR) { int nErr = WSAGetLastError(); if (nErr == WSAEADDRINUSE) strError = strprintf(_("Unable to bind to %s on this computer. %s is probably already running."), addrBind.ToString(), _(PACKAGE_NAME)); else strError = strprintf(_("Unable to bind to %s on this computer (bind returned error %s)"), addrBind.ToString(), NetworkErrorString(nErr)); LogPrintf("%s\n", strError); CloseSocket(hListenSocket); return false; } LogPrintf("Bound to %s\n", addrBind.ToString()); // Listen for incoming connections if (listen(hListenSocket, SOMAXCONN) == SOCKET_ERROR) { strError = strprintf(_("Error: Listening for incoming connections failed (listen returned error %s)"), NetworkErrorString(WSAGetLastError())); LogPrintf("%s\n", strError); CloseSocket(hListenSocket); return false; } - 最後,進行一些收尾工作。 把套接字放入 `vhListenSocket` 集合中。如果地址是可達的,並且不是白名單中的地址,則呼叫 `AddLocal` 方法,加入本地地址集合中。
-
處理完地址繫結之後,接下來處理種子節點引數指定集合。
void CConnman::AddOneShot(const std::string& strDest) { LOCK(cs_vOneShots); vOneShots.push_back(strDest); }
這個方法非常簡單,把每個種子節點加入 `vOneShots` 集合。
-
接下來,從檔案資料庫中載入地址列表和禁止地址列表。
{ CAddrDB adb; if (adb.Read(addrman)) LogPrintf("Loaded %i addresses from peers.dat %dms\n", addrman.size(), GetTimeMillis() - nStart); else { addrman.Clear(); // Addrman can be in an inconsistent state after failure, reset it LogPrintf("Invalid or missing peers.dat; recreating\n"); DumpAddresses(); } } CBanDB bandb; banmap_t banmap; if (bandb.Read(banmap)) { SetBanned(banmap); // thread save setter SetBannedSetDirty(false); // no need to write down, just read data SweepBanned(); // sweep out unused entries LogPrint(BCLog::NET, "Loaded %d banned node ips/subnets from banlist.dat %dms\n", banmap.size(), GetTimeMillis() - nStart); } else { LogPrintf("Invalid or missing banlist.dat; recreating\n"); SetBannedSetDirty(true); // force write DumpBanlist(); }
程式碼比較簡單,一看便知,不作具體展開。
-
最後,重中之重的執行緒相關處理終於要到來了。
-
首先,生成套接字相關的執行緒,以便進行網路的接收和傳送。處理方法和前面執行緒的類似,程式碼如下:
threadSocketHandler = std::thread(&TraceThread<std::function<void()> >, "net", std::function<void()>(std::bind(&CConnman::ThreadSocketHandler, this)));
真正執行的方法是 `ThreadSocketHandler`,這個方法太重要了,我們留在下一課網路處理中細講。
-
接下來,處理 DNS 種子節點執行緒,處理 DNS 種子相關的邏輯。程式碼如下:
if (!gArgs.GetBoolArg("-dnsseed", true)) LogPrintf("DNS seeding disabled\n"); else threadDNSAddressSeed = std::thread(&TraceThread<std::function<void()> >, "dnsseed", std::function<void()>(std::bind(&CConnman::ThreadDNSAddressSeed, this)));
真正執行的方法是 `ThreadDNSAddressSeed`,這個方法太重要了,我們留在下一課網路處理中細講。
-
接下來,處理出站連線。程式碼如下:
threadOpenAddedConnections = std::thread(&TraceThread<std::function<void()> >, "addcon", std::function<void()>(std::bind(&CConnman::ThreadOpenAddedConnections, this)));
真正執行的方法是 `ThreadOpenAddedConnections`,這個方法太重要了,我們留在下一課網路處理中細講。
-
接下來,處理開啟連線的執行緒。程式碼如下:
if (connOptions.m_use_addrman_outgoing || !connOptions.m_specified_outgoing.empty()) threadOpenConnections = std::thread(&TraceThread<std::function<void()> >, "opencon", std::function<void()>(std::bind(&CConnman::ThreadOpenConnections, this, connOptions.m_specified_outgoing)));
真正執行的方法是 `ThreadOpenConnections`,這個方法太重要了,我們留在下一課網路處理中細講。
- 最最重要的執行緒–處理訊息的執行緒,隆重登場。
-
-
threadMessageHandler = std::thread(&TraceThread<std::function<void()> >, "msghand", std::function<void()>(std::bind(&CConnman::ThreadMessageHandler, this)));
真正執行的方法是 ThreadMessageHandler
,這個方法太重要了,我們留在下一課網路處理中細講。
- 最後,定時把節點地址和禁止列表重新整理到資料庫檔案中。
第13步,結束啟動(src/init.cpp::AppInitMain()
)
-
呼叫
SetRPCWarmupFinished()
方法,設定熱身結束。方法內部主要設定
fRPCInWarmup
變數為假,表示熱身結束。 -
呼叫錢包介面物件的
Start
方法,開始進行錢包相關的處理,並定時重新整理錢包資料到資料庫中。程式碼如下:
for (const std::shared_ptr<CWallet>& pwallet : GetWallets()) { pwallet->postInitProcess(); } // Run a thread to flush wallet periodically scheduler.scheduleEvery(MaybeCompactWalletDB, 500);
方法中,首先便利所有錢包物件,呼叫其
postInitProcess
方法,進行後初始化設定。主要是把錢包中存在,但是交易池中不存在的交易新增到交易池中。然後,設定排程器定時呼叫
MaybeCompactWalletDB
方法,重新整理錢包資料到資料庫中。
**我是區小白,Ulord全球社群聯盟(優得社群)核心區塊鏈技術開發者,深入研究比特幣,以太坊,EOS Dash,Rsk,Java, Nodejs,PHP,Python,C++ 我希望能聚集更多區塊鏈開發者,一起學習共同進步。
往期文章:
從零開始學習比特幣開發(三)接入比特幣網路的關鍵步驟解析、建立比特幣錢包,以及重要rpc指令
原文轉載自:
從零開始學習比特幣開發(二)–如何接入比特幣網路以及原理分析