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

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, ..... }