QTUM源码解析-从入口到监听


  • qtum team

    就像大多数程序一样,当QTUM编译完成后,得到了两个重要的部分,一个是qtum-cli,另一个是qtumd。从名字就能知道,qtum-cli是客户端,qtumd是服务端,而业务核心在服务端,所以源码解析从服务端开始。

    客户端跟服务端之间是走的HTTP RPC通信,对于普通程序来说,找到入口就能把握住看代码的脉络,而对于服务器程序来说,找绑定监听的代码,才能找到业务处理的关键。

    首先,qtumd的入口,main函数在bitcoind.cpp里面定义的。

    int main(int argc, char* argv[])
    {
        SetupEnvironment();
        // Connect bitcoind signal handlers
        noui_connect();
        return (AppInit(argc, argv) ? EXIT_SUCCESS : EXIT_FAILURE);
    }
    

    可以看到,main函数一共三行,三个函数依次看一下,前两个都很简单,没有任何绑定监听的代码,所以监听的代码在第三个函数里面。

    打开AppInit

    bool AppInit(int argc, char* argv[])
    {
        boost::thread_group threadGroup;
        CScheduler scheduler;
    
        bool fRet = false;
        
        ...//参数和配置解析
        
        try
        {
            // Set this early so that parameter interactions go to console
            InitLogging();
            InitParameterInteraction();
            if (!AppInitBasicSetup())
            {
                // InitError will have been called with detailed error, which ends up on console
                exit(1);
            }
            if (!AppInitParameterInteraction())
            {
                // InitError will have been called with detailed error, which ends up on console
                exit(1);
            }
            if (!AppInitSanityChecks())
            {
                // InitError will have been called with detailed error, which ends up on console
                exit(1);
            }
    
            fRet = AppInitMain(threadGroup, scheduler);
        }
        catch (const std::exception& e) {
            PrintExceptionContinue(&e, "AppInit()");
        } catch (...) {
            PrintExceptionContinue(NULL, "AppInit()");
        }
    
        if (!fRet)
        {
            Interrupt(threadGroup);
            // threadGroup.join_all(); was left out intentionally here, because we didn't re-test all of
            // the startup-failure cases to make sure they don't result in a hang due to some
            // thread-blocking-waiting-for-another-thread-during-startup case
        } else {
            WaitForShutdown(&threadGroup);
        }
        Shutdown();
    
        return fRet;
    }
    

    这个函数很长,不过实际可以大体分为三个部分,第一个部分是参数和配置解析,实在太长了,而且现在我们不用关心,所以我没有拷过来。第二部分是一堆init,InitLoggingInitParameterInteractionAppInitBasicSetupAppInitParameterInteractionAppInitSanityChecksAppInitMain。第三部分是WaitForShutdown,这个名字一看就知道这个函数是循环等待结束命令。根据经验,能推测出监听绑定是在那堆init的函数里面。最后的WaitForShutdown使用了参数threadGroup,而那堆init函数只有AppInitMain使用了这个参数,所以AppInitMain里面多半跟监听有关。

    bool AppInitMain(boost::thread_group& threadGroup, CScheduler& scheduler)
    {
        const CChainParams& chainparams = Params();
        // ********************************************************* Step 4a: application initialization
        // After daemonization get the data directory lock again and hold on to it until exit
        // This creates a slight window for a race condition to happen, however this condition is harmless: it
        // will at most make us exit without printing a message to console.
        if (!LockDataDirectory(false)) {
            // Detailed error printed inside LockDataDirectory
            return false;
        }
    
    #ifndef WIN32
        CreatePidFile(GetPidFile(), getpid());
    #endif
        if (GetBoolArg("-shrinkdebugfile", !fDebug)) {
            // Do this first since it both loads a bunch of debug.log into memory,
            // and because this needs to happen before any other debug.log printing
            ShrinkDebugFile();
        }
    
        if (fPrintToDebugLog)
            OpenDebugLog();
    
        if (!fLogTimestamps)
            LogPrintf("Startup time: %s\n", DateTimeStrFormat("%Y-%m-%d %H:%M:%S", GetTime()));
        LogPrintf("Default data directory %s\n", GetDefaultDataDir().string());
        LogPrintf("Using data directory %s\n", GetDataDir().string());
        LogPrintf("Using config file %s\n", GetConfigFile(GetArg("-conf", BITCOIN_CONF_FILENAME)).string());
        LogPrintf("Using at most %i automatic connections (%i file descriptors available)\n", nMaxConnections, nFD);
    
        InitSignatureCache();
    
        LogPrintf("Using %u threads for script verification\n", nScriptCheckThreads);
        if (nScriptCheckThreads) {
            for (int i=0; i<nScriptCheckThreads-1; i++)
                threadGroup.create_thread(&ThreadScriptCheck);
        }
    
        // Start the lightweight task scheduler thread
        CScheduler::Function serviceLoop = boost::bind(&CScheduler::serviceQueue, &scheduler);
        threadGroup.create_thread(boost::bind(&TraceThread<CScheduler::Function>, "scheduler", serviceLoop));
    
        /* Start the RPC server already.  It will be started in "warmup" mode
         * and not really process calls already (but it will signify connections
         * that the server is there and will be ready later).  Warmup mode will
         * be disabled when initialisation is finished.
         */
        if (GetBoolArg("-server", false))
        {
            uiInterface.InitMessage.connect(SetRPCWarmupStatus);
            if (!AppInitServers(threadGroup))
                return InitError(_("Unable to start HTTP server. See debug log for details."));
        }
        ...
    

    又是另一个长函数,不过看了没几行就能发现这个调用,AppInitServers。打开它:

    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 (GetBoolArg("-rest", DEFAULT_REST_ENABLE) && !StartREST())
            return false;
        if (!StartHTTPServer())
            return false;
        return true;
    }
    

    这几个函数都看一遍,在StartHTTPRPC里面,发现了有意思的代码:

    bool StartHTTPRPC()
    {
        LogPrint("rpc", "Starting HTTP RPC server\n");
        if (!InitRPCAuthentication())
            return false;
    
        RegisterHTTPHandler("/", true, HTTPReq_JSONRPC);
    
        assert(EventBase());
        httpRPCTimerInterface = new HTTPRPCTimerInterface(EventBase());
        RPCSetTimerInterface(httpRPCTimerInterface);
        return true;
    }
    

    这个RegisterHTTPHandler的第一个参数是"/",这是很明显的注册调用处理函数的代码。
    打开这个函数

    void RegisterHTTPHandler(const std::string &prefix, bool exactMatch, const HTTPRequestHandler &handler)
    {
        LogPrint("http", "Registering HTTP handler for %s (exactmatch %d)\n", prefix, exactMatch);
        pathHandlers.push_back(HTTPPathHandler(prefix, exactMatch, handler));
    }
    

    可以看到第三个参数是handler,于是HTTPReq_JSONRPC就是处理函数。

    static bool HTTPReq_JSONRPC(HTTPRequest* req, const std::string &)
    {
        // JSONRPC handles only POST
        if (req->GetRequestMethod() != HTTPRequest::POST) {
            req->WriteReply(HTTP_BAD_METHOD, "JSONRPC server handles only POST requests");
            return false;
        }
        // Check authorization
        std::pair<bool, std::string> authHeader = req->GetHeader("authorization");
        if (!authHeader.first) {
            req->WriteHeader("WWW-Authenticate", WWW_AUTH_HEADER_DATA);
            req->WriteReply(HTTP_UNAUTHORIZED);
            return false;
        }
    
        JSONRPCRequest jreq;
        if (!RPCAuthorized(authHeader.second, jreq.authUser)) {
            LogPrintf("ThreadRPCServer incorrect password attempt from %s\n", req->GetPeer().ToString());
    
            /* Deter brute-forcing
               If this results in a DoS the user really
               shouldn't have their RPC port exposed. */
            MilliSleep(250);
    
            req->WriteHeader("WWW-Authenticate", WWW_AUTH_HEADER_DATA);
            req->WriteReply(HTTP_UNAUTHORIZED);
            return false;
        }
    
        try {
            // Parse request
            UniValue valRequest;
            if (!valRequest.read(req->ReadBody()))
                throw JSONRPCError(RPC_PARSE_ERROR, "Parse error");
    
            // Set the URI
            jreq.URI = req->GetURI();
    
            std::string strReply;
            // singleton request
            if (valRequest.isObject()) {
                jreq.parse(valRequest);
    
                UniValue result = tableRPC.execute(jreq);
    
                // Send reply
                strReply = JSONRPCReply(result, NullUniValue, jreq.id);
    
            // array of requests
            } else if (valRequest.isArray())
                strReply = JSONRPCExecBatch(valRequest.get_array());
            else
                throw JSONRPCError(RPC_PARSE_ERROR, "Top-level object parse error");
    
            req->WriteHeader("Content-Type", "application/json");
            req->WriteReply(HTTP_OK, strReply);
        } catch (const UniValue& objError) {
            JSONErrorReply(req, objError, jreq.id);
            return false;
        } catch (const std::exception& e) {
            JSONErrorReply(req, JSONRPCError(RPC_PARSE_ERROR, e.what()), jreq.id);
            return false;
        }
        return true;
    }
    

    这个函数的开始部分判断了请求的Method,限制了POST请求才有效。然后是验证身份,使用了WWW-Authenticate进行控制。然后定义了一个UniValuevalRequest接收请求数据。如果请求数据是个object,则直接调用tableRPC.execute进行处理。如果请求数据是个数组,那么则调用JSONRPCExecBatch。但实际查看JSONRPCExecBatch也能发现,它的底层也是调用tableRPC.execute进行处理。

    在整个项目文件中搜索tableRPC,可以找到几个代码片段。
    一个是server.cpp中定义了CRPCTable tableRPC,表明它是一个全局变量。
    另一个是init.cpp中的几行代码,

        RegisterAllCoreRPCCommands(tableRPC);
    #ifdef ENABLE_WALLET
        RegisterWalletRPCCommands(tableRPC);
    #endif
    

    这个代码是在AppInitParameterInteraction这个函数中,而这个函数就是上面的AppInit的那堆init函数中的一个。

    //register.h
    static inline void RegisterAllCoreRPCCommands(CRPCTable &t)
    {
        RegisterBlockchainRPCCommands(t);
        RegisterNetRPCCommands(t);
        RegisterMiscRPCCommands(t);
        RegisterMiningRPCCommands(t);
        RegisterRawTransactionRPCCommands(t);
    }
    //blockchain.cpp
    static const CRPCCommand commands[] =
    { //  category              name                      actor (function)         okSafe argNames
      //  --------------------- ------------------------  -----------------------  ------ ----------
        { "blockchain",         "getblockchaininfo",      &getblockchaininfo,      true,  {} },
        //more....
    };
    
    void RegisterBlockchainRPCCommands(CRPCTable &t)
    {
        for (unsigned int vcidx = 0; vcidx < ARRAYLEN(commands); vcidx++)
            t.appendCommand(commands[vcidx].name, &commands[vcidx]);
    }
    

    打开RegisterAllCoreRPCCommands,再打开它下面的第一个函数RegisterBlockchainRPCCommands,然后就看到了定义CRPCCommand的代码,从注释就能猜到,对于blockchain分类下面的getblockchaininfo的请求,调用的是getblockchaininfo这个函数进行处理。

    现在回头整理下,从main函数开始,调用了AppInit,它里面通过AppInitParameterInteraction去调用RegisterAllCoreRPCCommandsRegisterWalletRPCCommands对全局变量tableRPC进行设置,然后在AppInit函数的AppInitMainAppInitServers里面,通过StartHTTPRPCRegisterHTTPHandler把处理函数设置为HTTPReq_JSONRPC,而在HTTPReq_JSONRPC里面直接使用了全局变量tableRPC进行请求处理。具体的命令对应的处理函数,就在响应的register函数所在的文件所指定的。

    通过这次探索,理清了从函数入口到请求处理函数的绑定,并且找到了具体的命令对应处理函数的代码。之后再研究代码的时候,就比较有条理了。


Log in to reply
 

Looks like your connection to QTUM was lost, please wait while we try to reconnect.