高效能 C++ HTTP 客戶端原理與實現

Kevin Wan發表於2021-08-25

一、什麼是Http Client

Http協議,是全網際網路共同的語言,而Http Client,可以說是我們需要從網際網路世界獲取資料的最基本方法,它本質上是一個URL到一個網頁的轉換過程。而有了基本的Http客戶端功能,再搭配上我們想要的規則和策略,上至內容檢索下至資料分析都可以實現了。

繼上一次介紹用Workflow可以10行C++程式碼實現一個高效能Http伺服器,今天繼續給大家用C++實現一個高效能的Http客戶端也同樣很簡單!

// [http_client.cc]
#include "stdio.h"
#include "workflow/HttpMessage.h"
#include "workflow/WFTaskFactory.h"

int main (int argc, char *argv[])
{
    const char *url = "https://github.com/sogou/workflow";
    WFHttpTask *task = WFTaskFactory::create_http_task (url, 2, 3,
            [](WFHttpTask * task) { 
                fprintf(stderr, "%s %s %s\r\n",
                        task->get_resp()->get_http_version(),
                        task->get_resp()->get_status_code(),
                        task->get_resp()->get_reason_phrase());
    });
    task->start();
    getchar(); // press "Enter" to end.
    return 0;
}

只要安裝好了Workflow,以上程式碼即可以通過以下命令編譯出一個簡單的http_client:

g++ -o http_client http_client.cc --std=c++11 -lworkflow -lssl -lcrypto -lpthread

根據Http協議,我們執行這個可執行程式 ./http_client,就會得到以下內容:

HTTP/1.1 200 OK

同理,我們還可以通過其他api來獲得返回的其他Http header和Http body,一切內容都在這個 WFHttpTask 中。而因為Workflow是個非同步排程框架,因此這個任務發出之後,不會阻塞當前執行緒,外加內部自帶的連線複用,從根本上保證了我們的Http Client的高效能。

接下來給大家詳細講解一下原理~

二、請求的過程

1. 建立Http任務

上述demo可以看到,請求是通過發起一個Workflow的Http非同步任務來實現的,建立任務的介面如下:

WFHttpTask *create_http_task(const std::string& url,
                             int redirect_max, int retry_max,
                             http_callback_t callback);

第一個引數就是我們要請求的URL。對應的,在一開始的示例中,我們的重定向次數redirect_max是2次,而重試次數retry_max是3次。第四個引數是一個回撥函式,示例中我們用了一個lambda,由於Workflow的任務都是非同步的,因此我們處理結果這件事情是被動通知我們的,結果回來就會調起這個回撥函式,格式如下:

using http_callback_t = std::function<void (WFHttpTask *)>;

2. 填寫header併發出

我們的網路互動無非是請求-回覆,對應到Http Client上,在我們建立好了task之後,我們有一些時機是處理請求的,在Http協議裡,就是在header裡填好協議相關的事情,比如我們可以通過Connection來指定希望得到建立Http的長連線,以節省下次建立連線的耗時,那麼我們可以把Connection設定為Keep-Alive。示例如下:

protocol::HttpRequest *req = task->get_req();
req->add_header_pair("Connection", "Keep-Alive");
task->start();

最後我們會把設定好請求的任務,通過 task->start(); 發出。最開始的 http_client.cc 示例中,有一個 getchar(); 語句,是因為我們的非同步任務發出後是非阻塞的,當前執行緒不暫時停住就會退出,而我們希望等到回撥函式回來,因此我們可以用多種暫停的方式。

3. 處理返回結果

一個返回結果,根據Http協議,會包含三部分:訊息行訊息頭header訊息正文body。如果我們想要獲取body,可以這樣:

const void *body;
size_t body_len;
task->get_resp()->get_parsed_body(&body, &body_len); 

三、高效能的基本保證

我們使用C++來寫Http Client,最香的就是可以利用其高效能。Workflow對高併發是如何保證的呢?其實就兩點:

  • 純非同步;
  • 連線複用;

前者是對執行緒資源的重複利用、後者是對連線資源的重複利用,這些框架層級都為使用者管理好了,充分減少開發者的心智負擔。

1. 非同步排程模式

同步和非同步的模式直接決定了我們的Http Client可以有多大的併發度。為什麼呢?通過下圖可以先看看同步框架發起三個Http任務,執行緒模型是怎樣的:

網路延遲往往非常大,如果我們在同步等待任務回來的話,執行緒就會一直被佔用。這時候我們需要看看非同步框架是如何實現的:

如圖所示,只要任務發出之後,執行緒即可做其他事情,我們傳入了一個回撥函式做非同步通知,因此等任務的網路回覆收完之後,再讓執行緒執行這個回撥函式即可拿到Http請求的結果,期間多個任務併發出去的時候,執行緒是可以複用的,輕鬆達到幾十萬的QPS併發度。

2. 連線複用

我們剛才有提到,只要我們建立了長連線,即可提高效率。為什麼呢?因為框架對連線有複用。我們先來看看如果一個請求就建立一個連線,會是什麼樣的情況:

很顯然,佔用大量的連線是對系統資源的浪費,而且每次都要做connect以及close是非常耗時的,除了TCP常見的握手以外,許多應用層協議建立連線的過程也會相對複雜。但使用Workflow就不會有這樣的煩惱,Workflow會在任務發出的時候自動查詢當前可以複用的連線,如果沒有才會自動建立,完全不需要開發者關心連線如何複用的細節:

3. 解鎖其他功能

當然,除了以上的高效能以外,一個高效能的Http Client往往還有許多其他的需求,這裡可以結合實際情況與大家分享:

  1. 結合workflow的串並聯任務流,實現超大規模並行抓取
  2. 按順序或者按指定速度請求某個站點的內容,避免請求過猛被封禁;
  3. Http Client遇到redirect可以自動幫我做跳轉,一步到位請求到最終結果;
  4. 希望通過proxy代理訪問HTTPHTTPS資源;

以上這些需求,要求框架對於Http任務的編排有超高的靈活性,以及對實際需求(比如redirect、ssl代理等功能)有非常接地氣的支援,這些Workflow都已經實現。

專案地址

https://github.com/sogou/workflow

歡迎使用 workflowstar 支援一下!

相關文章