藉著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, 暫時沒寫, 有空補上.
如有錯誤疏漏, 請務必指出, 十分感謝, 同時歡迎一起探討相關問題, 轉載請註明, 感謝!