
【C三方组件】libcurlHTTP 客户端之王【摘要】libcurl 是一个跨平台传输库提供 HTTP/HTTPS 等协议的客户端能力。本文介绍 easy 与 multi 两套接口说明成熟协议库为什么能减少实现和维护成本再通过 GET、JSON POST、文件下载和并发请求四个案例展示用法。【版本基准】libcurl 8.22.0curl licenseC17。完整代码见 curl_demo.cpp。请求目标使用第 28 篇配套的本地 mock避免依赖公网响应与时延。1. Whatlibcurl 是什么libcurl 是 curl 项目提供的可嵌入传输库。curl 命令行程序与 libcurl 库属于同一项目但使用库时不需要启动命令行子进程。应用通过 C API 设置 URL、请求头、认证、超时和数据回调再由库完成传输。HTTP/HTTPS 是最常见的使用场景库也支持其他协议具体 TLS 后端、HTTP/2、HTTP/3 等能力取决于构建配置可以通过curl_version_info查询。它有两种主要驱动方式接口调用方式适合什么工作easycurl_easy_perform阻塞到传输结束顺序请求、后台任务、已有线程池中的请求multi多个 easy handle 加入同一管理器由应用驱动多请求并发、与现有事件循环集成easy handle 保存一次传输所需的配置和状态可以顺序复用multi 并不是另一套 HTTP 请求描述语言而是管理多个 easy handle 的执行。2. Why为什么不直接用 socket 发 HTTP向固定服务器发送一个简单请求并不困难。实际产品的复杂度会随着协议和运行环境增加新需求自行实现要继续补什么HTTPSTLS 库接入、证书链和主机名验证、后端差异分块或大响应HTTP 报文解析、分块编码、缓冲和流式消费重定向、代理、认证状态处理、代理协商、凭据使用范围超时与失败连接、解析、传输各阶段的超时和错误归因连续访问相同服务连接复用、DNS 缓存、连接失效处理多个请求同时进行非阻塞驱动、完成通知、连接数量控制除了写出第一版还要长期适配服务器行为、协议更新和平台变化。libcurl 把这些能力集中在一个广泛使用的库中应用可以把精力放在请求内容与业务处理上。它不会替业务判断“这个 POST 是否可以安全重试”也不会自动为所有应用选择相同的超时、响应体上限和重定向策略。采用成熟库之后仍要明确自己的运行约束。3. How接入与准备本地服务vcpkg 包名为curl。CMake 接入如下find_package(CURL REQUIRED) add_executable(app curl_demo.cpp) target_compile_features(app PRIVATE cxx_std_17) target_link_libraries(app PRIVATE CURL::libcurl)配套工程选择BLOG_COMPONENTScurl;httplib。按 示例说明构建后先在一个终端运行./build/httplib_demo serve18080该服务绑定回环地址提供/hi、/json、/download等端点。下文在另一个终端运行客户端。程序也允许传入其他 base URL源文件的命令行说明列出了参数顺序。3.1 公共配置与资源管理完整示例在进入请求前调用curl_global_init在全部 handle 释放后调用curl_global_cleanup。easy handle 用std::unique_ptr管理错误分支也能正确释放usingEasystd::unique_ptrCURL,decltype(curl_easy_cleanup);Easyeasy(curl_easy_init(),curl_easy_cleanup);if(!easy)throwstd::runtime_error(curl_easy_init failed);示例的make_easy(url)为本地练习设置以下策略所有setopt返回值都经check检查check(curl_easy_setopt(easy.get(),CURLOPT_URL,url.c_str()));check(curl_easy_setopt(easy.get(),CURLOPT_NOSIGNAL,1L));check(curl_easy_setopt(easy.get(),CURLOPT_CONNECTTIMEOUT_MS,2000L));check(curl_easy_setopt(easy.get(),CURLOPT_TIMEOUT_MS,5000L));check(curl_easy_setopt(easy.get(),CURLOPT_FOLLOWLOCATION,1L));check(curl_easy_setopt(easy.get(),CURLOPT_MAXREDIRS,5L));check(curl_easy_setopt(easy.get(),CURLOPT_ACCEPT_ENCODING,));这些是案例配置并不是每个项目必须照抄的“开场清单”。例如有些客户端需要观察原始 3xx 响应就不应自动跟随处理不可信 URL 时还应限制允许访问的目标及协议。3.2 GET接收响应并检查两层结果写回调可能被调用多次传入的数据也不保证以\0结尾应使用长度追加。C 回调边界不传播 C 异常staticsize_tcollect(char*data,size_t size,size_t count,void*context)noexcept{constautobytessize*count;try{static_caststd::string*(context)-append(data,bytes);returnbytes;}catch(...){return0;}}std::string body;charerror[CURL_ERROR_SIZE]{};autoeasymake_easy(base/hi);check(curl_easy_setopt(easy.get(),CURLOPT_ERRORBUFFER,error));check(curl_easy_setopt(easy.get(),CURLOPT_WRITEFUNCTION,collect));check(curl_easy_setopt(easy.get(),CURLOPT_WRITEDATA,body));constautoresultcurl_easy_perform(easy.get());if(result!CURLE_OK)throwstd::runtime_error(error[0]?error:curl_easy_strerror(result));longcode0;check(curl_easy_getinfo(easy.get(),CURLINFO_RESPONSE_CODE,code));CURLcode描述传输是否成功HTTP 状态码描述服务器的响应。默认情况下收到 404 或 500 并不意味着curl_easy_perform一定返回传输错误。完整示例先检查传输再判断是否收到预期的 2xx。./build/curl_demo getstatus200 bodyHello World!对于可能很大的响应不能一直向字符串追加。可以检查累计长度并中止或使用下一节的文件回调。3.3 POST JSON请求体和请求头的寿命libcurl 不负责把 C 对象转换为 JSON。示例使用固定字符串实际项目可以接入第 2 篇介绍的 JSON 库conststd::string payloadR({msg:hello, libcurl});Headersheaders(curl_slist_append(nullptr,Content-Type: application/json),curl_slist_free_all);if(!headers)throwstd::runtime_error(header allocation failed);check(curl_easy_setopt(easy.get(),CURLOPT_HTTPHEADER,headers.get()));check(curl_easy_setopt(easy.get(),CURLOPT_POSTFIELDS,payload.data()));check(curl_easy_setopt(easy.get(),CURLOPT_POSTFIELDSIZE_LARGE,static_castcurl_off_t(payload.size())));这段配置作用于 URL 为base /json的 easy handle接收回调与 GET 共用。POSTFIELDS默认借用数据字符串要活到传输结束请求头链表也要保持有效。完整源码用作用域和 RAII 保证这些关系。./build/curl_demo poststatus200 body{received:hello, libcurl}重用 handle 时要留意配置会保留。例如 POST 后改成 GET应明确设置方法、清理不再需要的请求体与请求头不能只换 URL 就假设所有选项已恢复默认。3.4 文件下载边接收边落盘把字符串回调换成文件回调就不需要把整个响应留在内存staticsize_tsave(char*data,size_t size,size_t count,void*context)noexcept{constautobytessize*count;try{autofile*static_caststd::ofstream*(context);file.write(data,static_caststd::streamsize(bytes));returnfile?bytes:0;}catch(...){return0;}}打开二进制输出文件后把它作为CURLOPT_WRITEDATA将save设置为CURLOPT_WRITEFUNCTION。返回值必须与已接收字节数一致返回短值会让传输失败。完整示例同时检查 HTTP 状态和文件关闭后的状态。./build/curl_demo download成功后得到download-curl.txt内容为download payload加换行。失败时文件可能只有一部分正式下载器通常写临时文件完成校验后再改名避免把半成品当成最终产物。3.5 multi单线程并发驱动多条请求完整案例创建两个 easy handle分别请求/hi和/echo?msgmulti加入同一个 multi。驱动循环的顺序是先推进传输读取完成消息有未完成任务时再等待。intrunning0;do{check_multi(curl_multi_perform(multi.value,running));intremaining0;while(auto*messagecurl_multi_info_read(multi.value,remaining)){if(message-msg!CURLMSG_DONE)continue;check(message-data.result);Job*jobnullptr;check(curl_easy_getinfo(message-easy_handle,CURLINFO_PRIVATE,job));constautocoderesponse_code(message-easy_handle);std::coutstatuscode bodyjob-body\n;if(code!200)throwstd::runtime_error(HTTP failure in multi);job-donetrue;}if(running)check_multi(curl_multi_poll(multi.value,nullptr,0,1000,nullptr));}while(running);CURLOPT_PRIVATE关联应用任务完成时反查是哪一条请求。配套Multi包装在退出时先移除 easy handle再销毁 multi任务及接收缓冲随后才释放。./build/curl_demo multi输出两条 200 响应完成先后不应写死。相同 multi 内的请求可以利用共享连接缓存但能否复用连接要看目的地址、协议、连接状态和并发方式不能保证任意两条请求只做一次 DNS 或 TLS 握手。4. 使用中需要确认的条件线程与初始化一个 handle 不能同时在多个线程中使用。curl_global_init从 7.84.0 起在具备CURL_VERSION_THREADSAFE特性的构建中支持线程安全统一在启动阶段初始化仍便于管理生命周期。共享缓存share API 可共享特定数据但连接池等对象不能简单理解为“加锁后即可跨线程任意共享”。按 线程安全文档逐项确认。DNS 超时NOSIGNAL1配合同步解析器时名称解析阶段可能无法被超时中断。需要 c-ares 或线程解析后端不能宣称一定由连接超时兜底。重定向上限8.3.0 起默认是 30 次本例显式设为 5 次表达自己的策略。TLS确认证书来源与构建后端。证书验证失败应排查信任链和主机名关闭验证会失去可靠的身份校验不能作为生产修复方式。重试根据幂等性、错误类型和剩余预算决定并控制次数与退避不能把网络失败直接等同于服务端没有执行请求。5. 选型与参考需要成熟传输能力、C ABI 或精细控制时可以直接用 libcurlC 业务代码希望减少配置与清理样板时可以看下一篇 cpr已经围绕 Asio 建设异步协议层时可以比较 Boost.Beast。选择取决于现有运行模型和功能需求。libcurl easy 接口、multi 接口。NOSIGNAL、重定向上限。Everything curl连接复用、协议和调试方法。完整示例四种运行模式、资源管理与错误分支。