比特币源码解析:RPC详解

这篇文章主要分析rpc模块代码的一个整体逻辑,详细的代码讲解,请关注下一篇文章

在这里,我们暂时先抛开bitcoin代码,仅仅来谈RPC,提到RPC大家肯定首先会想到远程过程服务调用,既然是调用,那就肯定存在一个client端和一个server端,clent端与server端通过RPC这个黑盒通过http请求进行交互,那么就有一个问题,我自定义的json格式的字符串(这里拿json来进行举例)是无法在网络上流通的,所以,必然会涉及到一个json的序列化与反序列化的过程。综上所述,大致流程如下:

《比特币源码解析:RPC详解》 image.png

RPC详解

rpc命令的入口函数是从 bitcoin-abc/src/rpc/register.h 出发的,根据功能模块的不同,分了如下函数用来注册rpc命令:

class CRPCTable;

//区块链RPC命令注册
void RegisterBlockchainRPCCommands(CRPCTable &tableRPC);
//P2P网络RPC命令注册
void RegisterNetRPCCommands(CRPCTable &tableRPC);
//其他工具RPC命令注册
void RegisterMiscRPCCommands(CRPCTable &tableRPC);
//挖矿RPC命令注册
void RegisterMiningRPCCommands(CRPCTable &tableRPC);
//交易PRC命令注册
void RegisterRawTransactionRPCCommands(CRPCTable &tableRPC);
//BCH特有的RPC命令注册
void RegisterABCRPCCommands(CRPCTable &tableRPC);

首先来看 CRPCTable 这个类,它是一个调度表变量,专门用来存储RPC命令,它有一个很重要的方法,如下:

bool appendCommand(const std::string &name, const CRPCCommand *pcmd);

其他模块的rpc命令通过这个函数不断追加到rpcTable中。有两个参数,一个是rpc命令的名字name,另一个是指向rpc command的一个指针。

接下来我们看一下 CRPCCommand 这个类,它主要有如下定义:

std::string category;
std::string name;
rpcfn_type actor;
bool okSafeMode;
std::vector<std::string> argNames;

它表示了当我需要新增加一个rpc命令时,我这个新增加的命令需要描述如上所述的信息。

下面,我们一起探索一下,各个模块都是怎么注册自己的rpc命令的,先看blockchain,具体如下:

static const CRPCCommand commands[] = {
    //  category类别            name                      actor (function)     okSafe argNames
    //  ------------------- ------------------------  ----------------------  ------ ----------
    { "blockchain",         "getblockchaininfo",      getblockchaininfo,      true,  {} },
    { "blockchain",         "getbestblockhash",       getbestblockhash,       true,  {} },
    { "blockchain",         "getblockcount",          getblockcount,          true,  {} },
    { "blockchain",         "getblock",               getblock,               true,  {"blockhash","verbose"} },
    { "blockchain",         "getblockhash",           getblockhash,           true,  {"height"} },
    { "blockchain",         "getblockheader",         getblockheader,         true,  {"blockhash","verbose"} },
};

篇幅有限,只列举其中几个。区块链rpc命令在常量数组commands中存储,name就是我们在client中可以使用的rpc命令,那么他是如何注册到CRPCTable中的呢?

void RegisterBlockchainRPCCommands(CRPCTable &t) {
    for (unsigned int vcidx = 0; vcidx < ARRAYLEN(commands); vcidx++) {
        t.appendCommand(commands[vcidx].name, &commands[vcidx]);
    }
}

如上,使用for循环不断迭代commands中的元素,通过appendCommand方法把命令追加到rpcTable中。

其他模块注册的方式与此类似,都是调用appendCommand方法进行追加,故不在赘述。

注册完成之后,当我们调用具体的某一个rpc命令时,会发生什么呢?我们拿blockchain中的getblockchaininfo来举例,具体实现如下:

UniValue getblockchaininfo(const Config &config, const JSONRPCRequest &request){
    if (request.fHelp || request.params.size() != 0) {
        throw std::runtime_error("...")
    }
    LOCK(cs_main);

    UniValue obj(UniValue::VOBJ);
    obj.push_back(Pair("chain", Params().NetworkIDString()));
    obj.push_back(Pair("blocks", int(chainActive.Height())));
    obj.push_back(
        Pair("headers", pindexBestHeader ? pindexBestHeader->nHeight : -1));
    obj.push_back(
        Pair("bestblockhash", chainActive.Tip()->GetBlockHash().GetHex()));
    obj.push_back(Pair("difficulty", double(GetDifficulty(chainActive.Tip()))));
    obj.push_back(
        Pair("mediantime", int64_t(chainActive.Tip()->GetMedianTimePast())));
    obj.push_back(
        Pair("verificationprogress",
             GuessVerificationProgress(Params().TxData(), chainActive.Tip())));
    obj.push_back(Pair("chainwork", chainActive.Tip()->nChainWork.GetHex()));
    obj.push_back(Pair("pruned", fPruneMode));

    const Consensus::Params &consensusParams = Params().GetConsensus();
    CBlockIndex *tip = chainActive.Tip();
    UniValue softforks(UniValue::VARR);
    UniValue bip9_softforks(UniValue::VOBJ);
    softforks.push_back(SoftForkDesc("bip34", 2, tip, consensusParams));
    softforks.push_back(SoftForkDesc("bip66", 3, tip, consensusParams));
    softforks.push_back(SoftForkDesc("bip65", 4, tip, consensusParams));
    BIP9SoftForkDescPushBack(bip9_softforks, "csv", consensusParams,
                             Consensus::DEPLOYMENT_CSV);
    obj.push_back(Pair("softforks", softforks));
    obj.push_back(Pair("bip9_softforks", bip9_softforks));

    if (fPruneMode) {
        CBlockIndex *block = chainActive.Tip();
        while (block && block->pprev &&
               (block->pprev->nStatus & BLOCK_HAVE_DATA)) {
            block = block->pprev;
        }

        obj.push_back(Pair("pruneheight", block->nHeight));
    }
    return obj;

如上,每一个rpc命令在他们相应的模块中,都有基于这个rpc所表述含义的相应代码实现。

我们看到getblockchaininfo这个函数返回值是UniValue,UniValue是cpp中一个json解析库,用来将json格式的rpc命令转化为网络可识别的。转化实现主要在client.cpp
中进行了简单的包装,具体实现如下:

class CRPCConvertParam {
public:
    std::string methodName; //!< method whose params want conversion
    int paramIdx;           //!< 0-based idx of param to convert
    std::string paramName;  //!< parameter name
};
  • methodName 代表的是 rpc 命令的 name
  • paramIDx 代表的是参数的位置,从 0 开始算起,
  • paramName 描述的是参数的具体信息,也就是前面methodName所代表的含义

我们会看到有时候会有相同的methodName,但是他们的paramIDx不一样,所代表的描述信息也就不同,这里要注意区分

clint.cpp 主要是服务于 bitcoin-cli

客户端在这边将json的命令进行序列化之后,发送给对应的服务端(各个不同的模块),服务端在做相应的处理。

在protocol.h中,我们定义了一些HTTP的状态码,以及RPC的错误码,json的格式规范遵循的是RFC4627。

//! HTTP status codes
enum HTTPStatusCode {
    HTTP_OK = 200,
    HTTP_BAD_REQUEST = 400,
    HTTP_UNAUTHORIZED = 401,
    HTTP_FORBIDDEN = 403,
    HTTP_NOT_FOUND = 404,
    HTTP_BAD_METHOD = 405,
    HTTP_INTERNAL_SERVER_ERROR = 500,
    HTTP_SERVICE_UNAVAILABLE = 503,
};
//! Bitcoin RPC error codes
enum RPCErrorCode {
    //! Standard JSON-RPC 2.0 errors
    // RPC_INVALID_REQUEST is internally mapped to HTTP_BAD_REQUEST (400).
    // It should not be used for application-layer errors.
    RPC_INVALID_REQUEST = -32600,
    // RPC_METHOD_NOT_FOUND is internally mapped to HTTP_NOT_FOUND (404).
    // It should not be used for application-layer errors.
    RPC_METHOD_NOT_FOUND = -32601,
    RPC_INVALID_PARAMS = -32602,
    .....
}

本文由Copernicus 团队 冉小龙 分析编写,转载无需授权!

家境清寒,整理不易。

《比特币源码解析:RPC详解》 image.png

    原文作者:wolf4j
    原文地址: https://www.jianshu.com/p/56f0ef5d8d18
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞