C++使用libcurl进行http通讯
借着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, 暂时没写, 有空补上.
如有错误疏漏, 请务必指出, 十分感谢, 同时欢迎一起探讨相关问题, 转载请注明, 感谢!