1. 程式人生 > >從零開始學習比特幣開發(七)-P2P網路建立流程之生成地址對並連線到指定地址

從零開始學習比特幣開發(七)-P2P網路建立流程之生成地址對並連線到指定地址

本節繼續講解比特幣P2P網路建立流程,這節講解的執行緒為’ThreadOpenAddedConnections’,它的作用是生成地址對並連線到指定地址。
本文可以結合比特幣系統啟動的的第12步的講解來看,可以更加系統的瞭解比特幣系統啟動的過程。

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

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

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

1、ThreadSocketHandler

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

2、ThreadDNSAddressSeed

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

3、ThreadOpenAddedConnections

這個執行緒的主要作用是生成地址物件,並且呼叫 OpenNetworkConnection 方法,連線到指定地址。

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

執行緒的主體是一個 while 迴圈。在迴圈中進行下面的處理。

  1. 呼叫 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 集合。

  2. 遍歷所有的節點資訊,如果當前節點還沒有連線,進行下面的處理:

    生成地址物件 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 函式的處理。

  1. 如果 interruptNet 為真,則返回。如果網路沒有啟用(fNetworkActive 為假),則返回。

     if (interruptNet) {
         return;
     }
     if (!fNetworkActive) {
         return;
     }
    
  2. 如果引數 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;
    
  3. 呼叫 ConnectNode 方法,連線到指定地址,並返回對等節點 CNode 物件。如果連線失敗,則返回。

  4. 如果引數 grantOutbound 物件存在,則呼叫其 MoveTo 方法,進行處理。

  5. 如果引數 fOneShot 為真,則設定對等節點的 fOneShot 屬性為真。

  6. 如果是臨時探測節點(引數fFeeler 為真),則設定對等節點的 fFeeler 屬性為真。

  7. 如果是手動連線的,則設定對等節點的 m_manual_connection 屬性為真。

  8. 呼叫網路事件處理器的 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 方法,傳送版本資訊。具體訊息訊息處理部分。

  9. 把生成的對等節點儲存到 vNodes 向量中

3.1、ConnectNode

在上文講解 OpenNetworkConnection 函式“3.呼叫 ConnectNode 方法,連線到指定地址,並返回對等節點 CNode 物件” 中我提到了‘ConnectNode’方法,這個方法負責連線到具體的對等節點。我們來看下具體的處理。

  1. 如果引數 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;
        }
    }
    
  2. 如果引數 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;
            }
        }
    }
    
  3. 如果要連線的地址物件是有效的,進行下面的處理。

    呼叫 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);
    }
    
  4. 如果要連線的字串不空,且存在代理,那麼:

    呼叫 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);
    
  5. 如果以上都沒有連線到主節點,則關閉套接字並返回空指標。

     if (!connected) {
         CloseSocket(hSocket);
         return nullptr;
     }
    
  6. 最後,生成並返回主節點物件。

     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節點

原文轉載自:

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

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

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

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

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

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

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