從零開始學習比特幣(六)--P2P網路建立的流程之查詢DNS節點
上節開始我們已經開始講解比特幣系統中P2P網路是如何建立的。還記得在比特幣系統啟動的的第12步的講解中,我們提到有幾個執行緒相關的處理非常重要嗎?以下內容正是基於此做了詳細的講解。由於篇幅過長,我們分幾篇文章依次道來。
P2P 網路的建立是在比特幣系統啟動的第 12 步,最後時刻呼叫 CConnman::Start
方法開始的。
本部分內容在 net.cpp
、net_processing.cpp
等檔案中。
下面開始講解各個執行緒的具體處理。
1、ThreadSocketHandler
詳情見上一篇文章:
從零開始學習比特幣(五)–P2P網路建立的流程之套接字的讀取和傳送
2、ThreadDNSAddressSeed
這個執行緒的目標是,通過查詢DNS節點來找到足夠多的比特幣節點。找到之後才可以連線比特幣網路進行同步。
只有在需要地址時才查詢 DNS 種子,當我們不需要 DNS 種子時,會避免 DNS 種子查詢。這樣可以通過建立更少的識別 DNS 請求來提高使用者隱私。
執行緒定義在 net.cpp
檔案的 1603 行。下面我們開始進行具體的解讀。
-
如果對等節點的數量大於 0,且沒有指定
-forcednsseed
,或指定了但值為false
,進行下面的處理:遍歷所有的節點,如果節點已成功連線,且不是引導節點,且
fOneShot
nRelevant
加1。如果變數
nRelevant
大於2,即 P2P 網路已經可用,則退出函式。if ((addrman.size() > 0) && (!gArgs.GetBoolArg("-forcednsseed", DEFAULT_FORCEDNSSEED))) { if (!interruptNet.sleep_for(std::chrono::seconds(11))) return; LOCK(cs_vNodes); int nRelevant = 0; for (auto pnode : vNodes) { nRelevant += pnode->fSuccessfullyConnected && !pnode->fFeeler && !pnode->fOneShot && !pnode->m_manual_connection && !pnode->fInbound; } if (nRelevant >= 2) { LogPrintf("P2P peers available. Skipped DNS seeding.\n"); return; } }
-
獲取並遍歷所有的 DNS 種子節點。
for (const std::string &seed : vSeeds) { if (interruptNet) { return; } if (HaveNameProxy()) { AddOneShot(seed); } else { std::vector<CNetAddr> vIPs; std::vector<CAddress> vAdd; ServiceFlags requiredServiceBits = GetDesirableServiceFlags(NODE_NONE); std::string host = strprintf("x%x.%s", requiredServiceBits, seed); CNetAddr resolveSource; if (!resolveSource.SetInternal(host)) { continue; } unsigned int nMaxIPs = 256; // Limits number of IPs learned from a DNS seed if (LookupHost(host.c_str(), vIPs, nMaxIPs, true)) { for (const CNetAddr& ip : vIPs) { int nOneDay = 24*3600; CAddress addr = CAddress(CService(ip, Params().GetDefaultPort()), requiredServiceBits); addr.nTime = GetTime() - 3*nOneDay - GetRand(4*nOneDay); // use a random age between 3 and 7 days old vAdd.push_back(addr); found++; } addrman.Add(vAdd, resolveSource); } else { // We now avoid directly using results from DNS Seeds which do not support service bit filtering, // instead using them as a oneshot to get nodes with our desired service bits. AddOneShot(seed); } } }
下面,對上面的程式碼進行講解。
如果指定了代理,則呼叫
AddOneShot
方法,儲存當前 DNS 種子節點到vOneShots
集合中。否則,進行下面的處理:-
生成兩個集合
vIPs
、vAdd
。vIPs
集合中儲存的是CNetAddr
物件,代表了一個IP地址。vAdd
集合中儲存的是CAddress
物件,CAddress
繼承自CService
,後者又繼承自CAddress
,包含了一些關於對等節點別的資訊。 -
呼叫
GetDesirableServiceFlags
方法,獲得服務標誌位。 -
呼叫
strprintf
函式,格式化 DNS 種子節點的地址。strprintf
是一個巨集定義,實際呼叫的是 Boost 庫的tfm::format
。 -
生成型別為
CNetAddr
的地址物件resolveSource
,並呼叫其SetInternal
方法,設定resolveSource
的 IP。如果出錯,則返回處理下一個。 -
呼叫
LookupHost
方法,根據 DNS 種子節點獲取其儲存的對等節點列表。並儲存在vIPs
集合中。LookupHost
方法內部主要呼叫了LookupIntern
方法進行處理。下面我們看下後者的具體處理。-
生成一個地址物件
addr
。然後呼叫其SetSpecial
方法進行處理。在該方法內部,如果 DNS種子節點不是以
.onion
結尾,即不是暗網地址,則直接返回假。否則進行下面的處理。呼叫
DecodeBase32
方法,解析不包括暗網字尾在內的具體的地址。接下來,檢查地址的長度是否不等於指定的長度,如果是則返回假。否則,對地址進行處理並轉化為IP地址,然後返回真。bool CNetAddr::SetSpecial(const std::string &strName) { if (strName.size()>6 && strName.substr(strName.size() - 6, 6) == ".onion") { std::vector<unsigned char> vchAddr = DecodeBase32(strName.substr(0, strName.size() - 6).c_str()); if (vchAddr.size() != 16-sizeof(pchOnionCat)) return false; memcpy(ip, pchOnionCat, sizeof(pchOnionCat)); for (unsigned int i=0; i<16-sizeof(pchOnionCat); i++) ip[i + sizeof(pchOnionCat)] = vchAddr[i]; return true; } return false; }
如果前面方法返回的結果為真,即 DNS 種子為暗網地址,則把當前地址加入
vIP
集合,並返回。CNetAddr addr; if (addr.SetSpecial(std::string(pszName))) { vIP.push_back(addr); return true; }
-
生成一個型別為
addrinfo
的結構體物件 aiHint,並設定其各個屬性值。 -
生成一個型別為
addrinfo
的結構體物件 aiRes,然後呼叫getaddrinfo
方法,根據 DNS 種子節點來獲取一個地址連結表。這個方法是系統提供的方法,返回的是一個 sockaddr 結構的連結串列而不是一個地址清單。第一個引數是一個主機名或者地址串,第二個引數是一個服務名或者10進位制埠號數串,第三個引數可以是一個空指標,也可以是一個指向某個addrinfo結構的指標,呼叫者在這個結構中填入關於期望返回的資訊型別的暗示,最後一個引數是返回的結果。
int nErr = getaddrinfo(pszName, nullptr, &aiHint, &aiRes); if (nErr) return false;
-
接下來只要地址資訊連結串列不空,且當前獲取的對等節點IP數量小於指定的數量或者指定的數量是0(即不限制對等節點的數量),就迴圈這個連結串列進行下面的處理。
根據返回的地址資訊物件,是IPV4 或者是 IPV6,生成生成不同的
CNetAddr
物件。如果這個地址物件不是內部 IP,則儲存到vIP
集合中。從地址資訊連結串列中取得下一個地址資訊物件。struct addrinfo *aiTrav = aiRes; while (aiTrav != nullptr && (nMaxSolutions == 0 || vIP.size() < nMaxSolutions)) { CNetAddr resolved; if (aiTrav->ai_family == AF_INET) { assert(aiTrav->ai_addrlen >= sizeof(sockaddr_in)); resolved = CNetAddr(((struct sockaddr_in*)(aiTrav->ai_addr))->sin_addr); } if (aiTrav->ai_family == AF_INET6) { assert(aiTrav->ai_addrlen >= sizeof(sockaddr_in6)); struct sockaddr_in6* s6 = (struct sockaddr_in6*) aiTrav->ai_addr; resolved = CNetAddr(s6->sin6_addr, s6->sin6_scope_id); } /* Never allow resolving to an internal address. Consider any such result invalid */ if (!resolved.IsInternal()) { vIP.push_back(resolved); } aiTrav = aiTrav->ai_next; }
-
呼叫
freeaddrinfo
方法,釋放getaddrinfo
方法所申請的記憶體空間。 -
根據
vIP
集合的大小,返回真假。
-
-
如果
LookupHost
方法返回結果為真,即根據當前 DNS 種子節點查詢到了至少一個對等節點,則進行下面的處理。遍歷
vIPs
集合,根據當前的 IP 地址,生成一個CAddress
地址物件,並儲存在vAdd
集合中,同時把代表找到節點的變數found
加1。呼叫地址管理器的
Add
方法,儲存多個地址。具體程式碼如下:
for (const CNetAddr& ip : vIPs) { int nOneDay = 24*3600; CAddress addr = CAddress(CService(ip, Params().GetDefaultPort()), requiredServiceBits); addr.nTime = GetTime() - 3*nOneDay - GetRand(4*nOneDay); // use a random age between 3 and 7 days old vAdd.push_back(addr); found++; } addrman.Add(vAdd, resolveSource);
-
如果
LookupHost
方法返回結果為假,即根據當前 DNS 種子節點沒找到一個對等節點,則呼叫AddOneShot
方法進行處理。AddOneShot
方法內部簡單地把當前 DNS 種子加入vOneShots
集合。
-
2.1、CAddrMan::Add 方法
下面我們對地址管理器的 Add
方法做下介紹。這個方法位於 addrman.h
檔案的 540 行。
這個方法主體是一個 for 迴圈,遍歷 CAddress
集合,針對每一個 CAddress
物件呼叫 Add_
方法進行處理。並返回是否新增成功。程式碼如下:
bool Add(const std::vector<CAddress> &vAddr, const CNetAddr& source, int64_t nTimePenalty = 0)
{
LOCK(cs);
int nAdd = 0;
Check();
for (std::vector<CAddress>::const_iterator it = vAddr.begin(); it != vAddr.end(); it++)
nAdd += Add_(*it, source, nTimePenalty) ? 1 : 0;
Check();
if (nAdd) {
LogPrint(BCLog::ADDRMAN, "Added %i addresses from %s: %i tried, %i new\n", nAdd, source.ToString(), nTried, nNew);
}
return nAdd > 0;
}
接下來,我們來看一下 Add_
方法。這個方法在 addrman.cpp
檔案的第254行。
-
如果當前地址是不可路由的,則直接返回假。
if (!addr.IsRoutable()) return false;
-
呼叫
Find
方法,根據地址物件找到其對應的地址資訊。std::map<CNetAddr, int>::iterator it = mapAddr.find(addr); if (it == mapAddr.end()) return nullptr; if (pnId) *pnId = (*it).second; std::map<int, CAddrInfo>::iterator it2 = mapInfo.find((*it).second); if (it2 != mapInfo.end()) return &(*it2).second; return nullptr;
-
如果地址物件來源物件,設定變數
nTimePenalty
等於0。 -
如果找到對應的地址資訊,則設定地址資訊的相關屬性
bool fCurrentlyOnline = (GetAdjustedTime() - addr.nTime < 24 * 60 * 60); int64_t nUpdateInterval = (fCurrentlyOnline ? 60 * 60 : 24 * 60 * 60); if (addr.nTime && (!pinfo->nTime || pinfo->nTime < addr.nTime - nUpdateInterval - nTimePenalty)) pinfo->nTime = std::max((int64_t)0, addr.nTime - nTimePenalty); // add services pinfo->nServices = ServiceFlags(pinfo->nServices | addr.nServices); // do not update if no new information is present if (!addr.nTime || (pinfo->nTime && addr.nTime <= pinfo->nTime)) return false; // do not update if the entry was already in the "tried" table if (pinfo->fInTried) return false; // do not update if the max reference count is reached if (pinfo->nRefCount == ADDRMAN_NEW_BUCKETS_PER_ADDRESS) return false; // stochastic test: previous nRefCount == N: 2^N times harder to increase it int nFactor = 1; for (int n = 0; n < pinfo->nRefCount; n++) nFactor *= 2; if (nFactor > 1 && (RandomInt(nFactor) != 0)) return false;
-
如果沒有找到對應的地址資訊,則生成新的地址資訊。
pinfo = Create(addr, source, &nId); pinfo->nTime = std::max((int64_t)0, (int64_t)pinfo->nTime - nTimePenalty); nNew++; fNew = true;
在
Create
方法中,生成一個新的CAddrInfo
物件,並放到mapInfo
集合中,同時在在mapAddr
集合中增加對應的條目。具體程式碼如下:int nId = nIdCount++; mapInfo[nId] = CAddrInfo(addr, addrSource); mapAddr[addr] = nId; mapInfo[nId].nRandomPos = vRandom.size(); vRandom.push_back(nId); if (pnId) *pnId = nId; return &mapInfo[nId];
-
接下來處理其他一些資訊,程式碼比較簡單不詳述。
int nUBucket = pinfo->GetNewBucket(nKey, source); int nUBucketPos = pinfo->GetBucketPosition(nKey, true, nUBucket); if (vvNew[nUBucket][nUBucketPos] != nId) { bool fInsert = vvNew[nUBucket][nUBucketPos] == -1; if (!fInsert) { CAddrInfo& infoExisting = mapInfo[vvNew[nUBucket][nUBucketPos]]; if (infoExisting.IsTerrible() || (infoExisting.nRefCount > 1 && pinfo->nRefCount == 0)) { // Overwrite the existing new table entry. fInsert = true; } } if (fInsert) { ClearNew(nUBucket, nUBucketPos); pinfo->nRefCount++; vvNew[nUBucket][nUBucketPos] = nId; } else { if (pinfo->nRefCount == 0) { Delete(nId); } } }
-
返回真。
我是區小白,Ulord全球社群聯盟(優得社群)核心區塊鏈技術開發者,深入研究比特幣,以太坊,EOS Dash,Rsk,Java, Nodejs,PHP,Python,C++ 我希望能聚集更多區塊鏈開發者,一起學習共同進步。歡迎將以上問題的答案發在群中討論,或者在帖子下面留言。
往期文章:
從零開始學習比特幣開發(三)接入比特幣網路的關鍵步驟解析、建立比特幣錢包,以及重要rpc指令
從零開始學習比特幣開發(四)–網路初始化,載入區塊鏈和錢包,匯入區塊啟動節點
從零開始學習比特幣(五)–P2P網路建立的流程之套接字的讀取和傳送
原文轉載自:
從零開始學習比特幣開發(二)–如何接入比特幣網路以及原理分析
從零開始學習比特幣開發(三)–接入比特幣網路的關鍵步驟解析、建立比特幣錢包,以及重要rpc指令