1. 程式人生 > >從零開始學習比特幣(六)--P2P網路建立的流程之查詢DNS節點

從零開始學習比特幣(六)--P2P網路建立的流程之查詢DNS節點

上節開始我們已經開始講解比特幣系統中P2P網路是如何建立的。還記得在比特幣系統啟動的的第12步的講解中,我們提到有幾個執行緒相關的處理非常重要嗎?以下內容正是基於此做了詳細的講解。由於篇幅過長,我們分幾篇文章依次道來。

P2P 網路的建立是在比特幣系統啟動的第 12 步,最後時刻呼叫 CConnman::Start 方法開始的。

本部分內容在 net.cppnet_processing.cpp 等檔案中。

下面開始講解各個執行緒的具體處理。

1、ThreadSocketHandler

詳情見上一篇文章:

從零開始學習比特幣(五)–P2P網路建立的流程之套接字的讀取和傳送

2、ThreadDNSAddressSeed

這個執行緒的目標是,通過查詢DNS節點來找到足夠多的比特幣節點。找到之後才可以連線比特幣網路進行同步。

只有在需要地址時才查詢 DNS 種子,當我們不需要 DNS 種子時,會避免 DNS 種子查詢。這樣可以通過建立更少的識別 DNS 請求來提高使用者隱私。

執行緒定義在 net.cpp 檔案的 1603 行。下面我們開始進行具體的解讀。

  1. 如果對等節點的數量大於 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;
        }
    }
    
  2. 獲取並遍歷所有的 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 集合中。否則,進行下面的處理:

    • 生成兩個集合 vIPsvAddvIPs 集合中儲存的是 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行。

  1. 如果當前地址是不可路由的,則直接返回假。

     if (!addr.IsRoutable())
         return false;
    
  2. 呼叫 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;
    
  3. 如果地址物件來源物件,設定變數 nTimePenalty 等於0。

  4. 如果找到對應的地址資訊,則設定地址資訊的相關屬性

     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;
    
  5. 如果沒有找到對應的地址資訊,則生成新的地址資訊。

     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];
    
  6. 接下來處理其他一些資訊,程式碼比較簡單不詳述。

     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);
             }
         }
     }
    
  7. 返回真。


我是區小白,Ulord全球社群聯盟(優得社群)核心區塊鏈技術開發者,深入研究比特幣,以太坊,EOS Dash,Rsk,Java, Nodejs,PHP,Python,C++ 我希望能聚集更多區塊鏈開發者,一起學習共同進步。歡迎將以上問題的答案發在群中討論,或者在帖子下面留言。

往期文章:

從零開始學習比特幣開發(一)–從原始碼編譯比特幣

從零開始學習比特幣開發(二)如何接入比特幣網路以及步驟分析

從零開始學習比特幣開發(三)接入比特幣網路的關鍵步驟解析、建立比特幣錢包,以及重要rpc指令

從零開始學習比特幣開發(四)–網路初始化,載入區塊鏈和錢包,匯入區塊啟動節點

從零開始學習比特幣(五)–P2P網路建立的流程之套接字的讀取和傳送

原文轉載自:

優得社群–從0開始學習比特幣專題

從零開始學習比特幣開發(一)–從原始碼編譯比特幣

從零開始學習比特幣開發(二)–如何接入比特幣網路以及原理分析

從零開始學習比特幣開發(三)–接入比特幣網路的關鍵步驟解析、建立比特幣錢包,以及重要rpc指令

從零開始學習比特幣開發(四)–網路初始化,載入區塊鏈和錢包,匯入區塊啟動節點

從零開始學習比特幣(六)–P2P網路建立的流程之查詢DNS節點