比特幣原始碼為我們提供了一個比特幣核心客戶端,這個核心客戶端也稱為中本聰客戶端,和SPV輕量級客戶端相比,比特幣核心客戶端包含了比特幣的方方面面。比特幣核心客戶端中包含一個服務端bitcoind和一個命令列工具bitcoin-cli,通過bitcoin-cli,使用者可以在命令列進行諸如建立交易、傳送交易,檢視交易,檢視區塊等一系列的操作。bitcoin-cli和bitcoind是典型的C/S模式,bitcoind中實現了一個http伺服器,bitcoin-cli則是一個http客戶端,二者之間的傳輸資料遵循json-rpc協議。本文將結合原始碼對比特幣json-rpc服務的實現進行分析。
1、比特幣核心客戶端架構
1.1 示例
先來看看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服務。
1.2 架構
bitcoin-cli通過http向bitcoind請求服務,其傳輸資料格式遵循json-rpc協議,整體架構如下圖:
搞清楚bitcoin-cli和bicoind的實現原理以及二者之間的互動過程對後續區塊鏈的學習會很有幫助,在搞明白這一部分的原理後,還可以比較容易的在各種平臺上(比如android、ios、pc等等)實現自己定製的比特幣錢包客戶端,下文來一步一步的開始分析。
2、json-rpc介紹
json-rpc是一個基於json的跨語言rpc協議,具有傳輸資料小,便於實現,擴充套件和除錯等優點,目前主流的程式語言比如java,c/c++等都有json-rpc的實現框架。2.1 請求
json-rpc的請求非常簡單,其格式如下:
{
"jsonrpc" : 2.0,
"method" : "getinfo",
"params" : [""],
"id" : 1
}複製程式碼
jsonrpc:json-rpc的版本;
method:rpc呼叫的方法名;
params:方法傳入的引數,沒有引數傳入nullptr;
id:呼叫的識別符號,可以為字串,也可以為nullptr,但是不建議使用nullptr,因為容易引起混亂。
2.2 響應
{
"jsonrpc" : 2.0,
"result" : "info",
"error" : null,
"id" : 1
}
複製程式碼
jsonrpc:json-rpc版本;
result:rpc呼叫的返回值,呼叫成功時不能為nullptr,呼叫失敗必須為nullptr;
error:呼叫錯誤時用,無錯誤為nullptr,有錯誤時返回錯誤物件,參見下一節;
id:呼叫識別符號,與呼叫方傳入的保持一致。
2.3 錯誤物件
{
"code" : 1,
"message" : "some error.",
"data":null
}複製程式碼
code:錯誤碼;
message:錯誤資訊;
data:附加資訊,可以為nullptr。
錯誤碼見下表:
錯誤碼 | 錯誤 | 含義 |
-32700 | 解析錯誤 | 伺服器收到無效json,或者解析json出錯 |
-32600 | 無效的請求 | 傳送的json不是一個有效的請求 |
-32601 | 方法未找到 | 方法不存在或不可見 |
-36602 | 無效的引數 | 無效的方法引數 |
-36603 | 內部錯誤 | json-rpc的內部錯誤 |
-32000到-32099 | 伺服器端錯誤 | 保留給具體伺服器實現的服務端錯誤 |
3、libevent庫
3.1 介紹
libevent是一個輕量級、跨平臺、基於事件的高效能網路庫。它封裝了不同平臺的io複用技術,對外暴露一致的介面。通常在編寫伺服器程式時所面臨的一個最大問題就是高併發,libevent是解決大量併發請求的一個較好的解決方案。
常見的處理大量併發請求的方法:
(1) IO複用技術
通過select,poll或epoll等系統api,實現io複用。
(2) 多執行緒或多程式
多執行緒和多程式也可以解決大量併發請求的問題,但是無論多程式還是多執行緒,都存在問題:多程式不適合短連線,程式的建立和銷燬開銷比較大;多執行緒不適合短連線,大量的執行緒會導致較大的記憶體開銷。
(3) 多執行緒結合IO複用
將IO複用和多執行緒結合起來,這是目前解決大併發的常用方案。最常見的套路就是主執行緒裡監聽某個埠以及接受的描述符,當有讀寫事件產生時,將事件交給工作執行緒去處理。
libevent封裝了select,poll,epoll等io複用技術,同時採用時間驅動的機制:應用向libevent註冊事件和相應的回撥,當事件發生時libevent呼叫這些回撥,libevent支援三種事件:網路IO,定時器和訊號。
3.2 api
關於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。
3.3 libevent示例
這裡用一個簡單的示例來看看用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中讀取到客戶資料。
3.4 用libevent實現http伺服器
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服務封裝的更為複雜一些。
4、比特幣JSONRPC伺服器的實現
4.1、JSONRPC伺服器框架
比特幣使用libevent實現了一個基於工作佇列的http伺服器,通過採用工作佇列的方式可以提高伺服器的併發處理能力。該伺服器框架如下:
邏輯其實非常簡單,在http事件迴圈中等待http請求的到來,當收到http請求以後,從請求資料中解析出資料並封裝到HttpWorkItem,放入到工作佇列裡,工作佇列執行起來以後會開啟工作執行緒檢查工作佇列,如果佇列裡有資料就從對頭取出並執行相應的動作。
4.2、JSONRPC伺服器初始化
4.2.1 註冊RPC命令及處理器
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伺服器。
4.2.2 建立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<HTTPClosure>(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實現了此介面。
4.2.3 http請求的處理
我們來看看當bitcoind收到一個http請求以後是如何處理的,就是http_request_cb回撥,主要程式碼如下:
// Find registered handler for prefix
std::string strURI = hreq->GetURI();
std::string path;
std::vector<HTTPPathHandler>::const_iterator i = pathHandlers.begin();
std::vector<HTTPPathHandler>::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<HTTPWorkItem> 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<HTTPRPCTimerInterface>(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<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++) {
g_thread_http_workers.emplace_back(HTTPWorkQueueRun, workQueue);
}
return true;
}複製程式碼
最終會呼叫到工作佇列的run方法:
void Run()
{
while (true) {
std::unique_ptr<WorkItem> i;
{
std::unique_lock<std::mutex> 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<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;
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服務端的脈絡我們就梳理的差不多了,整體框架並不難理解,只是封裝的略微複雜一點點。
5、小結
本文對json-rpc協議,libevent進行了簡要描述,並結合原始碼分析了比特幣的JSONRPC服務的實現。比特幣核心客戶端的bitcon-cli只是一個示例性質的命令列工具,如果想自己擼一個特定平臺上的帶有GUI的比特幣錢包客戶端,看完本文後相信將能信手拈來。