C++使用libcurl進行http通訊

Z發表於2021-02-22

藉著curl 7.75.0版本更新, 最近又下載下來玩了玩, 在此做個簡單記錄

1.環境搭建

首先是libcurl動態庫, 自己下載原始碼編譯的話如果要使用https協議還要下載OpenSSL和libssh的原始碼一起編譯, 我嫌麻煩, 所以直接官網下載的官方編譯好的動態庫

linux一般自帶的有或者直接apt get都很方便了

這裡放個windows環境的下載地址 : https://curl.se/windows/

 

紅框部分是curl部分功能的依賴庫, 這裡我建議都下載下來扔到專案目錄裡

 

下載下來解壓後curl目錄結構如上圖, 其中bin放的是動態庫, lib是靜態庫, include裡是標頭檔案, 需要提及的是lib中兩個靜態庫都是.a結尾的, 較小且帶dll的應該是windows版本的, 我在編譯自己的程式時將這個靜態庫名稱改成了libcurl.lib

 

 最後我將有可能用到的動態庫, 靜態庫, 證書, 標頭檔案整合了一下, 內容如下:

之後在自己的程式中連結libcurl, 包含curl目錄下的標頭檔案, 將dll放在可執行程式同目錄下就可以開始使用了.

 

2. 呼叫介面進行http通訊

下面先列一下curl請求的基本流程和重要變數

(1) CURLcode curl_global_init() : 該介面用於初始化curl庫, 應該在所有curl操作之前被呼叫

(2) CURL* curl_easy_init() : 該介面返回一個curl控制程式碼, 型別為CURL*, 一次會話的相關操作都在這個返回控制程式碼上進行

(3) void curl_easy_cleanup(CURL*) : 該介面用於釋放給定curl控制程式碼, 每一次會話結束都應該呼叫此介面釋放對應的curl控制程式碼

(4) CURLcode curl_easy_setopt(CURL*, CURLoption, ...) : 該介面通過傳入不同的巨集可以設定指定curl控制程式碼的相關屬性, 以此控制會話的各種屬性內容

這裡給一個官方連結可以查詢OPT的含義, 其中也包含官方的example : https://curl.se/libcurl/c/curl_easy_setopt.html

(5) CURLcode curl_easy_perform(CURL*) : 通過給定控制程式碼執行通訊會話

(6) CURLcode : 幾乎所有的curl介面的返回值都為此型別, 這個code定義了所有curl操作時的狀態, 這裡給一個官方連結可以查詢code的含義 : https://curl.se/libcurl/c/libcurl-errors.html

(7) const char* curl_easy_strerror(CURLcode) : 將CURLcode轉為對應含義的字串方便進行日誌輸出等操作

以上是我經常使用到的curl介面, curl功能強大, 支援的協議與內容遠不止http/https

官方自己給出的評價是 : libcurl is probably the most portable, most powerful and most often used network transfer library on this planet.

這裡我封裝了兩個功能, 分別是http GET請求網頁和http GET下載檔案, 過程中啟用了cookie.

上程式碼 :

自己封裝的curl類

 1 class CHttpClient
 2 {
 3 public:
 4     CHttpClient();
 5     ~CHttpClient();
 6 
 7     long http_enable_cookie(const char *path);
 8     long http_post(const char *url);
 9     long http_submit(const char *url, std::vector<std::string> &form);
10     long http_get(const char *url, std::string &body);
11     long http_download(const char *url, const char *fullpath);
12     long http_add_header(const char *header);
13     long http_add_multi_header(std::vector<std::string> &list);
14 
15 private:
16     bool prepare_curl(const char *url);
17     bool exec_curl();
18     bool try_cleanup_curl();
19 
20 private:
21     CURL *m_pCurl;
22     struct curl_slist *m_pHeader;
23     bool m_bSetCookie;
24     bool m_bSetHeader;
25     char m_szCookiePath[MAX_PATH];
26 };

最主要的curl_global_init放在了建構函式中, 這裡不再展示, 其中prepare_curl, exec_curl, try_cleanup_curl為我對curl http通訊流程的基本封裝

下面展示上述三個介面

 1 bool CHttpClient::prepare_curl(const char *url)
 2 {
 3     m_pCurl = curl_easy_init();
 4     if (nullptr == m_pCurl) return false;
 5 
 6     curl_easy_setopt(m_pCurl, CURLOPT_URL, url);
 7     curl_easy_setopt(m_pCurl, CURLOPT_FOLLOWLOCATION, 1L);
 8     curl_easy_setopt(m_pCurl, CURLOPT_SSL_VERIFYPEER, 0L);
 9     curl_easy_setopt(m_pCurl, CURLOPT_SSL_VERIFYHOST, 0L);
10 
11     if (m_bSetHeader)
12     {
13         curl_easy_setopt(m_pCurl, CURLOPT_HTTPHEADER, m_pHeader);
14     }
15 
16     if (m_bSetCookie)
17     {
18         curl_easy_setopt(m_pCurl, CURLOPT_COOKIEJAR, m_szCookiePath);    //set-cookie將會修改此路徑對應cookie快取檔案
19         curl_easy_setopt(m_pCurl, CURLOPT_COOKIEFILE, m_szCookiePath);    //傳送請求時將會從此檔案中讀取cookie
20     }
21 
22 #ifdef DEBUG
23     curl_easy_setopt(m_pCurl, CURLOPT_VERBOSE, 1L);
24     curl_easy_setopt(m_pCurl, CURLOPT_DEBUGFUNCTION, cb_dbg);
25 #endif
26     
27     return true;
28 }
29 
30 bool CHttpClient::exec_curl()
31 {
32     CURLcode retCode = curl_easy_perform(m_pCurl);
33     try_cleanup_curl();
34 
35 #ifdef DEBUG
36     print_dbg_msg();
37 #endif
38 
39     if (CURLE_OK != retCode)
40     {
41         LOG_MSG(LOG_ERROR, "curl execute with code[%d] msg[%s]", retCode, curl_easy_strerror(retCode));
42         return false;
43     }
44     return true;
45 }
46 
47 bool CHttpClient::try_cleanup_curl()
48 {
49     if (nullptr != m_pCurl)
50     {
51         curl_easy_cleanup(m_pCurl);
52         m_pCurl = nullptr;
53     }
54 
55     if (m_bSetHeader)
56     {
57         curl_slist_free_all(m_pHeader);
58         m_pHeader = nullptr;
59         m_bSetHeader = false;
60     }
61 
62     return true;
63 }

prepare_curl主要進行curl控制程式碼的初始化, 設定http通用的引數

exec_curl執行curl通訊, 通訊完成後呼叫try_cleanup_curl進行記憶體釋放, 並列印debug通訊資訊

針對prepare_curl中curl_easy_setopt的引數, 這裡展開解釋一下

(1)CURLOPT_URL : http通訊的地址, 可以解析域名

(2)CURLOPT_FOLLOWLOCATION : 跟隨網頁重定向

(3)CURLOPT_SSL_VERIFYPEER & CURLOPT_SSL_VERIFYHOST : 雙端是否進行SSL安全驗證, 此處我把這個功能關掉了, 正常生產環境是不會這樣做的, curl庫中也帶的有證書, 老版本可能需要更新一下證書防止有些網頁不能訪問, 這裡我只做除錯, 就比較隨意了

(4)CURLOPT_HTTPHEADER : 設定http header, 這裡傳入curl_slist結構體, 使用curl_slist_append可以直接把const char*型別字串加入這個結構體, 如果不設定, curl預設請求頭只有GET, Accept,Host

(5)CURLOPT_COOKIEJAR : 指定本次通訊cookie儲存的路徑, 儲存操作在對應的curl控制程式碼執行curl_easy_cleanup時執行

(6)CURLOPT_COOKIEFILe : 指定本次通訊cookie讀取的路徑

(7)CURLOPT_VERBOSE : 設定是否回顯通訊內容, 開啟後如果不指定回撥函式, 則使用stderr

(8)CURLOPT_DEBUGFUNCTION : 設定回顯時呼叫的回撥函式, 回撥函式的引數列表應為(CURL *curl, curl_infotype type, char *data, size_t size, void *usr_ptr), 其中type指示了當前data的型別, 型別包括CURLINFO_TEXT, CURLINFO_HEADER_IN, CURLINFO_HEADER_OUT, CURLINFO_DATA_IN, CURLINFO_DATA_OUT, CURLINFO_SSL_DATA_IN, CURLINFO_SSL_DATA_OUT, CURLINFO_END, 具體含義參考官方文件實際除錯一下比較好理解

下面展示CURLOPT_DEBUGFUNCTION對應的回撥函式以及print_dbg_msg列印函式

 1 static std::string g_sHeaderOut;
 2 static std::string g_sHeaderIn;
 3 static std::string g_sDataOut;
 4 static std::string g_sDataIn;
 5 
 6 int cb_dbg(CURL *curl, curl_infotype type, char *data, size_t size, void *usr_ptr)
 7 {
 8     switch (type)
 9     {
10     case CURLINFO_HEADER_OUT:
11         g_sHeaderOut.append(data, size);
12         break;
13     case CURLINFO_DATA_OUT:
14         g_sDataOut.append(data, size);
15         break;
16     case CURLINFO_HEADER_IN:
17         g_sHeaderIn.append(data, size);
18         break;
19     case CURLINFO_DATA_IN:
20         g_sDataIn.append(data, size);
21         break;
22     default:
23         break;
24     }
25     return 0;
26 }
27 
28 void print_dbg_msg()
29 {
30     if (!g_sHeaderOut.empty())
31     {
32         str_replace(g_sHeaderOut, "%", "%%");
33         LOG_MSG(LOG_DEBUG, "%s", g_sHeaderOut.c_str());
34         g_sHeaderOut.clear();
35     }
36 
37     if (!g_sDataOut.empty())
38     {
39         str_replace(g_sDataOut, "%", "%%");
40         LOG_MSG(LOG_DEBUG, "%s", g_sDataOut.c_str());
41         g_sDataOut.clear();
42     }
43 
44     if (!g_sHeaderIn.empty())
45     {
46         str_replace(g_sHeaderIn, "%", "%%");
47         LOG_MSG(LOG_DEBUG, "%s", g_sHeaderIn.c_str());
48         g_sHeaderIn.clear();
49     }
50 
51     if (!g_sDataIn.empty())
52     {
53         str_replace(g_sDataIn, "%", "%%");
54         LOG_MSG(LOG_DEBUG, "%s", g_sDataIn.c_str());
55         g_sDataIn.clear();
56     }
57 }

這裡因為我自己寫的日誌列印使用vsprinf遇到%會報錯, 這裡我又封裝了一個string的replace函式把%替換成%%, 列印的時候可能不太美觀, 暫時還沒花時間優化

下面展示GET請求和GET download請求

 1 long CHttpClient::http_get(const char *url, std::string &res)
 2 {
 3     if (!prepare_curl(url)) return TSI_INTERNAL_ERR;
 4 
 5     // CURLOPT_WRITEDATA後的引數會傳給回撥函式的usrdata
 6     curl_easy_setopt(m_pCurl, CURLOPT_WRITEFUNCTION, cb_get);
 7     curl_easy_setopt(m_pCurl, CURLOPT_WRITEDATA, &res);
 8 
 9     if (!exec_curl())
10     {
11         LOG_MSG(LOG_ERROR, "http get fail");
12         return TSI_INTERNAL_ERR;
13     }
14     return TSI_NO_ERR;
15 }
16 
17 long CHttpClient::http_download(const char *url, const char *fullpath)
18 {
19     if (!prepare_curl(url)) return TSI_INTERNAL_ERR;
20 
21     //二進位制寫入模式建立下載檔案
22     FILE *download_file = fopen(fullpath, "wb");
23     if (nullptr == download_file)
24     {
25         try_cleanup_curl();
26         return TSI_INTERNAL_ERR;
27     }
28 
29     //將檔案控制程式碼設定到下載回撥中, curl內部會將大檔案分割並多次呼叫回撥寫入資料
30     curl_easy_setopt(m_pCurl, CURLOPT_WRITEFUNCTION, cb_download);
31     curl_easy_setopt(m_pCurl, CURLOPT_WRITEDATA, download_file);
32 
33     //TODO:要確定一下下載過程是否是非同步的, 防止檔案還沒下載完畢, 後面就fclose了
34     if (!exec_curl())
35     {
36         LOG_MSG(LOG_ERROR, "http download [%s] fail", fullpath);
37         fclose(download_file);
38         std::remove(fullpath);
39         return TSI_INTERNAL_ERR;
40     }
41 
42     LOG_MSG(LOG_INFO, "http download [%s] success", fullpath);
43     fclose(download_file);
44     return TSI_NO_ERR;
45 }

其中主要涉及兩個CURLOPT, 此處展開解釋

(1)CURLOPT_WRITEFUNCTION : 該引數指定get請求到的內容的寫入方法, curl預設使用fwrite, 該回撥函式引數列表必須為(char *data, size_t size, size_t nmemb, void *usrdata)

(2)CURLOPT_WRITEDATA : 該引數將後跟的資料作為引數傳入指定的writefunction中

下面展示兩個回撥函式cb_get和cb_download

 1 size_t cb_get(char *data, size_t size, size_t nmemb, void *usrdata)
 2 {
 3     size_t data_size = size * nmemb;
 4     static_cast<std::string*>(usrdata)->append(data, data_size);
 5     return data_size;
 6 }
 7 
 8 size_t cb_download(char *data, size_t size, size_t nmemb, void *usrdata)
 9 {
10     size_t data_size = size * nmemb;
11     fwrite(data, size, nmemb, (FILE*)usrdata);
12     return data_size;
13 }

因為download功能涉及具體網站的分析, 這裡就不展示除錯內容了

以上是http get請求的簡單例項, 常用功能應該還有form POST, 暫時沒寫, 有空補上.

如有錯誤疏漏, 請務必指出, 十分感謝, 同時歡迎一起探討相關問題, 轉載請註明, 感謝!

相關文章