比特币源码为我们提供了一个比特币核心客户端,这个核心客户端也称为中本聪客户端,和SPV轻量级客户端相比,比特币核心客户端包含了比特币的方方面面。比特币核心客户端中包含一个服务端bitcoind和一个命令行工具bitcoin-cli,通过bitcoin-cli,用户可以在命令行进行诸如创建交易、发送交易,查看交易,查看区块等一系列的操作。bitcoin-cli和bitcoind是典型的C/S模式,bitcoind中实现了一个http服务器,bitcoin-cli则是一个http客户端,二者之间的传输数据遵循json-rpc协议。本文将结合源码对比特币json-rpc服务的实现进行分析。
先来看看bitcoin-cli的一个使用示例。我们知道区块链的一个最大特性就是能够追根溯源,任何一笔交易在验证后最终都记录到了区块链上,之后就无法篡改,现在我们通过bitcoin-cli来查看区块链上的一笔交易,这个示例来自于《精通比特币》一书:
假设我们已经下载并且编译好了比特币客户端,并且bitcoind已经运行起来了(可以在命令行输入bitciond &让其在后台运行)现在我们要查看一下交易hash为9ca8f969bd3ef5ec2a8685660fdbf7a8bd365524c2e1fc66c309acbae2c14ae3的详细信息,我们在命令行输入:
$ bitcoin-cli gettransaction 9ca8f969bd3ef5ec2a8685660fdbf7a8bd365524c2e1fc66c309acbae2c14ae3
然后,在命令行就能看到服务器返回的json格式表示的交易的信息:
{
"amount" : 0.05000000,
"confirmations" : 0,
"txid":"9ca8f969bd3ef5ec2a8685660fdbf7a8bd365524c2e1fc66c309acbae2c14ae3",
"time" : 1392660908,
"timereceived" : 1392660908,
"details" : [
{
"account" : "",
"address":"1hvzSofGwT8cjb8JU7nBsCSfEVQX5u9CL",
"category" : "receive",
"amount" : 0.05000000
}
]
}
可以看到这笔交易里向比特币地址1hvzSofGwT8cjb8JU7nBsCSfEVQX5u9CL转移了0.05个比特币。
用类似的方式我们还能进行很多操作,比如创建交易,生成钱包地址,查看区块等等。而这种简单的命令背后的实现,就是本文要讲述的基于json-rpc的http服务。
bitcoin-cli通过http向bitcoind请求服务,其传输数据格式遵循json-rpc协议,整体架构如下图:
搞清楚bitcoin-cli和bicoind的实现原理以及二者之间的交互过程对后续区块链的学习会很有帮助,在搞明白这一部分的原理后,还可以比较容易的在各种平台上(比如android、ios、pc等等)实现自己定制的比特币钱包客户端,下文来一步一步的开始分析。
json-rpc是一个基于json的跨语言rpc协议,具有传输数据小,便于实现,扩展和调试等优点,目前主流的编程语言比如java,c/c++等都有json-rpc的实现框架。
json-rpc的请求非常简单,其格式如下:
{
"jsonrpc" : 2.0,
"method" : "getinfo",
"params" : [""],
"id" : 1
}
jsonrpc:json-rpc的版本;
method:rpc调用的方法名;
params:方法传入的参数,没有参数传入nullptr;
id:调用的标识符,可以为字符串,也可以为nullptr,但是不建议使用nullptr,因为容易引起混乱。
{
"jsonrpc" : 2.0,
"result" : "info",
"error" : null,
"id" : 1
}
jsonrpc:json-rpc版本;
result:rpc调用的返回值,调用成功时不能为nullptr,调用失败必须为nullptr;
error:调用错误时用,无错误为nullptr,有错误时返回错误对象,参见下一节;
id:调用标识符,与调用方传入的保持一致。
{
"code" : 1,
"message" : "some error.",
"data":null
}
code:错误码;
message:错误信息;
data:附加信息,可以为nullptr。
错误码见下表:
错误码 | 错误 | 含义 |
-32700 | 解析错误 | 服务器收到无效json,或者解析json出错 |
-32600 | 无效的请求 | 发送的json不是一个有效的请求 |
-32601 | 方法未找到 | 方法不存在或不可见 |
-36602 | 无效的参数 | 无效的方法参数 |
-36603 | 内部错误 | json-rpc的内部错误 |
-32000到-32099 | 服务器端错误 | 保留给具体服务器实现的服务端错误 |
比特币核心实现了一个基于libevent的http服务器,下文会对这个服务器的实现进行介绍。
libevent是一个轻量级、跨平台、基于事件的高性能网络库。它封装了不同平台的io复用技术,对外暴露一致的接口。通常在编写服务器程序时所面临的一个最大问题就是高并发,libevent是解决大量并发请求的一个较好的解决方案。
常见的处理大量并发请求的方法:
(1) IO复用技术
通过select,poll或epoll等系统api,实现io复用。
(2) 多线程或多进程
多线程和多进程也可以解决大量并发请求的问题,但是无论多进程还是多线程,都存在问题:多进程不适合短连接,进程的创建和销毁开销比较大;多线程不适合短连接,大量的线程会导致较大的内存开销。
(3) 多线程结合IO复用
将IO复用和多线程结合起来,这是目前解决大并发的常用方案。最常见的套路就是主线程里监听某个端口以及接受的描述符,当有读写事件产生时,将事件交给工作线程去处理。
libevent封装了select,poll,epoll等io复用技术,同时采用事件驱动的机制:应用向libevent注册事件和相应的回调,当事件发生时libevent调用这些回调,libevent支持三种事件:网络IO,定时器和信号。
关于libevent的实现原理本文不详细展开,有兴趣的同学可以在github自行下载源代码学习。这里只简单介绍几个核心的api。
(1) struct event_base * event_base_new()
创建一个事件集,事件必须加入到事件集里才能接收到回调。
(2) struct event event_new(struct event_base *, evutil_socket_t, short, event_callback_fn, void*)
创建一个事件,其参数如下:
event_base:事件所在的事件集;
evutil_socket_t:socket描述符;
short:事件类型,EV_READ表示等待读事件,EV_WRITE表示写事件,EV_SIGNAL表示要等待的信号;
event_callback_fn:事件发生时的回调函数;
void* 回调函数的参数
(3) int event_add(struct event *, struct timeval *)
添加事件。
event:要添加的事件
timeval:等待事件的超时值,如果为nullptr将是无限等待。
(4) int event_del(struct event *)
删除事件。
(5) struct bufferevent *bufferevent_socket_new(struct event_base *, evutil_socket_t, int options)
创建一个bufferevent,bufferevent封装了read,write等读写函数。
event_base:bufferevent事件所在的事件集;
evutil_socket_t:相关的套接字描述符;
options:选项。
(6) int bufferevent_enable(struct bufferevent *, short event)
启用bufferevent。
(7) size_t bufferevent_read(struct bufferevent *, void *data, size_t size)
读取bufferevent,返回读取的字节数。
(8) size_t bufferevent_write(struct bufferevent *, const void *data, size_t size)
写入bufferevent。
这里用一个简单的示例来看看用libevent如何开发一个简单的服务器。
(1) 首先创建套接字并在指定的端口上监听
int sock_fd = ::socket(AF_INET, SOCK_STREAM, 0);
if( sock_fd == -1 )
return -1;
evutil_make_listen_socket_reuseable(sock_fd);
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = 0;
sin.sin_port = htons(port);
if( ::bind(sock_fd, (SA*)&sin, sizeof(sin)) < 0 )
goto error;
if( ::listen(sock_fd, listen_num) < 0)
goto error;
evutil_make_socket_nonblocking(listener);
(2) 创建一个监听客户连接请求的事件:
struct event* ev_listen = event_new(base, sock_fd, EV_READ | EV_PERSIST,
accept_cb, base);
event_add(ev_listen, NULL);
event_base_dispatch(base);
当监听套接字有新连接时,事件将被触发,从而执行回调accept_cb:
void accept_cb(int fd, short events, void* arg)
{
evutil_socket_t sockfd;
struct sockaddr_in client;
socklen_t len = sizeof(client);
sockfd = ::accept(fd, (struct sockaddr*)&client, &len );
evutil_make_socket_nonblocking(sockfd);
printf("accept a client %d\n", sockfd);
struct event_base* base = (event_base*)arg;
bufferevent* bev = bufferevent_socket_new(base, sockfd, BEV_OPT_CLOSE_ON_FREE);
bufferevent_setcb(bev, socket_read_cb, NULL, event_cb, arg);
bufferevent_enable(bev, EV_READ | EV_PERSIST);
}
创建一个bufferevent事件,将accept以后的已连接套接字与之关联,这样当套接字上有数据到来时,就会触发bufferevent事件,从而执行socket_read_cb回调:
void socket_read_cb(bufferevent* bev, void* arg)
{
char msg[4096];
size_t len = bufferevent_read(bev, msg, sizeof(msg));
msg[len] = '\0';
char reply_msg[4096] = "recvieced msg:";
strcat(reply_msg + strlen(reply_msg), msg);
bufferevent_write(bev, reply_msg, strlen(reply_msg));
}
然后就能从bufferevent中读取到客户数据。
libevent提供了http的支持,用libevent很容易实现自己的http服务,步骤如下:
(1) 创建事件集和evhttp事件:
struct event_base *event_base_new(void);
struct evhttp *evhttp_new(struct event_base *base);
(2) 绑定地址和端口
int evhttp_bind_socket(struct evhttp *http, const char *address, ev_uint16_t port);
(3) 设置回调来处理http请求
void evhttp_set_gencb(struct evhttp *http, void (*cb)(struct evhttp_request *, void *), void *arg);
(4) 进入事件循环
int event_base_dispatch(struct event_base *);
在下一节我们结合比特币的源码,来看看比特币中是如使用上面这些api实现http服务的,当然比特币的http服务封装的更为复杂一些。
比特币使用libevent实现了一个基于工作队列的http服务器,通过采用工作队列的方式可以提高服务器的并发处理能力。该服务器框架如下:
逻辑其实非常简单,在http事件循环中等待http请求的到来,当收到http请求以后,从请求数据中解析出数据并封装到HttpWorkItem,放入到工作队列里,工作队列运行起来以后会开启工作线程检查工作队列,如果队列里有数据就从对头取出并执行相应的动作。
JSONRPC服务器的初始化也是在bitcoind的初始化步骤中。在init.cpp的AppInitMain函数里:
/* Register RPC commands regardless of -server setting so they will be
* available in the GUI RPC console even if external calls are disabled.
*/
RegisterAllCoreRPCCommands(tableRPC);
g_wallet_init_interface.RegisterRPC(tableRPC);
/* 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 (gArgs.GetBoolArg("-server", false))
{
uiInterface.InitMessage.connect(SetRPCWarmupStatus);
if (!AppInitServers())
return InitError(_("Unable to start HTTP server. See debug log for details."));
}
首先调用RegisterAllCoreRPCCommands注册比特币核心客户端所支持的所有RPC指令:
static inline void RegisterAllCoreRPCCommands(CRPCTable &t)
{
RegisterBlockchainRPCCommands(t);
RegisterNetRPCCommands(t);
RegisterMiscRPCCommands(t);
RegisterMiningRPCCommands(t);
RegisterRawTransactionRPCCommands(t);
}
这里可以看到对RPC命令进行了分类,操作区块链的、网络相关的、挖矿相关的以及比特币交易相关的RPC命令一应俱全。这里不妨列出来,这样读者对通过客户端能做些什么事情有个大概印象:
(1) 区块链相关的rpc,位于blockchain.cpp中:
static const CRPCCommand commands[] =
{ // category name actor (function) argNames
// --------------------- ------------------------ ----------------------- ----------
{ "blockchain", "getblockchaininfo", &getblockchaininfo, {} },
{ "blockchain", "getchaintxstats", &getchaintxstats, {"nblocks", "blockhash"} },
{ "blockchain", "getblockstats", &getblockstats, {"hash_or_height", "stats"} },
{ "blockchain", "getbestblockhash", &getbestblockhash, {} },
{ "blockchain", "getblockcount", &getblockcount, {} },
{ "blockchain", "getblock", &getblock, {"blockhash","verbosity|verbose"} },
{ "blockchain", "getblockhash", &getblockhash, {"height"} },
{ "blockchain", "getblockheader", &getblockheader, {"blockhash","verbose"} },
{ "blockchain", "getchaintips", &getchaintips, {} },
{ "blockchain", "getdifficulty", &getdifficulty, {} },
{ "blockchain", "getmempoolancestors", &getmempoolancestors, {"txid","verbose"} },
{ "blockchain", "getmempooldescendants", &getmempooldescendants, {"txid","verbose"} },
{ "blockchain", "getmempoolentry", &getmempoolentry, {"txid"} },
{ "blockchain", "getmempoolinfo", &getmempoolinfo, {} },
{ "blockchain", "getrawmempool", &getrawmempool, {"verbose"} },
{ "blockchain", "gettxout", &gettxout, {"txid","n","include_mempool"} },
{ "blockchain", "gettxoutsetinfo", &gettxoutsetinfo, {} },
{ "blockchain", "pruneblockchain", &pruneblockchain, {"height"} },
{ "blockchain", "savemempool", &savemempool, {} },
{ "blockchain", "verifychain", &verifychain, {"checklevel","nblocks"} },
{ "blockchain", "preciousblock", &preciousblock, {"blockhash"} },
/* Not shown in help */
{ "hidden", "invalidateblock", &invalidateblock, {"blockhash"} },
{ "hidden", "reconsiderblock", &reconsiderblock, {"blockhash"} },
{ "hidden", "waitfornewblock", &waitfornewblock, {"timeout"} },
{ "hidden", "waitforblock", &waitforblock, {"blockhash","timeout"} },
{ "hidden", "waitforblockheight", &waitforblockheight, {"height","timeout"} },
{ "hidden", "syncwithvalidationinterfacequeue", &syncwithvalidationinterfacequeue, {} },
};
所有的RPC命令以及对应的回调函数指针都封装在了CRPCCommand中,按分类、rpc方法名,回调函数,参数名封装。基本上通过方法名就能猜出其作用。
(2) 网络相关的rpc,位于net.cpp中:
static const CRPCCommand commands[] =
{ // category name actor (function) argNames
// --------------------- ------------------------ ----------------------- ----------
{ "network", "getconnectioncount", &getconnectioncount, {} },
{ "network", "ping", &ping, {} },
{ "network", "getpeerinfo", &getpeerinfo, {} },
{ "network", "addnode", &addnode, {"node","command"} },
{ "network", "disconnectnode", &disconnectnode, {"address", "nodeid"} },
{ "network", "getaddednodeinfo", &getaddednodeinfo, {"node"} },
{ "network", "getnettotals", &getnettotals, {} },
{ "network", "getnetworkinfo", &getnetworkinfo, {} },
{ "network", "setban", &setban, {"subnet", "command", "bantime", "absolute"} },
{ "network", "listbanned", &listbanned, {} },
{ "network", "clearbanned", &clearbanned, {} },
{ "network", "setnetworkactive", &setnetworkactive, {"state"} },
(3) 挖矿相关的rpc,位于mining.cpp中:
static const CRPCCommand commands[] =
{ // category name actor (function) argNames
// --------------------- ------------------------ ----------------------- ----------
{ "mining", "getnetworkhashps", &getnetworkhashps, {"nblocks","height"} },
{ "mining", "getmininginfo", &getmininginfo, {} },
{ "mining", "prioritisetransaction", &prioritisetransaction, {"txid","dummy","fee_delta"} },
{ "mining", "getblocktemplate", &getblocktemplate, {"template_request"} },
{ "mining", "submitblock", &submitblock, {"hexdata","dummy"} },
{ "generating", "generatetoaddress", &generatetoaddress, {"nblocks","address","maxtries"} },
{ "hidden", "estimatefee", &estimatefee, {} },
{ "util", "estimatesmartfee", &estimatesmartfee, {"conf_target", "estimate_mode"} },
{ "hidden", "estimaterawfee", &estimaterawfee, {"conf_target", "threshold"} },
};
(4) 比特币交易相关rpc,位于rawtransaction.cpp中:
static const CRPCCommand commands[] =
{ // category name actor (function) argNames
// --------------------- ------------------------ ----------------------- ----------
{ "rawtransactions", "getrawtransaction", &getrawtransaction, {"txid","verbose","blockhash"} },
{ "rawtransactions", "createrawtransaction", &createrawtransaction, {"inputs","outputs","locktime","replaceable"} },
{ "rawtransactions", "decoderawtransaction", &decoderawtransaction, {"hexstring","iswitness"} },
{ "rawtransactions", "decodescript", &decodescript, {"hexstring"} },
{ "rawtransactions", "sendrawtransaction", &sendrawtransaction, {"hexstring","allowhighfees"} },
{ "rawtransactions", "combinerawtransaction", &combinerawtransaction, {"txs"} },
{ "rawtransactions", "signrawtransaction", &signrawtransaction, {"hexstring","prevtxs","privkeys","sighashtype"} }, /* uses wallet if enabled */
{ "rawtransactions", "signrawtransactionwithkey", &signrawtransactionwithkey, {"hexstring","privkeys","prevtxs","sighashtype"} },
{ "rawtransactions", "testmempoolaccept", &testmempoolaccept, {"rawtxs","allowhighfees"} },
{ "blockchain", "gettxoutproof", &gettxoutproof, {"txids", "blockhash"} },
{ "blockchain", "verifytxoutproof", &verifytxoutproof, {"proof"} },
};
当注册完以后,如果用户启用了-server选项,将会调用AppInitServers创建Http服务器。
AppInitServers实现如下:
static bool AppInitServers()
{
RPCServer::OnStarted(&OnRPCStarted);
RPCServer::OnStopped(&OnRPCStopped);
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;
}
这里按步骤一步一步的来。首先是调用InitHTTPServer,使用libevent api来建立http服务器,这里截取主要代码来看看,位于httpserver.cpp文件:
raii_event_base base_ctr = obtain_event_base();
/* Create a new evhttp object to handle requests. */
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(workQueueDepth);
// transfer ownership to eventBase/HTTP via .release()
eventBase = base_ctr.release();
eventHTTP = http_ctr.release();
这里的套路和3.4节中用libevent建立http服务器的步骤基本一样,注意两点:
(1) 用evhttp_set_gencb设置了http请求的处理函数:http_request_cb;
(2) 创建了一个工作队列,队列里的元素类型HTTPClosure,这是一个函数对象接口类,重写了函数调用操作符,HttpWorkItem实现了此接口。
我们来看看当bitcoind收到一个http请求以后是如何处理的,就是http_request_cb回调,主要代码如下:
// Find registered handler for prefix
std::string strURI = hreq->GetURI();
std::string path;
std::vector::const_iterator i = pathHandlers.begin();
std::vector::const_iterator iend = pathHandlers.end();
for (; i != iend; ++i) {
bool match = false;
if (i->exactMatch)
match = (strURI == i->prefix);
else
match = (strURI.substr(0, i->prefix.size()) == i->prefix);
if (match) {
path = strURI.substr(i->prefix.size());
break;
}
}
// Dispatch to worker thread
if (i != iend) {
std::unique_ptr item(new HTTPWorkItem(std::move(hreq), path, i->handler));
assert(workQueue);
if (workQueue->Enqueue(item.get()))
item.release(); /* if true, queue took ownership */
else {
LogPrintf("WARNING: request rejected because http work queue depth exceeded, it can be increased with the -rpcworkqueue= setting\n");
item->req->WriteReply(HTTP_INTERNAL, "Work queue depth exceeded");
}
} else {
hreq->WriteReply(HTTP_NOTFOUND);
}
用一句话来概括这个函数的作用就是:将请求的url的path部分与注册过的前缀进行匹配,并生成HttpWorkItem放入到工作队列中。目前注册了两个前缀:/和/wallet/,代码在StartHttpRPC中:
bool StartHTTPRPC()
{
LogPrint(BCLog::RPC, "Starting HTTP RPC server\n");
if (!InitRPCAuthentication())
return false;
RegisterHTTPHandler("/", true, HTTPReq_JSONRPC);
#ifdef ENABLE_WALLET
// ifdef can be removed once we switch to better endpoint support and API versioning
RegisterHTTPHandler("/wallet/", false, HTTPReq_JSONRPC);
#endif
assert(EventBase());
httpRPCTimerInterface = MakeUnique(EventBase());
RPCSetTimerInterface(httpRPCTimerInterface.get());
return true;
}
两个前缀/和/wallet/对应的回调处理函数均为HttpReq_JSONRPC。
之后调用StartHttpServer让工作队列运行起来:
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 task(ThreadHTTP);
threadResult = task.get_future();
threadHTTP = std::thread(std::move(task), eventBase, eventHTTP);
for (int i = 0; i < rpcThreads; i++) {
g_thread_http_workers.emplace_back(HTTPWorkQueueRun, workQueue);
}
return true;
}
最终会调用到工作队列的run方法:
void Run()
{
while (true) {
std::unique_ptr i;
{
std::unique_lock lock(cs);
while (running && queue.empty())
cond.wait(lock);
if (!running)
break;
i = std::move(queue.front());
queue.pop_front();
}
(*i)();
}
}
很简单,工作队列为空的时候线程阻塞等待,当收到http请求以后,解析请求并添加HttpWorkItem到队列中并唤醒线程,线程从队列头部取出一个item运行。最终将执行HttpReq_JSONRPC这个回调,这里会将JSONRPC中的rpc方法分发到服务端不同的方法中,来看看其处理:
(1) 请求合法性检查及认证
首先检查请求是否合法,http头部中的auchoization是否合法:
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 authHeader = req->GetHeader("authorization");
if (!authHeader.first) {
req->WriteHeader("WWW-Authenticate", WWW_AUTH_HEADER_DATA);
req->WriteReply(HTTP_UNAUTHORIZED);
return false;
}
JSONRPCRequest jreq;
jreq.peerAddr = req->GetPeer().ToString();
if (!RPCAuthorized(authHeader.second, jreq.authUser)) {
LogPrintf("ThreadRPCServer incorrect password attempt from %s\n", jreq.peerAddr);
/* 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;
}
可以看到,比特币的json rpc服务只支持POST。
(2) 读取http请求数据,将rpc请求分发到不同的函数
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(jreq, 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);
如果收到的是单个json,则tableRPC.execute执行,否则如果收到的是以数组形式的批量rpc请求,则批量执行,批量执行最终也是走tableRPC.execute()来分发,execute()执行后的结果将写入到http响应包中:
UniValue CRPCTable::execute(const JSONRPCRequest &request) const
{
// Return immediately if in warmup
{
LOCK(cs_rpcWarmup);
if (fRPCInWarmup)
throw JSONRPCError(RPC_IN_WARMUP, rpcWarmupStatus);
}
// Find method
const CRPCCommand *pcmd = tableRPC[request.strMethod];
if (!pcmd)
throw JSONRPCError(RPC_METHOD_NOT_FOUND, "Method not found");
g_rpcSignals.PreCommand(*pcmd);
try
{
// Execute, convert arguments to array if necessary
if (request.params.isObject()) {
return pcmd->actor(transformNamedArguments(request, pcmd->argNames));
} else {
return pcmd->actor(request);
}
}
catch (const std::exception& e)
{
throw JSONRPCError(RPC_MISC_ERROR, e.what());
}
}
代码也比较容易理解,就是从根据json-rpc协议,从请求中读取method,然后根据method找到对应的CRPCCommand执行体,这些执行体就是4.2.1节中提到那几张分门别类的映射表。
至此,比特币的json-rpc服务端的脉络我们就梳理的差不多了,整体框架并不难理解,只是封装的略微复杂一点点。
本文对json-rpc协议,libevent进行了简要描述,并结合源码分析了比特币的JSONRPC服务的实现。比特币核心客户端的bitcon-cli只是一个示例性质的命令行工具,如果想自己撸一个特定平台上的带有GUI的比特币钱包客户端,看完本文后相信将能信手拈来。