七、初始化和啟動模塊(6)

(十四)繼續(xù)看bitcoind.cpp中的第168行

  fRet = AppInitMain(threadGroup, scheduler);

該行代碼是一個賦值語句,主要是調(diào)用了AppInitMain()函數(shù)。AppInitMain()函數(shù)的的聲明在init.h中的57行:

/**
 * 比特幣核心的主要初始化。
 *注意: 這只能在守護進程之后完成。 如果這個函數(shù)失敗,調(diào)用Shutdown()。
 *前提: 應(yīng)該解析參數(shù)并讀取配置文件,應(yīng)該調(diào)用AppInitLockDataDirectory。
 */
bool AppInitMain(boost::thread_group& threadGroup, CScheduler& scheduler);

該函數(shù)的聲明知道這個函數(shù)會在調(diào)用AppInitLockDataDirectory()函數(shù)之后,事實上確實是在它后面執(zhí)行的,而且是比特幣核心的主要初始化,而在實現(xiàn)文件中確實是有大量的初始化代碼的,該函數(shù)實現(xiàn)在init.cpp的1223-1735行,500多行的代碼,包括初始化bitcoin的第4a步一直到第12步共九步的初始化。
下面會對這些大量的代碼詳細說明,但是對之前提到的函數(shù)只是簡單的解釋,而且整體上也可能沒有前面那樣逐行詳細解釋,更多的是對功能塊的功能講解。


1. 選擇網(wǎng)路

這個部分在AppInitMain()函數(shù)的最開始執(zhí)行:

const CChainParams& chainparams = Params();

這個部分主要是關(guān)于運行網(wǎng)絡(luò)的選擇(運行網(wǎng)絡(luò)前面的四、初始化和啟動模塊(3)已經(jīng)說過了,有私有網(wǎng)絡(luò)、測試網(wǎng)絡(luò)和主網(wǎng)絡(luò),默認(rèn)為主網(wǎng)絡(luò)),如果我們在啟動比特幣核心程序時,沒有設(shè)置相應(yīng)網(wǎng)絡(luò)參數(shù),則默認(rèn)運行主鏈,否則將根據(jù)輸入的參數(shù)啟動相應(yīng)網(wǎng)絡(luò)。

2. 第4a步:應(yīng)用程序初始化
2-1 pid文件

這個是一段條件編譯代碼:

#ifndef WIN32
    CreatePidFile(GetPidFile(), getpid());
#endif

這個主要是在非Windows系統(tǒng)中編譯的部分,通過CreatePidFile()函數(shù)創(chuàng)建進程編號記錄文件,該文件名為bitcoind.pid。
通過在ubuntu系統(tǒng)中對編譯好的bitcoin客戶端在終端執(zhí)行bitcoind命令的嘗試后,發(fā)現(xiàn)在bitcoin的數(shù)據(jù)目錄文件夾$HOME/.bitcoin中會立刻出現(xiàn)三個新的文件和文件夾如圖所示:

新出現(xiàn)的部分
其中就會出現(xiàn)bitcoind.pid文件,而這個文件在未執(zhí)行這個命令或者終止執(zhí)行該命令時是沒有的。
bitcoind.pid文件中記錄了比特幣核心的進程號,如圖所示:
bitcoind.pid記錄進程號
示例圖中說明該比特幣核心的進程號為2570。
該文件的目的其實和前面的目錄鎖文件.lock相似,它是為了防止出現(xiàn)多個比特幣核心程序,始終在文件中記錄第一個啟動的程序的pid,防止后面新啟動的比特幣程序再使用它。
pid文件的詳細作用可以參考下面的資料:
http://blog.csdn.net/yinqingwang/article/details/52841744

2-2調(diào)試日志輸出

(1)在AppInitMain()函數(shù)第4a步中也牽涉到一些調(diào)試日志輸出到日志文件debug.log中情況,其中最開始會有個-shrinkdebugfile參數(shù)的處理:

if (gArgs.GetBoolArg("-shrinkdebugfile", logCategories == BCLog::NONE)) {
        //首先這樣做,不僅因為它能將一堆debug.log加載到內(nèi)存中去,而且因為這也需要在任何其他debug.log打印之前發(fā)生。
        ShrinkDebugFile();
    }

其中-shrinkdebugfile參數(shù)為壓縮日志文件。該參數(shù)在幫助文件中的解釋為:當(dāng)客戶端啟動時,對debug.log文件進行壓縮處理。默認(rèn)為1,即在不進行調(diào)試時會進行壓縮操作。此處的含義為,當(dāng)啟動不使用(一般都不會使用)該參數(shù)時會默認(rèn)執(zhí)行ShrinkDebugFile()函數(shù),該函數(shù)就是具體的調(diào)試日志壓縮處理過程函數(shù)。
(2)在對debug.log文件進行壓縮處理后,會有兩行代碼:

    if (fPrintToDebugLog)
        OpenDebugLog();

因為fPrintToDebugLog變量(定義在util.cpp第95行)默認(rèn)為true,則程序?qū)⒂?code>OpenDebugLog()函數(shù)正式打開debug.log文件,實現(xiàn)程序運行過程中的記錄,方便調(diào)試用。
其中OpenDebugLog()函數(shù)(定義在util.cpp的192行)完成的是debug.log文件的打開,打開方式是增加內(nèi)容“a”模式,即啟動后程序?qū)⒃谏弦淮稳罩拘畔⒌幕A(chǔ)上添加本次運行的日志內(nèi)容。
OpenDebugLog()函數(shù)中也出現(xiàn)了vMsgsBeforeOpenLog包含內(nèi)容的打印輸出,vMsgsBeforeOpenLog為日志文件未打開之前,預(yù)先存儲的一些打印輸出信息(預(yù)先存儲信息參考四、初始化和啟動模塊(3)的第(八)部分)。
至此完成了日志文件的打開操作,并完成了預(yù)先存儲日志信息的輸出。
(3)緊接著是時間戳信息在日志文件中的輸出處理代碼:

    if (!fLogTimestamps)
        LogPrintf("Startup time: %s\n", DateTimeStrFormat("%Y-%m-%d %H:%M:%S", GetTime()));

fLogTimestamps的定義在util.cpp的第97行,默認(rèn)值為DEFAULT_LOGTIMESTAMPS,而DEFAULT_LOGTIMESTAMPS在util.h中的第37行,為true,意味著一般if后的語句不執(zhí)行,即此處不單獨打印“Startup time:(+時間)”這樣的時間信息。fLogTimestamps主要是默認(rèn)在讓日志的每一行都帶有時間戳信息,而像如下圖所示的:

時間戳+輸入內(nèi)容格式
這種時間戳加輸入的日志內(nèi)容這樣格式的實現(xiàn)代碼,是在位于util.cpp中的300-326行的LogTimestampStr()函數(shù)中。
(4)接下來就是在日志文件中輸出4行的日志內(nèi)容:

    LogPrintf("Default data directory %s\n", GetDefaultDataDir().string());
    LogPrintf("Using data directory %s\n", GetDataDir().string());
    LogPrintf("Using config file %s\n", GetConfigFile(gArgs.GetArg("-conf", BITCOIN_CONF_FILENAME)).string());
    LogPrintf("Using at most %i automatic connections (%i file descriptors available)\n", nMaxConnections, nFD);

輸出的結(jié)果如下圖所示:

其中調(diào)用了三個函數(shù)得到路徑和兩個變量得到最大連接數(shù)和文件描述符個數(shù)。

2-3 簽名緩存信息

接著會遇到一個簽名緩存函數(shù):

 InitSignatureCache();

這個函數(shù)聲明在script/sigcache.h中54行,實現(xiàn)在script/sigcache.cpp中73-81行:

// 要在AppInitMain()函數(shù)的基本測試設(shè)置中調(diào)用一次以初始化簽名緩存。
void InitSignatureCache()
{
    // nMaxCacheSize是無符號類型變量。如果-maxsigcachesize參數(shù)設(shè)置成0,
    // setup_bytes()函數(shù)將會創(chuàng)建最小可能的緩存 (2個元素)。
    size_t nMaxCacheSize = std::min(std::max((int64_t)0, gArgs.GetArg("-maxsigcachesize", DEFAULT_MAX_SIG_CACHE_SIZE) / 2), MAX_MAX_SIG_CACHE_SIZE) * ((size_t) 1 << 20);
    size_t nElems = signatureCache.setup_bytes(nMaxCacheSize);
    LogPrintf("Using %zu MiB out of %zu/2 requested for signature cache, able to store %zu elements\n",
            (nElems*sizeof(uint256)) >>20, (nMaxCacheSize*2)>>20, nElems);
}

此處有個-maxsigcachesize參數(shù),此參數(shù)在幫助文件中的解釋為:將簽名緩存和腳本執(zhí)行緩存大小的總和限制為n MB,默認(rèn)為32MB。此處的設(shè)置為:如果設(shè)置了-maxsigcachesize參數(shù),而且大于32MB,那么nMaxCacheSize變量值將是-maxsigcachesize參數(shù)設(shè)定值的一半或者16384這兩個數(shù)中的最小者,然后把該值乘以1048576(2^20)。
nMaxCacheSize變量值也是最大簽名緩存大小值。nElems變量就是在最大簽名緩存時能存儲的簽名數(shù)。然后在日志文件中輸出最大簽名緩存大小、簽名數(shù)等信息。

2-4 腳本執(zhí)行緩存信息

接著會出現(xiàn)一個初始化腳本執(zhí)行緩存函數(shù):

InitScriptExecutionCache();

該函數(shù)和上面的簽名緩存信息函數(shù)類似,該函數(shù)的聲明在validation.h中第393行:

/** 初始化腳本執(zhí)行緩存 */
void InitScriptExecutionCache();

該函數(shù)的實現(xiàn)代碼在validation.cpp中的1236-1243行:

void InitScriptExecutionCache() {
    // nMaxCacheSize is unsigned. If -maxsigcachesize is set to zero,
    // setup_bytes creates the minimum possible cache (2 elements).
    size_t nMaxCacheSize = std::min(std::max((int64_t)0, gArgs.GetArg("-maxsigcachesize", DEFAULT_MAX_SIG_CACHE_SIZE) / 2), MAX_MAX_SIG_CACHE_SIZE) * ((size_t) 1 << 20);
    size_t nElems = scriptExecutionCache.setup_bytes(nMaxCacheSize);
    LogPrintf("Using %zu MiB out of %zu/2 requested for script execution cache, able to store %zu elements\n",
            (nElems*sizeof(uint256)) >>20, (nMaxCacheSize*2)>>20, nElems);
}

可以比較發(fā)現(xiàn)這個函數(shù)和InitSignatureCache()函數(shù)的數(shù)的計算完全一樣,就是把一些名詞修改了,在日志文件中可以得到如下圖所示的結(jié)果:

可見這兩個函數(shù)是完全類似的功能,此處的腳本執(zhí)行緩存和之前的簽名緩存相等。

2-5 腳本驗證線程

接著會出現(xiàn)一段關(guān)于腳本驗證線程的代碼:

 LogPrintf("Using %u threads for script verification\n", nScriptCheckThreads);
 if (nScriptCheckThreads) {
    for (int i=0; i<nScriptCheckThreads-1; i++)
        threadGroup.create_thread(&ThreadScriptCheck);
    }

開始是在日志文件中打印消息:

日志打印示例
“使用0個線程進行腳本驗證?!?br> 變量nScriptCheckThreads定義在validation.cpp為0。后面的判斷語句為如果該變量不為0,假設(shè)為n,則會創(chuàng)建n個線程用來驗證腳本。

2-6 輕量級任務(wù)調(diào)度
    // 啟動輕量級任務(wù)調(diào)度線程
    CScheduler::Function serviceLoop = boost::bind(&CScheduler::serviceQueue, &scheduler);
    threadGroup.create_thread(boost::bind(&TraceThread<CScheduler::Function>, "scheduler", serviceLoop));

從注釋文件中可以知道:這部分的功能就是啟動一個輕量級的程序,這個程序功能是用來做任務(wù)調(diào)度的。
(1)代碼第一行的效果就是:用boost::bind()函數(shù)綁定類的成員函數(shù)serviceQueue()和類的對象scheduler,最后的serviceLoop函數(shù)對象就可以等效于scheduler.serviceQueue();
(2)代碼的第二行通過線程組對象threadGroup實例化create_thread()函數(shù)來創(chuàng)建新的線程,線程的執(zhí)行函數(shù)為boost::bind()函數(shù)返回的函數(shù)對象。
bind()函數(shù)中有個TraceThread()函數(shù),該函數(shù)在util.h的295行實現(xiàn),該函數(shù)的功能在注釋文件中指的是:這是一個封裝器,而且僅調(diào)用func一次。由該函數(shù)的表示:TraceThread<CScheduler::Function>可以知道:該函數(shù)在實際調(diào)用時傳入的函數(shù)類型為CScheduer::Function。結(jié)合bind()函數(shù)就是:TraceThread()函數(shù)實例化對象為serviceLoop函數(shù)。其中還有第一個參數(shù)為"scheduler",這個參數(shù)就是重命名線程為scheduler。
(3)整體來看這兩段代碼就是:創(chuàng)建一個線程,該線程將調(diào)用一次scheduler.serviceQueue()決定的serviceLoop函數(shù),并且會重新命名該線程為scheduler。而serviceQueue()函數(shù)就是關(guān)于任務(wù)調(diào)度的函數(shù)。

2-7 注冊后臺處理信號
GetMainSignals().RegisterBackgroundSignalScheduler(scheduler);

該段代碼中GetMainSignals()在validationinterface.cpp中53行實現(xiàn):

CMainSignals& GetMainSignals()
{
    return g_signals;
}

它的返回值為:g_signals,這是一個CMainSignals類型的靜態(tài)變量。那么該行代碼就是調(diào)用CMainSignals類中的成員函數(shù)RegisterBackgroundSignalScheduler()該成員函數(shù)是一個公開方法,在validationinterface.h的79行聲明:

 /**注冊一個CScheduler,為了給應(yīng)該在后臺運行的回調(diào)(只能被調(diào)用一次)。 */
 void RegisterBackgroundSignalScheduler(CScheduler& scheduler);

該函數(shù)的實現(xiàn)在validationinterface.cpp的40行:

void CMainSignals::RegisterBackgroundSignalScheduler(CScheduler& scheduler) {
    assert(!m_internals);
    m_internals.reset(new MainSignalsInstance(&scheduler));
}

可以知道:通過這個函數(shù),將CMainSignals類中的unique_ptr<MainSignalsInstance>指針類型的成員變量m_internals賦值為一個新建的對象,這個新建的對象類型為結(jié)構(gòu)體MainSignalsInstance。總之這個函數(shù)是設(shè)置變量m_internals成為結(jié)構(gòu)體MainSignalsInstance類型,用來注冊后臺處理信號。

2-8 啟動RPCServer、HTTPServer

接下來是一段判斷參數(shù)的代碼:

    /* 已經(jīng)啟動RPC服務(wù)器。 它將以“熱身”模式啟動進程,而不是已經(jīng)真正的進程調(diào)用(但它將表示服務(wù)器在那里并且稍后準(zhǔn)備就緒的連接)。 初始化完成后,熱身模式將被禁用。
     */
    if (gArgs.GetBoolArg("-server", false))
    {
        uiInterface.InitMessage.connect(SetRPCWarmupStatus);
        if (!AppInitServers(threadGroup))
            return InitError(_("Unable to start HTTP server. See debug log for details."));
    }

(1)此處涉及到一個參數(shù):-server
該函數(shù)在幫助文件中的解釋為:接受命令行和JSON-RPC命令。此處的判斷語句為:如果在命令行中有-server命令,就執(zhí)行下面的語句,如果沒有該命令則不執(zhí)行。
(2)在執(zhí)行語句中首先給InitMessage信號添加一個新的執(zhí)行函數(shù)SetRPCWarmupStatus,該執(zhí)行函數(shù)的聲明在rpc/server.h中的61行:

/**
 * 設(shè)置RPC預(yù)熱狀態(tài)。 完成此操作后,所有的RPC調(diào)用都將立即通過RPC_IN_WARMUP出錯。
 */
void SetRPCWarmupStatus(const std::string& newStatus);

函數(shù)的實現(xiàn)在rpc/server.cpp中340行:

void SetRPCWarmupStatus(const std::string& newStatus)
{
    LOCK(cs_rpcWarmup);
    rpcWarmupStatus = newStatus;
}

其中rpcWarmupStatus是一個靜態(tài)的string類型的全局變量,則函數(shù)的作用就是將新的參數(shù)值賦給rpcWarmupStatus變量,完成RPC預(yù)熱狀態(tài)的設(shè)置。
(3)然后又會出現(xiàn)一個判斷語句,這個判斷語句主要涉及到AppInitServers()函數(shù)的調(diào)用。該函數(shù)的實現(xiàn)在init.cpp中的725行:

bool AppInitServers(boost::thread_group& threadGroup)
{
    RPCServer::OnStarted(&OnRPCStarted);
    RPCServer::OnStopped(&OnRPCStopped);
    RPCServer::OnPreCommand(&OnRPCPreCommand);
    if (!InitHTTPServer())
        return false;
    if (!StartRPC())
        return false;
    if (!StartHTTPRPC())
        return false;
    if (gArgs.GetBoolArg("-rest", DEFAULT_REST_ENABLE) && !StartREST())
        return false;
    if (!StartHTTPServer())
        return false;
    return true;
}

①從該函數(shù)的代碼中可以知道:它首先的三行是調(diào)用了RPCServer類中的三個函數(shù),這三個函數(shù)的功能分別是連接到各自對應(yīng)的信號槽,這三個信號槽分別做了一些信號的連接工作:OnRPCStarted負(fù)責(zé)將RPCNotifyBlockChange連接到NotifyBlockTip信號上;OnRPCStopped負(fù)責(zé)將連接解除,并做一些其他的清除工作;OnRPCPreCommand檢查在安全模式下是否有警告消息,如果有那么就拋出相應(yīng)的異常。
InitHTTPServer():初始化http server。
從這個函數(shù)開始的為5個判斷語句,調(diào)用了5個不同開啟服務(wù)函數(shù),當(dāng)都開啟成功后才返回true,否則返回false。現(xiàn)在我們先看第一個函數(shù):InitHTTPServer(),這個是初始化http server的。該函數(shù)的聲明在httpserver.h中的24行:

/** 初始化HTTP服務(wù)器。 在RegisterHTTPHandler或EventBase()之前調(diào)用它。
 */
bool InitHTTPServer();

由注釋也只道這是一個初始化HTTP服務(wù)器的函數(shù)。該函數(shù)的實現(xiàn)在httpserver.cpp中的378-434行:

bool InitHTTPServer()
{
    if (!InitHTTPAllowList())
        return false;

    if (gArgs.GetBoolArg("-rpcssl", false)) {
        uiInterface.ThreadSafeMessageBox(
            "SSL mode for RPC (-rpcssl) is no longer supported.",
            "", CClientUIInterface::MSG_ERROR);
        return false;
    }

    //將libevent的日志記錄重定向到我們自己的日志
    event_set_log_callback(&libevent_log_cb);
    //更新libevent的日志處理。 如果我們的libevent版本不支持調(diào)試日志記錄,則返回false,
    //在這種情況下,我們應(yīng)該清除BCLog :: LIBEVENT標(biāo)志。
    if (!UpdateHTTPServerLogging(logCategories & BCLog::LIBEVENT)) {
        logCategories &= ~BCLog::LIBEVENT;
    }

#ifdef WIN32
    evthread_use_windows_threads();
#else
    evthread_use_pthreads();
#endif

    raii_event_base base_ctr = obtain_event_base();

    /* 創(chuàng)建一個新的evhttp對象來處理請求。 */
    raii_evhttp http_ctr = obtain_evhttp(base_ctr.get());
    struct evhttp* http = http_ctr.get();
    if (!http) {
        LogPrintf("couldn't create evhttp. Exiting.\n");
        return false;
    }

    evhttp_set_timeout(http, gArgs.GetArg("-rpcservertimeout", DEFAULT_HTTP_SERVER_TIMEOUT));
    evhttp_set_max_headers_size(http, MAX_HEADERS_SIZE);
    evhttp_set_max_body_size(http, MAX_SIZE);
    evhttp_set_gencb(http, http_request_cb, nullptr);

    if (!HTTPBindAddresses(http)) {
        LogPrintf("Unable to bind any endpoint for RPC server\n");
        return false;
    }

    LogPrint(BCLog::HTTP, "Initialized HTTP server\n");
    int workQueueDepth = std::max((long)gArgs.GetArg("-rpcworkqueue", DEFAULT_HTTP_WORKQUEUE), 1L);
    LogPrintf("HTTP: creating work queue of depth %d\n", workQueueDepth);

    workQueue = new WorkQueue<HTTPClosure>(workQueueDepth);
    // 通過.release()將所有權(quán)轉(zhuǎn)移到eventBase / HTTP
    eventBase = base_ctr.release();
    eventHTTP = http_ctr.release();
    return true;
}

(a)380-381,這兩行是初始化HTTP Server。首先通過InitHTTPAllowList()函數(shù)來初始化訪問控制列表,函數(shù)中有個變量rpc_allow_subnets,它是一個vector類型的變量,它的作用是保存所有允許訪問的主機ip。這個函數(shù)的作用是:首先添加本地的地址,然后從命令行中的-rpcallowip參數(shù)讀取ip列表,讀取時檢測地址的有效性,最后打印出允許訪問的所有ip列表。
(b)383-388,這部分是看命令行中是否設(shè)置了-rpcssl參數(shù),由于目前的版本不支持ssl,所以如果設(shè)置了這個參數(shù)就報錯。
(c)391-397行就是將libevent的日志記錄重定向到我們自己的日志。
(d)最后的399-434都是初始化基于libevent的http協(xié)議。
首先代碼根據(jù)系統(tǒng)環(huán)境判斷使用windows線程還是其他環(huán)境下的線程,接下來就是基于libevent的http協(xié)議的實現(xiàn):

采用libevent的原因有以下幾個方面:

  • 事件驅(qū)動,高性能;
  • 輕量級,專注于網(wǎng)絡(luò);
  • 跨平臺,支持各主流平臺;
  • 支持多種I/O多路復(fù)用技術(shù),epoll、poll、dev/poll、select和kqueue等;
  • 支持I/O,定時器和信號等事件。

基于libevent實現(xiàn)的http協(xié)議主要有以下這么幾個步驟:

  1. event_base base = event_base_new(),首先新建event_base對象;
  2. evhttp http = evhttp_new(base),然后新建evhttp對象;
  3. evhttp_bind_socket(http, "0.0.0.0", port),接下來綁定ip地址和端口;
  4. evhttp_set_gencb(http, http_call_back, NULL),然后設(shè)置請求處理函數(shù)http_call_back;
  5. event_base_dispatch(base), 最后派發(fā)事件循環(huán)

在這里使用的基于libevent的http協(xié)議基本上就是把簡單版的http協(xié)議封裝了一下,原來的變量類型也都換了一個新的類型,這個新類型的前面都加上了raii,raii就是為了避免申請內(nèi)存但是沒有釋放從而導(dǎo)致內(nèi)存泄漏的情況出現(xiàn)所使用的一種技術(shù),這種技術(shù)能夠在對象離開作用域是自動釋放。關(guān)于RALL詳細信息可以參考:
http://blog.csdn.net/doc_sgl/article/details/43028009

http server 請求回調(diào)函數(shù)
415-417行的三個evhttp_set_xxx函數(shù)都是設(shè)置連接的限制條件,http_request_cb為請求的回調(diào)函數(shù)。

StartRPC():啟動RPC服務(wù)。
繼續(xù)看StartRPC()函數(shù),該函數(shù)聲明在rpc/server.h中的191行,實現(xiàn)在rpc/server.cpp的312行:

bool StartRPC()
{
    LogPrint(BCLog::RPC, "Starting RPC\n");
    fRPCRunning = true;
    g_rpcSignals.Started();
    return true;
}

啟動RPC就是將之前的連接到Started的信號全部觸發(fā)運行,并修改變量fRPCRunning為true。
StartHTTPRPC():啟動HTTP RPC服務(wù)。
再看StartHTTPRPC()函數(shù),該函數(shù)聲明在httprpc.h的14行:

/** 啟動HTTP RPC子系統(tǒng)。
 * 前提:HTTP和RPC已經(jīng)被啟動了。
 */
bool StartHTTPRPC();

該函數(shù)的實現(xiàn)在httprpc.cpp的229-244行:

bool StartHTTPRPC()
{
    LogPrint(BCLog::RPC, "Starting HTTP RPC server\n");
    if (!InitRPCAuthentication())
        return false;

    RegisterHTTPHandler("/", true, HTTPReq_JSONRPC);
#ifdef ENABLE_WALLET
    //一旦我們切換到更好的端點支持和API版本,ifdef可以被刪除。
    RegisterHTTPHandler("/wallet/", false, HTTPReq_JSONRPC);
#endif
    assert(EventBase());
    httpRPCTimerInterface = new HTTPRPCTimerInterface(EventBase());
    RPCSetTimerInterface(httpRPCTimerInterface);
    return true;
}

此時會由一個函數(shù):InitRPCAuthentication(),用來驗證用戶的身份,并且這個函數(shù)只進行了驗證環(huán)境的初始化,還沒有進行真正的驗證過程。
然后會由RegisterHTTPHandler()函數(shù)來注冊url處理函數(shù)。該函數(shù)的第一個參數(shù)是請求的路徑,第二個true表示的是精確匹配,最后一個參數(shù)是處理的函數(shù)。緊跟著相同的會有個條件編譯語句,也是由RegisterHTTPHandler()函數(shù)來控制,第一個參數(shù)是請求的路徑,第二個false表示的是前綴匹配,最后一個參數(shù)是處理的函數(shù)。
最后是設(shè)置http定時器接口,就是在指定時間間隔后執(zhí)行某個函數(shù)一次。
StartREST():啟動REST。
然后就是啟動REST服務(wù)了,在該部分會有一個參數(shù)的判斷,這個參數(shù)是-rest,該參數(shù)在幫助文件中的解釋為:接受公共REST請求。默認(rèn)為false。此處的含義是:通過命令行的-rest命令能表示啟動REST服務(wù)。REST服務(wù)具體就是將一堆URL路徑和對應(yīng)的處理函數(shù)通過RegisterHTTPHandler函數(shù)存儲到pathHandlers中,以便在對應(yīng)的請求到達時能調(diào)用對應(yīng)的函數(shù)進行處理,這里面使用的都是前綴匹配。
StartHTTPServer():啟動HTTP server。
這個是最后一個函數(shù)了,該函數(shù)聲明在httpserver.h中的29行:

/** 啟動HTTP服務(wù)。
 * 這與InitHTTPServer是分開的,為用戶提供無競爭條件的時間來在InitHTTPServer和StartHTTPServer之間注冊它們的處理程序。
 */
bool StartHTTPServer();

該函數(shù)的實現(xiàn)在httpserver.cpp的453-467行:

bool StartHTTPServer()
{
    LogPrint(BCLog::HTTP, "Starting HTTP server\n");
    int rpcThreads = std::max((long)gArgs.GetArg("-rpcthreads", DEFAULT_HTTP_THREADS), 1L);
    LogPrintf("HTTP: starting %d worker threads\n", rpcThreads);
    std::packaged_task<bool(event_base*, evhttp*)> task(ThreadHTTP);
    threadResult = task.get_future();
    threadHTTP = std::thread(std::move(task), eventBase, eventHTTP);

    for (int i = 0; i < rpcThreads; i++) {
        std::thread rpc_worker(HTTPWorkQueueRun, workQueue);
        rpc_worker.detach();
    }
    return true;
}

程序首先從命令行中通過-rpcthreads參數(shù)獲取rpc執(zhí)行的最大線程數(shù),接下來使用了<future>庫中的packaged_task類創(chuàng)建了一個task對象,然后綁定了函數(shù)ThreadHTTP,并將返回最終的結(jié)果保存在threadResult中,再然后創(chuàng)建了線程threadHTTP來執(zhí)行任務(wù)。

thread()函數(shù)中第一個參數(shù)使用了std::move()函數(shù),這個函數(shù)作用是返回輸出參數(shù)的右值類型,與右值相對應(yīng)的有左值類型,這兩者的區(qū)別是:右值類型只能出現(xiàn)在賦值語句的右邊,一般的情況有常數(shù)、臨時變量(函數(shù)返回值)等;左值則可以出現(xiàn)在等號的兩邊,同時需要進行初始化,普通的變量都是左值類型。

接下來的程序根據(jù)命令行設(shè)置的rpc線程數(shù)創(chuàng)建對應(yīng)的rpc_worker來執(zhí)行workQueue,創(chuàng)建完線程之后便讓線程從當(dāng)前線程脫離出去,通過detach()操作,交給了系統(tǒng)去管理。

AppInitServers()函數(shù)總結(jié):
AppInitServers()函數(shù)主要就是HTTP Server的初始化,將外部的請求和內(nèi)部相應(yīng)的處理函數(shù)對應(yīng)起來,并做好相應(yīng)的任務(wù)分配。

2-9 定義變量nStart

這個是第4a步的最后了,就是一個簡單的變量的定義,此時定義變量名為nStart,該變量的類型為int64_t。

********************************************
第4a步總結(jié):
這個步驟為應(yīng)用程序的初始化:
包括啟動時創(chuàng)建一個.pid文件來保證只有一個比特幣核心程序運行;啟動時壓縮debug.log文件,打開日志文件,并增加一些調(diào)試日志,包括在每行輸出內(nèi)容前加時間戳、數(shù)據(jù)文件路徑、簽名和腳本執(zhí)行的緩存信息等;然后打開腳本驗證線程并啟動一個輕量級的用來做任務(wù)調(diào)度的線程;最后注冊后臺處理信號并啟動RPCServer、HTTPServer服務(wù)。
總之是一個進一步保證程序的唯一性、完善調(diào)試日志內(nèi)容、開啟任務(wù)調(diào)度線程、啟動RPC和HTTP服務(wù)等的初始化步驟。
********************************************


3. 第五步:驗證錢包數(shù)據(jù)庫的完整性

這一步只有簡單的四行代碼:

   // ********第五步: 驗證錢包數(shù)據(jù)庫的完整性
#ifdef ENABLE_WALLET
    if (!CWallet::Verify())
        return false;
#endif

這是一個條件編譯程序段:如果有ENABLE_WALLET宏定義,則執(zhí)行CWallet::Verify()函數(shù)。
(1)對于ENABLE_WALLET宏定義在config\bitcoind-config.h中有定義,為1:

/* 定義為1以啟用錢包功能 */
#define ENABLE_WALLET 1

可以知道定義為1為啟動錢包功能。那么如果總會是1的話,為什么在這里加上一個條件編譯語句呢?通過思考,我找到前面我寫過的一篇編譯源碼過程的文章,我發(fā)現(xiàn)在編譯源碼時是可以禁用錢包功能的:

禁用錢包功能
只是我在編譯時使用的是:

./configure --without-gui --with-incompatible-bdb

當(dāng)不使用禁用錢包命令時,默認(rèn)是開啟錢包功能的,所以我這里會有ENABLE_WALLET宏定義,且值是1,默認(rèn)開啟的。
(2)CWallet::Verify()函數(shù)就是驗證錢包數(shù)據(jù)庫的完整性了。該函數(shù)的聲明在wallet/wallet.h的1064行:

    //! 負(fù)責(zé)閱讀和驗證-wallet參數(shù)并驗證錢包數(shù)據(jù)庫。
    //  只要有一個錢包被加載,這個功能就會對錢包進行補救。
    // (CWallet::ParameterInteraction forbids -salvagewallet, -zapwallettxes or -upgradewallet with multiwallet).
    static bool Verify();

該函數(shù)的實現(xiàn)在wallet/wallet.cpp中的497-552行:

bool CWallet::Verify()
{
    if (gArgs.GetBoolArg("-disablewallet", DEFAULT_DISABLE_WALLET))
        return true;

    uiInterface.InitMessage(_("Verifying wallet(s)..."));

    // 跟蹤每個錢包絕對路徑以檢測重復(fù)項。
    std::set<fs::path> wallet_paths;

    for (const std::string& walletFile : gArgs.GetArgs("-wallet")) {
        if (boost::filesystem::path(walletFile).filename() != walletFile) {
            return InitError(strprintf(_("Error loading wallet %s. -wallet parameter must only specify a filename (not a path)."), walletFile));
        }

        if (SanitizeString(walletFile, SAFE_CHARS_FILENAME) != walletFile) {
            return InitError(strprintf(_("Error loading wallet %s. Invalid characters in -wallet filename."), walletFile));
        }

        fs::path wallet_path = fs::absolute(walletFile, GetDataDir());

        if (fs::exists(wallet_path) && (!fs::is_regular_file(wallet_path) || fs::is_symlink(wallet_path))) {
            return InitError(strprintf(_("Error loading wallet %s. -wallet filename must be a regular file."), walletFile));
        }

        if (!wallet_paths.insert(wallet_path).second) {
            return InitError(strprintf(_("Error loading wallet %s. Duplicate -wallet filename specified."), walletFile));
        }

        std::string strError;
        if (!CWalletDB::VerifyEnvironment(walletFile, GetDataDir().string(), strError)) {
            return InitError(strError);
        }

        if (gArgs.GetBoolArg("-salvagewallet", false)) {
            // 恢復(fù)可讀密鑰對:
            CWallet dummyWallet;
            std::string backup_filename;
            if (!CWalletDB::Recover(walletFile, (void *)&dummyWallet, CWalletDB::RecoverKeysOnlyFilter, backup_filename)) {
                return false;
            }
        }

        std::string strWarning;
        bool dbV = CWalletDB::VerifyDatabaseFile(walletFile, GetDataDir().string(), strWarning, strError);
        if (!strWarning.empty()) {
            InitWarning(strWarning);
        }
        if (!dbV) {
            InitError(strError);
            return false;
        }
    }

    return true;
}

①此函數(shù)首先判斷命令行中是否含有-disablewallet命令,如果含有這個命令則表示禁用了錢包功能,那么就沒有接下來檢查完整性的必要了,就直接返回true,結(jié)束該函數(shù)。
////////////////////////////////////////////////////////////
注意:
看到這出現(xiàn)了兩個禁用錢包功能命令:一個是在編譯時的

./configure --without-gui --disable-wallet

命令,這個是在編譯時禁用錢包功能,決定宏定義ENABLE_WALLET是否包含在程序中,此時的禁用錢包功能是完全的禁用,就是客戶端中根本就沒有這個模塊;而-disablewallet命令是在編譯時沒有關(guān)閉錢包功能(默認(rèn)就是打開錢包功能),在執(zhí)行CWallet::Verify()函數(shù)來檢驗錢包數(shù)據(jù)庫的完整性時,因為檢測到在命令行中輸入了-disablewallet命令就知道運行客戶端時禁止了錢包功能,就會立刻結(jié)束函數(shù)的運行。但是這個是暫時的禁止錢包功能,如果下次重新打開客戶端,不輸入這個命令,就會繼續(xù)執(zhí)行下面的代碼。
總之,前面的禁用錢包功能是在客戶端沒有完成的編譯過程中的禁止,它是去除錢包功能模塊,是“根本就沒有錢包功能(想用都用不了)”;而這個-disablewallet命令是帶有錢包功能的客戶端在運行是刻意加上這個命令來禁止錢包功能,是“有錢包功能,但就是不用(有卻不用)”。
////////////////////////////////////////////////////////////

②接下來對于命令行中傳入的每一個錢包路徑,首先檢查文件的路徑、是否包含非法字符、是否是regular file或者鏈接、是否有相同的文件等。
③然后用CWalletDB::VerifyEnvironment()函數(shù)驗證錢包的環(huán)境。
該函數(shù)傳入錢包文件名和錢包的絕對路徑,首先檢查錢包文件名是否是不包含路徑的純文件名,接下來調(diào)用一個CDBEnv類型變量bitdb中的open函數(shù)(open函數(shù)首先檢查數(shù)據(jù)庫文件是否存在,不存在就立即創(chuàng)建;然后設(shè)置日志文件,并且通過DbEnv類型變量指針dbenv設(shè)置了一系列數(shù)據(jù)庫運行的相關(guān)參數(shù))。open函數(shù)執(zhí)行成功的話,就直接返回true;否則就進入if語句,之后首先將原來的錢包數(shù)據(jù)庫文件進行備份,然后再次嘗試調(diào)用open函數(shù),創(chuàng)建數(shù)據(jù)庫文件。
④然后會有一個判斷是否含有-salvagewallet命令的判斷語句,其中-salvagewallet命令通過參照幫助文件可以知道它的作用是:嘗試在啟動時從損壞的錢包中恢復(fù)私鑰。所以后面的代碼都是執(zhí)行在錢包中恢復(fù)私鑰的作用的,這個工作是由CWalletDB::Recover()函數(shù)完成的。這個函數(shù)中有4個重要的參數(shù):

  • filename:待恢復(fù)的錢包文件名;
  • callbackDataIn:恢復(fù)數(shù)據(jù)寫入對象;
  • recoverKVcallback:回調(diào)函數(shù),用來將恢復(fù)數(shù)據(jù)寫入callbackDataIn;
  • newFilename:備份文件名。

私鑰恢復(fù)的步驟是首先備份原來的錢包文件,然后調(diào)用CDBEnv類中的Salvage函數(shù),這個函數(shù)實現(xiàn)的功能是從文件中將公私鑰讀取出來并保存在到salvagedData中?;謴?fù)完之后就將恢復(fù)的數(shù)據(jù)寫入到本地數(shù)據(jù)庫中,這個寫入的過程都是通過pdbCopy對象來進行的,同時如果定義了recoverKVcallback函數(shù),那么還同時寫入到callbackDataIn對象中,用于傳給上層調(diào)用函數(shù)。
⑤最后會出現(xiàn)一個CWalletDB::VerifyDatabaseFile()函數(shù),這個函數(shù)主要是用來驗證數(shù)據(jù)庫文件的。這個函數(shù)的最終實現(xiàn)是:如果驗證通過的話那么就直接返回VERIFY_OK;否則就先看是否設(shè)置了恢復(fù)函數(shù),如果沒有就返回RECOVER_FAIL;如果設(shè)置了恢復(fù)函數(shù),那么就調(diào)用恢復(fù)函數(shù),并返回恢復(fù)函數(shù)執(zhí)行的結(jié)果。由前面可以發(fā)現(xiàn)恢復(fù)函數(shù)被設(shè)置成了CWalletDB::Recover函數(shù),所以這里都會調(diào)用這個函數(shù)。

********************************************
第五步總結(jié):
這一步主要是驗證錢包數(shù)據(jù)庫的完整性的,當(dāng)編譯時沒有禁用錢包功能(默認(rèn)開啟錢包功能)時會執(zhí)行這一步,主要由Verify()函數(shù)來完成驗證工作:包括是否在命令行禁用錢包功能、檢查錢包路徑、驗證錢包環(huán)境、恢復(fù)私鑰、驗證錢包的數(shù)據(jù)庫文件等。這個主要是針對錢包的初始化,保證錢包的完整性,方便錢包后面的使用。
********************************************


下面的第六步:網(wǎng)絡(luò)初始化和第七步:加載塊鏈 占據(jù)著AppInitMain()最多的代碼量,也是該函數(shù)最重要的工作,會在后面詳細解析。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • (九)繼續(xù)看bitcoind.cpp中的132-136行 對它的注釋為: InitError將被調(diào)用,并有詳細的錯...
    風(fēng)來霧去閱讀 2,062評論 0 1
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,568評論 19 139
  • linux資料總章2.1 1.0寫的不好抱歉 但是2.0已經(jīng)改了很多 但是錯誤還是無法避免 以后資料會慢慢更新 大...
    數(shù)據(jù)革命閱讀 13,246評論 2 33
  • 有一天我不再年輕 我已經(jīng)看到看到 那一天遠山青翠 一百年前你曾撥弄的發(fā)絲 迎風(fēng)飛舞 歲月無聲我也無淚 那一天我就有...
    行走的卓瑪閱讀 232評論 0 0
  • 說起鬧市,首先想到的是云南麗江,那是一個“時間停止”的地方。 第一次去麗江,事先并沒有做功課。綠皮車開的很慢,沿途...
    香果季客服小小閱讀 201評論 0 0

友情鏈接更多精彩內(nèi)容