從零開始學習比特幣開發(七)-P2P網路建立流程之生成地址對並連線到指定地址
本節繼續講解比特幣P2P網路建立流程,這節講解的執行緒為’ThreadOpenAddedConnections’,它的作用是生成地址對並連線到指定地址。
本文可以結合比特幣系統啟動的的第12步的講解來看,可以更加系統的瞭解比特幣系統啟動的過程。
P2P 網路的建立是在比特幣系統啟動的第 12 步,最後時刻呼叫 CConnman::Start
方法開始的。
本部分內容在 net.cpp
、net_processing.cpp
等檔案中。
下面開始講解各個執行緒的具體處理。
1、ThreadSocketHandler
見文章從零開始學習比特幣(五)–P2P網路建立的流程之套接字的讀取和傳送
2、ThreadDNSAddressSeed
見文章從零開始學習比特幣(六)–P2P網路建立的流程之查詢DNS節點
3、ThreadOpenAddedConnections
這個執行緒的主要作用是生成地址物件,並且呼叫 OpenNetworkConnection
方法,連線到指定地址。
執行緒定義在 net.cpp
檔案的 1959 行。下面我們開始進行具體的解讀。
執行緒的主體是一個 while
迴圈。在迴圈中進行下面的處理。
-
呼叫
GetAddedNodeInfo
方法,獲取所有的節點資訊。本方法返回所有的節點資訊,其中即有已連線的,也有未連線的地址。
-
首先,生成儲存節點資訊的容器變數
ret
和儲存地址字串的列表物件lAddresses
。然後把vAddedNodes
集合中的所有地址拷貝到lAddresses
中。std::vector<AddedNodeInfo> ret;
std::list<std::string> lAddresses(0); { LOCK(cs_vAddedNodes); ret.reserve(vAddedNodes.size()); std::copy(vAddedNodes.cbegin(), vAddedNodes.cend(), std::back_inserter(lAddresses)); }
-
遍歷所有的節點(
vNodes
節點容器),進行下面處理。如果當前節點的地址是有效的,則加入
mapConnected
map 中,Key 為當前節點的地址,值標明當前節點是否為入站節點。獲取當前節點的地址名稱。如果名稱不空,則放進
mapConnectedByName
map 中,Key 為當前節點的地址名稱,值為一個std::pair
物件,其中第一個值表明當前節點是否為入站節點,第二個值為節點的地址。std::map<CService, bool> mapConnected; std::map<std::string, std::pair<bool, CService>> mapConnectedByName; { LOCK(cs_vNodes); for (const CNode* pnode : vNodes) { if (pnode->addr.IsValid()) { mapConnected[pnode->addr] = pnode->fInbound; } std::string addrName = pnode->GetAddrName(); if (!addrName.empty()) { mapConnectedByName[std::move(addrName)] = std::make_pair(pnode->fInbound, static_cast<const CService&>(pnode->addr)); } } }
-
遍歷
lAddresses
變數,進行下面處理。根據當前地址和當前網路型別,生成一個
service
物件,型別為CService
,和一個節點資訊物件。如果當前地址是 IP:Port 形式,那麼查詢
mapConnected
集合對應的地址。如果可以找到,則設定節點資訊物件的相關屬性。如果當前地址是名稱的形式,那麼查詢
mapConnectedByName
集合對應的地址。如果可以找到,則設定節點資訊物件的相關屬性。把當前地址資訊物件加入
ret
集合中。for (const std::string& strAddNode : lAddresses) { CService service(LookupNumeric(strAddNode.c_str(), Params().GetDefaultPort())); AddedNodeInfo addedNode{strAddNode, CService(), false, false}; if (service.IsValid()) { // strAddNode is an IP:port auto it = mapConnected.find(service); if (it != mapConnected.end()) { addedNode.resolvedAddress = service; addedNode.fConnected = true; addedNode.fInbound = it->second; } } else { // strAddNode is a name auto it = mapConnectedByName.find(strAddNode); if (it != mapConnectedByName.end()) { addedNode.resolvedAddress = it->second.second; addedNode.fConnected = true; addedNode.fInbound = it->second.first; } } ret.emplace_back(std::move(addedNode)); }
-
返回
ret
集合。
-
-
遍歷所有的節點資訊,如果當前節點還沒有連線,進行下面的處理:
生成地址物件
addr
,型別為CAddress
。呼叫
OpenNetworkConnection
方法,連線到當前的節點。for (const AddedNodeInfo& info : vInfo) { if (!info.fConnected) { if (!grant.TryAcquire()) { // If we've used up our semaphore and need a new one, let's not wait here since while we are waiting // the addednodeinfo state might change. break; } tried = true; CAddress addr(CService(), NODE_NONE); OpenNetworkConnection(addr, false, &grant, info.strAddedNode.c_str(), false, false, true); if (!interruptNet.sleep_for(std::chrono::milliseconds(500))) return; } }
下面我們具體看下 OpenNetworkConnection
函式的處理。
-
如果
interruptNet
為真,則返回。如果網路沒有啟用(fNetworkActive
為假),則返回。if (interruptNet) { return; } if (!fNetworkActive) { return; }
-
如果引數
pszDest
為空(當前節點資訊的地址),進一步,如果要連線的節點是本地的,或是已連線的,或是禁止的,則返回。如果引數pszDest
不為空,進一步,如果節點是已連線的,則返回。if (!pszDest) { if (IsLocal(addrConnect) || FindNode(static_cast<CNetAddr>(addrConnect)) || IsBanned(addrConnect) || FindNode(addrConnect.ToStringIPPort())) return; } else if (FindNode(std::string(pszDest))) return;
-
呼叫
ConnectNode
方法,連線到指定地址,並返回對等節點CNode
物件。如果連線失敗,則返回。 -
如果引數
grantOutbound
物件存在,則呼叫其MoveTo
方法,進行處理。 -
如果引數
fOneShot
為真,則設定對等節點的fOneShot
屬性為真。 -
如果是臨時探測節點(引數
fFeeler
為真),則設定對等節點的fFeeler
屬性為真。 -
如果是手動連線的,則設定對等節點的
m_manual_connection
屬性為真。 -
呼叫網路事件處理器的
InitializeNode
方法,進行對等節點初始化。具體程式碼在
net_processing.cpp
檔案的第 611 行,如下所示:void PeerLogicValidation::InitializeNode(CNode *pnode) { CAddress addr = pnode->addr; std::string addrName = pnode->GetAddrName(); NodeId nodeid = pnode->GetId(); { LOCK(cs_main); mapNodeState.emplace_hint(mapNodeState.end(), std::piecewise_construct, std::forward_as_tuple(nodeid), std::forward_as_tuple(addr, std::move(addrName))); } if(!pnode->fInbound) PushNodeVersion(pnode, connman, GetTime()); }
程式碼最主要的動作是,檢查節點是否為出站節點,即連線到別的對等節點,如果是則呼叫
PushNodeVersion
方法,傳送版本資訊。具體訊息訊息處理部分。 -
把生成的對等節點儲存到
vNodes
向量中。
3.1、ConnectNode
在上文講解 OpenNetworkConnection
函式“3.呼叫 ConnectNode
方法,連線到指定地址,並返回對等節點 CNode
物件” 中我提到了‘ConnectNode’方法,這個方法負責連線到具體的對等節點。我們來看下具體的處理。
-
如果引數
pszDest
為空指標,則處理如下:如果要連線的地址是本地地址,則直接返回空指標。呼叫
FindNode
方法,檢視指定的節點是否存在。如果存在,即已經連線,則返回空指標。if (pszDest == nullptr) { if (IsLocal(addrConnect)) return nullptr; // Look for an existing connection CNode* pnode = FindNode(static_cast<CService>(addrConnect)); if (pnode) { LogPrintf("Failed to open new connection, already connected\n"); return nullptr; } }
-
如果引數
pszDest
不是空指標,那麼呼叫Lookup
方法,查詢/生成地址字串對應的地址物件。如果找到,則進行下面的處理:生成要連線的地址物件。如果地址地址物件是無效的,則返回空指標。呼叫
FindNode
方法,查詢對應的地址物件。如果存在,即已經連線,則返回空指標。這個地方解析要連線的地址字串生成要連線的地址物件。
const int default_port = Params().GetDefaultPort(); if (pszDest) { std::vector<CService> resolved; if (Lookup(pszDest, resolved, default_port, fNameLookup && !HaveNameProxy(), 256) && !resolved.empty()) { addrConnect = CAddress(resolved[GetRand(resolved.size())], NODE_NONE); if (!addrConnect.IsValid()) { LogPrint(BCLog::NET, "Resolver returned invalid address %s for %s\n", addrConnect.ToString(), pszDest); return nullptr; } LOCK(cs_vNodes); CNode* pnode = FindNode(static_cast<CService>(addrConnect)); if (pnode) { pnode->MaybeSetAddrName(std::string(pszDest)); LogPrintf("Failed to open new connection, already connected\n"); return nullptr; } } }
-
如果要連線的地址物件是有效的,進行下面的處理。
呼叫
GetProxy
方法,返回代理型別。如果方法返回為真,即存在代理,那麼呼叫CreateSocket
方法,建立代理套接字。如果成功建立,呼叫ConnectThroughProxy
方法,通過代理連線到對等節點。如果不存在代理,那麼呼叫
CreateSocket
方法,建立對等節點的套接字。如果成功建立,呼叫ConnectSocketDirectly
方法,直接連線到對等節點。bool proxyConnectionFailed = false;
if (GetProxy(addrConnect.GetNetwork(), proxy)) { hSocket = CreateSocket(proxy.proxy); if (hSocket == INVALID_SOCKET) { return nullptr; } connected = ConnectThroughProxy(proxy, addrConnect.ToStringIP(), addrConnect.GetPort(), hSocket, nConnectTimeout, &proxyConnectionFailed); } else { // no proxy needed (none set for target network) hSocket = CreateSocket(addrConnect); if (hSocket == INVALID_SOCKET) { return nullptr; } connected = ConnectSocketDirectly(addrConnect, hSocket, nConnectTimeout, manual_connection); } if (!proxyConnectionFailed) { // If a connection to the node was attempted, and failure (if any) is not caused by a problem connecting to // the proxy, mark this as an attempt. addrman.Attempt(addrConnect, fCountFailure); }
-
如果要連線的字串不空,且存在代理,那麼:
呼叫
CreateSocket
方法,生成代理的套接字。然後,呼叫ConnectThroughProxy
方法,通過代理連線到指定的對等節點。hSocket = CreateSocket(proxy.proxy); if (hSocket == INVALID_SOCKET) { return nullptr; } std::string host; int port = default_port; SplitHostPort(std::string(pszDest), port, host); connected = ConnectThroughProxy(proxy, host, port, hSocket, nConnectTimeout, nullptr);
-
如果以上都沒有連線到主節點,則關閉套接字並返回空指標。
if (!connected) { CloseSocket(hSocket); return nullptr; }
-
最後,生成並返回主節點物件。
NodeId id = GetNewNodeId(); uint64_t nonce = GetDeterministicRandomizer(RANDOMIZER_ID_LOCALHOSTNONCE).Write(id).Finalize(); CAddress addr_bind = GetBindAddress(hSocket); CNode* pnode = new CNode(id, nLocalServices, GetBestHeight(), hSocket, addrConnect, CalculateKeyedNetGroup(addrConnect), nonce, addr_bind, pszDest ? pszDest : "", false); pnode->AddRef(); return pnode;
我是區小白,Ulord全球社群聯盟(優得社群)核心區塊鏈技術開發者,深入研究比特幣,以太坊,EOS Dash,Rsk,Java, Nodejs,PHP,Python,C++ 我希望能聚集更多區塊鏈開發者,一起學習共同進步。
為了更高效的交流探討區塊鏈開發過程中遇到的問題,我建立了一個技術交流群。
歡迎將以上問題的答案發在群中討論,或者在帖子下面留言。
往期文章:
從零開始學習比特幣開發(三)接入比特幣網路的關鍵步驟解析、建立比特幣錢包,以及重要rpc指令
從零開始學習比特幣開發(四)–網路初始化,載入區塊鏈和錢包,匯入區塊啟動節點
從零開始學習比特幣(五)–P2P網路建立的流程之套接字的讀取和傳送
從零開始學習比特幣(六)–P2P網路建立的流程之查詢DNS節點
原文轉載自:
從零開始學習比特幣開發(二)–如何接入比特幣網路以及原理分析
從零開始學習比特幣開發(三)–接入比特幣網路的關鍵步驟解析、建立比特幣錢包,以及重要rpc指令
從零開始學習比特幣開發(四)–網路初始化,載入區塊鏈和錢包,匯入區塊啟動節點