比特幣原始碼解析(18)
0x01 InitHTTPServer
初始化訪問控制列表(ACL)
if (!InitHTTPAllowList())
return false;
if (gArgs.GetBoolArg("-rpcssl", false)) {
uiInterface.ThreadSafeMessageBox(
"SSL mode for RPC (-rpcssl) is no longer supported.",
"", CClientUIInterface::MSG_ERROR);
return false;
}
初始化HTTP Server首先通過InitHTTPAllowList
來初始化訪問控制列表,然後看命令列中是否設定了rpcssl
引數,由於目前的版本不支援ssl,所以如果設定了這個引數就報錯。再來看看InitHTTPAllowList
的實現,
/** Initialize ACL list for HTTP server
httpserver.cpp line 190
*/
static bool InitHTTPAllowList()
{
rpc_allow_subnets.clear();
CNetAddr localv4;
CNetAddr localv6;
LookupHost("127.0.0.1" , localv4, false);
LookupHost("::1", localv6, false);
rpc_allow_subnets.push_back(CSubNet(localv4, 8)); // always allow IPv4 local subnet
rpc_allow_subnets.push_back(CSubNet(localv6)); // always allow IPv6 localhost
for (const std::string& strAllow : gArgs.GetArgs("-rpcallowip" )) {
CSubNet subnet;
LookupSubNet(strAllow.c_str(), subnet);
if (!subnet.IsValid()) {
uiInterface.ThreadSafeMessageBox(
strprintf("Invalid -rpcallowip subnet specification: %s. Valid are a single IP (e.g. 1.2.3.4), a network/netmask (e.g. 1.2.3.4/255.255.255.0) or a network/CIDR (e.g. 1.2.3.4/24).", strAllow),
"", CClientUIInterface::MSG_ERROR);
return false;
}
rpc_allow_subnets.push_back(subnet);
}
std::string strAllowed;
for (const CSubNet& subnet : rpc_allow_subnets)
strAllowed += subnet.ToString() + " ";
LogPrint(BCLog::HTTP, "Allowing HTTP connections from: %s\n", strAllowed);
return true;
}
rpc_allow_subnets
是一個vector型別變數,儲存所有允許訪問的主機ip,首先先加入本地的地址,然後從命令列中的-rpcallowip
引數讀取ip列表,讀取時檢測地址的有效性,最後打印出允許訪問的所有ip列表。
重定向日誌
// Redirect libevent's logging to our own log
event_set_log_callback(&libevent_log_cb);
// Update libevent's log handling. Returns false if our version of
// libevent doesn't support debug logging, in which case we should
// clear the BCLog::LIBEVENT flag.
if (!UpdateHTTPServerLogging(logCategories & BCLog::LIBEVENT)) {
logCategories &= ~BCLog::LIBEVENT;
}
這段程式碼就是將libevent的日誌重定向到程式碼的日誌系統中。
初始化基於libevent的http協議
#ifdef WIN32
evthread_use_windows_threads();
#else
evthread_use_pthreads();
#endif
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();
return true;
首先程式碼根據系統環境判斷使用windows執行緒還是其他環境下的執行緒,接下來就是基於libevent的http協議的實現,採用libevent的原因有以下幾個方面:
- 事件驅動,高效能;
- 輕量級,專注於網路;
- 跨平臺,支援各主流平臺;
- 支援多種I/O多路複用技術,epoll、poll、dev/poll、select和kqueue等;
- 支援I/O,定時器和訊號等事件。
基於libevent實現的http協議主要有以下這麼幾個步驟:
event_base base = event_base_new()
,首先新建event_base
物件;evhttp http = evhttp_new(base)
,然後新建evhttp
物件;evhttp_bind_socket(http, "0.0.0.0", port)
,接下來繫結ip地址和埠;evhttp_set_gencb(http, http_call_back, NULL)
,然後設定請求處理函式http_call_back
;event_base_dispatch(base)
, 最後派發事件迴圈。
所以一個基於libevent簡單的http server程式碼如下:
#include "event2/http.h"
#include "event2/event.h"
#include "event2/buffer.h"
#include <stdlib.h>
#include <stdio.h>
void HttpGenericCallback(struct evhttp_request* request, void* arg)
{
const struct evhttp_uri* evhttp_uri = evhttp_request_get_evhttp_uri(request);
char url[8192];
evhttp_uri_join(const_cast<struct evhttp_uri*>(evhttp_uri), url, 8192);
printf("accept request url:%s\n", url);
struct evbuffer* evbuf = evbuffer_new();
if (!evbuf)
{
printf("create evbuffer failed!\n");
return ;
}
evbuffer_add_printf(evbuf, "Server response. Your request url is %s", url);
evhttp_send_reply(request, HTTP_OK, "OK", evbuf);
evbuffer_free(evbuf);
}
int main(int argc, char** argv)
{
if (argc != 2)
{
printf("usage:%s port\n", argv[0]);
return 1;
}
int port = atoi(argv[1]);
if (port == 0)
{
printf("port error:%s\n", argv[1]);
return 1;
}
struct event_base* base = event_base_new();
if (!base)
{
printf("create event_base failed!\n");
return 1;
}
struct evhttp* http = evhttp_new(base);
if (!http)
{
printf("create evhttp failed!\n");
return 1;
}
if (evhttp_bind_socket(http, "0.0.0.0", port) != 0)
{
printf("bind socket failed! port:%d\n", port);
return 1;
}
evhttp_set_gencb(http, HttpGenericCallback, NULL);
event_base_dispatch(base);
return 0;
}
作者:楚客
連結:http://www.jianshu.com/p/906c8b9f0629
來源:簡書
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。
再回到原始碼中,我們發現整體步驟和上面的簡單版http server很類似,但是使用的函式都換了個名字,像obtain_event_base()
和obtain_evhttp()
,看一下它們的實現,
inline raii_event_base obtain_event_base() {
auto result = raii_event_base(event_base_new());
if (!result.get())
throw std::runtime_error("cannot create event_base");
return result;
}
可以發現就是把原來的函式封裝了一下,原來的變數型別也都換了一個新的型別,這個新型別的前面都加上了raii
,而這又是什麼東西呢?
RAII
什麼是RAII 技術?
我們在C++中經常使用new申請了記憶體空間,但是卻也經常忘記delete回收申請的空間,容易造成記憶體溢位,於是RAII技術就誕生了,來解決這樣的問題。RAII(Resource Acquisition Is Initialization)機制是Bjarne Stroustrup首先提出的,是一種利用物件生命週期來控制程式資源(如記憶體、檔案控制代碼、網路連線、互斥量等等)的簡單技術。 我們知道在函式內部的一些成員是放置在棧空間上的,當函式返回時,這些棧上的區域性變數就會立即釋放空間,於是Bjarne Stroustrup就想到確保能執行資源釋放程式碼的地方就是在這個程式段(棧)中放置的物件的析構函數了,因為stack winding會保證它們的解構函式都會被執行。RAII就利用了棧裡面的變數的這一特點。RAII 的一般做法是這樣的:在物件構造時獲取資源,接著控制對資源的訪問使之在物件的生命週期內始終保持有效,最後在物件析構的時候釋放資源。藉此,我們實際上把管理一份資源的責任託管給了一個存放在棧空間上的區域性物件。
這種做法有兩大好處:
(1)不需要顯式地釋放資源。
(2)採用這種方式,物件所需的資源在其生命期內始終保持有效。
概括一下,raii
就是為了避免申請記憶體但是沒有釋放從而導致記憶體洩漏的情況出現所使用的一種技術,這種技術能夠在物件離開作用域是自動釋放。
http server 請求回撥函式
所以raii_event_base
和raii_evhttp
都是這兩種物件的一個變種。接下來的幾個evhttp_set_xxx
函式都是設定連線的限制條件以及請求的回撥函式http_request_cb
,來看看這個回撥函式的實現,
/** HTTP request callback */
static void http_request_cb(struct evhttp_request* req, void* arg)
{
std::unique_ptr<HTTPRequest> hreq(new HTTPRequest(req));
LogPrint(BCLog::HTTP, "Received a %s request for %s from %s\n",
RequestMethodString(hreq->GetRequestMethod()), hreq->GetURI(), hreq->GetPeer().ToString());
// Early address-based allow check
if (!ClientAllowed(hreq->GetPeer())) {
hreq->WriteReply(HTTP_FORBIDDEN);
return;
}
// Early reject unknown HTTP methods
if (hreq->GetRequestMethod() == HTTPRequest::UNKNOWN) {
hreq->WriteReply(HTTP_BADMETHOD);
return;
}
// 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, 加到workQueue中
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);
}
}
這個函式和之前例子中的HttpGenericCallback
函式所起的作用類似,都是用evhttp_set_gencb
進行設定的,不過這裡並沒有實際對請求進行處理,只是檢查請求的路徑是否有對應的處理函式,所有路徑的處理函式都儲存在在pathHandlers
變數中,通過RegisterHTTPHandler
進行新增,UnregisterHTTPHandler
進行刪除。如果在pathHandlers
中找到了對應的處理函式,那麼就將請求和對應的處理函式封裝到一個新的物件HTTPWorkItem
中,然後再把HTTPWorkItem
加入到workQueue
中,這個workQueue
由單獨的執行緒不斷的執行。如果找不到對應的處理函式或者請求的格式錯誤,那麼就返回對應的錯誤提示。