1. 程式人生 > >從零開始學習比特幣開發(四)--網路初始化,載入區塊鏈和錢包,匯入區塊啟動節點

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

寫在前面:
本篇文章接續

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

從零開始學習區塊鏈技術(二)–如何接入比特幣網路以及其原理分析
以及從零開始學習區塊鏈技術(一)–從原始碼編譯比特幣
如果這篇文章看不明白,請務必先閱讀之前的文章。


第6步,網路初始化(src/init.cpp::AppInitMain()

  1. 生成智慧指標物件 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;
    
  2. 生成智慧指標物件 peerLogic,型別為 PeerLogicValidation

      peerLogic.reset(new PeerLogicValidation(&connman, scheduler, gArgs.GetBoolArg("-enablebip61", DEFAULT_ENABLE_BIP61)));
    

    PeerLogicValidation 繼承了 CValidationInterface、NetEventsInterface 兩個類。實現 CValidationInterface 這個類可以訂閱驗證過程中產生的事件。實現 NetEventsInterface 這個類可以處理訊息網路訊息。

  3. 註冊各種驗證處理器,即訊號處理器,在傳送訊號時會呼叫這些處理器。

      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 步應用程式初始化過程中生成。

  4. 根據命令列引數 -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);
      }
    
  5. 構造並檢查版本字串長度是否大於 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));
      }
    
  6. 如果指定了 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 方法,設定這些型別為受限的。

  7. 獲取是否允許進行 DNS 查詢,是否進行代理隨機

      fNameLookup = gArgs.GetBoolArg("-dns", DEFAULT_NAME_LOOKUP);
      bool proxyRandomize = gArgs.GetBoolArg("-proxyrandomize", DEFAULT_PROXYRANDOMIZE);
    

    兩者預設都為真。

  8. 處理網路代理。

    如果指定了 -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
    }
    
  9. 處理洋蔥網路。 如果指定了 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);
        }
    }
    
  10. 處理通過 -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));
    }
    
  11. 如果設定了 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 迴圈。

  1. 進行 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 方法處理如下:

  1. 首先,呼叫 RegisterValidationInterface 方法註冊 TxIndexMainSignalsInstance 上各種事件的訊號處理器,在傳送訊號時會呼叫這些處理器。

      RegisterValidationInterface(this);
    
  2. 然後,呼叫 Init 方法升級交易索引從老的資料庫到新的資料庫。

    TxIndex 子類過載了這個方法,會呼叫 m_db->MigrateData(*pblocktree, chainActive.GetLocator()) 方法來升級資料庫。

    然後,呼叫父類 BaseIndex 的同名方法進行處理。在父類的 Init 方法中,首先會呼叫 ReadBestBlock 方法從資料庫中讀取 Key 為 B 的區塊做為定位器(可能是所有沒有分叉的區塊)。然後,呼叫 FindForkInGlobalIndex 方法,找到活躍區塊鏈上的分叉前的最後一區塊索引(從這個區塊產生了分叉)。如果這個索引對應的區塊和活躍區塊鏈的頂端區塊是相同的,設定同步完成標誌為真。

  3. 啟動一個執行緒,執行緒執行的真正方法為 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()

  1. 呼叫 CheckDiskSpace 函式,檢查硬碟空間是否足夠。

    如果沒有足夠的硬碟空間,則退出。

  2. 檢查最佳區塊鏈頂端指示指標是否為空。

    如果頂端打針為空,UI介面進行通知。如果不空,則設定有創世區塊,即 fHaveGenesis 設為真。

    if (chainActive.Tip() == nullptr) {
        uiInterface.NotifyBlockTip_connect(BlockNotifyGenesisWait);
    } else {
        fHaveGenesis = true;
    }
    
  3. 如果指定了 blocknotify 引數,設定介面通知為 BlockNotifyCallback

  4. 遍歷引數 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));
    
  5. 獲取 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()

  1. 獲取活躍區塊鏈的當前排程。

      chain_active_height = chainActive.Height();
    
    
  2. 如果指定了監聽洋蔥網路 -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_pthreadsevthread_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_cbTorController::disconnected_cb

      • 呼叫 bufferevent_socket_connect 方法,連線到前面生成的 bufferevent。

        方法在連線成功後,會立即呼叫事件回撥函式 TorControlConnection::eventcb

    1. 呼叫 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);
    }
    
    
  3. 如果指定了 upnp 引數,則呼叫 StartMapPort 函式,開始進行埠對映。

      if (gArgs.GetBoolArg("-upnp", DEFAULT_UPNP)) {
          StartMapPort();
      }
    
    
  4. 生成選項物件,並進行初始化。

      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;
    
    

    上面的程式碼基本就是設定本地支援的服務、最大連線數、最大出站數、最大節點數、最大費率、活躍區塊鏈的高度、節點邏輯驗證器、傳送的最大緩衝值、接收的最大緩衝值、連線的節點數等。

  5. 如果指定了 -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 屬性中。

  6. 如果指定了 -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 屬性中。

  7. 如果指定了 -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 屬性中。

  8. 取得引數 seednode 指定的值,放入選項物件的 vSeedNodes 屬性中。

      connOptions.vSeedNodes = gArgs.GetArgs("-seednode");
    
    
  9. 呼叫 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()

  1. 呼叫 SetRPCWarmupFinished() 方法,設定熱身結束。

    方法內部主要設定 fRPCInWarmup 變數為假,表示熱身結束。

  2. 呼叫錢包介面物件的 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指令

原文轉載自:

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

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

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

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

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