1. 程式人生 > >比特幣原始碼解析(18)

比特幣原始碼解析(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_baseraii_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由單獨的執行緒不斷的執行。如果找不到對應的處理函式或者請求的格式錯誤,那麼就返回對應的錯誤提示。