C++网络编程实战:从Socket到HTTP,用libcurl构建健壮客户端
1. 项目概述从Socket到HTTP的跃迁搞了这么多年C网络编程从最底层的Socket API一路摸爬滚打上来我越来越觉得真正能让你的程序“走出去”和世界对话的往往是那些封装好的、成熟的协议与应用层库。如果说Socket编程是让你学会了如何“造电话”和“打电话”那么HTTP协议与libcurl的实战就是教你如何用这部电话去和全球数以亿计的“服务台”进行标准、高效的业务沟通。很多新手在掌握了TCP/UDP通信后面对一个具体的需求比如从某个天气API获取数据、向自己的服务器提交一个表单或者下载一个文件往往会感到无从下手——难道要自己从零开始拼装HTTP请求报文、解析响应头、处理重定向和Cookie吗当然不是。这就是libcurl存在的意义。它不是一个简单的“下载工具”而是一个功能极其强大、稳定且跨平台的客户端URL传输库。它帮你封装了HTTP、HTTPS、FTP、SMTP等数十种协议的复杂细节让你能用C/C以近乎“声明式”的方式完成复杂的网络交互。这次我们不谈空洞的理论直接进入实战。我会带你彻底搞懂HTTP协议在编程中的核心要点并用libcurl手把手实现几个最常用的场景GET请求获取数据、POST请求提交数据、处理JSON、管理连接与超时。你会发现原来那些看似复杂的网络操作用对了工具代码可以如此简洁优雅。2. HTTP协议核心要点精讲程序员视角在动手写代码之前我们必须把HTTP协议里那些直接影响编程的关键点掰扯清楚。教科书上的东西太全我们只抓重点。2.1 请求与响应数据是如何流动的HTTP的本质是一次“请求-响应”的交互。你的程序客户端发送一个格式化的文本块请求服务器解析后返回另一个格式化的文本块响应。一个最简单的GET请求报文长这样GET /api/v1/weather?cityBeijing HTTP/1.1 Host: api.weather.com User-Agent: MyCppApp/1.0 Accept: application/json第一行是请求行包含了方法GET、路径/api/v1/weather?cityBeijing和协议版本HTTP/1.1。紧接着是若干行请求头每行一个Key: Value对。头信息结束后有一个空行\r\n标识Header的结束。对于GET请求通常没有正文Body。服务器的响应报文则类似HTTP/1.1 200 OK Content-Type: application/json; charsetutf-8 Content-Length: 128 Date: Mon, 15 Apr 2024 08:00:00 GMT {city: Beijing, temperature: 22, condition: sunny}第一行是状态行包含协议版本、状态码200和状态描述OK。之后是响应头空行之后是响应正文Body这里是一段JSON。注意那个分隔Header和Body的空行\r\n至关重要。很多自己实现HTTP解析的新手会在这里栽跟头要么漏掉要么用的换行符不对HTTP规范要求是\r\n虽然很多服务器也容忍\n。使用libcurl的一个巨大好处就是这些底层细节它全帮你处理了。2.2 状态码服务器给你的“脸色”状态码是服务器对你请求的“第一句回应”。你必须理解它们因为你的程序逻辑要基于此进行分支。2xx 成功最熟悉的是200 OK。还有201 Created资源创建成功204 No Content成功但无返回体。3xx 重定向301 Moved Permanently永久重定向302 Found临时重定向。libcurl可以自动跟随重定向但你得知道它发生了有时需要关闭此功能以进行特殊处理。4xx 客户端错误400 Bad Request你的请求格式有问题比如JSON语法错误401 Unauthorized未认证403 Forbidden无权限404 Not Found资源不存在429 Too Many Requests请求过于频繁。看到4xx首先应该检查自己的请求构造是否正确。5xx 服务器错误500 Internal Server Error服务器内部错误502 Bad Gateway网关错误常见于Nginx等代理后端服务挂掉503 Service Unavailable服务不可用可能正在维护。看到5xx通常意味着问题在服务器端但你的程序需要有重试或降级策略。网络热词里反复出现的unexpected status 502 bad gateway就是一个典型的5xx错误。在你的C客户端里你需要检查CURLcode和HTTP状态码来区分是网络错误如超时还是服务器错误如502。2.3 请求方法、头部与Body告诉服务器你的意图GET vs POST这是最核心的区分。GET用于获取数据参数附在URL的查询字符串?keyvalue...中有长度限制且明文可见。POST用于提交数据数据放在请求正文中更安全且无长度限制。在RESTful API中GET、POST、PUT更新、DELETE删除分别对应查、增、改、删。Content-Type这是最重要的头部之一它定义了请求或响应正文的格式。常见值有application/x-www-form-urlencoded传统的表单提交格式如nameJohnage30。application/json现代API最常用的格式正文是JSON字符串。multipart/form-data用于文件上传。 如果你的客户端发送的是JSON但Content-Type设置成x-www-form-urlencoded服务器很可能返回400错误。其他常用头部Authorization用于传递认证信息如Bearer TokenAuthorization: Bearer your_token_here。User-Agent标识客户端软件。有些API会检查这个值设置一个合理的UA是良好实践。Accept告诉服务器客户端希望接收什么格式的数据如Accept: application/json。3. libcurl入门环境配置与第一个请求理论说再多不如一行代码。我们开始用libcurl。3.1 获取与编译libcurllibcurl官网提供了源代码和预编译库。对于Windows开发者如果你使用VC2008网络热词中提到确实需要寻找对应版本的预编译库或者自己用VC2008编译源码。更现代的做法是使用vcpkg或MSVC的包管理器。Linux/macOS上通常用包管理器安装开发包即可# Ubuntu/Debian sudo apt-get install libcurl4-openssl-dev # CentOS/RHEL sudo yum install libcurl-devel # macOS (使用Homebrew) brew install curlWindows VSCode MSVC环境配置对应部分网络热词下载curl官网的Windows二进制包或者使用vcpkgvcpkg install curl:x64-windows。在VSCode的c_cpp_properties.json中将curl的头文件路径include目录添加到includePath。在tasks.json用于编译和launch.json用于调试中将curl的库文件路径lib目录和库名如libcurl.lib添加到链接器设置中。这是一个典型的VSCode C配置痛点需要仔细设置。实操心得我强烈推荐在Linux下学习网络编程环境配置问题少更贴近生产环境。如果必须在Windows上使用MSYS2MinGW-w64或WSL来获得类似Linux的体验会比在纯Windows环境下折腾VC库轻松很多。3.2 最简单的GET请求示例下面是一个使用libcurl进行简单同步GET请求的完整示例。我们以获取一个公开的API例如http://httpbin.org/get一个用于HTTP测试的服务为例。#include iostream #include curl/curl.h // 这是一个回调函数用于处理从服务器接收到的数据。 // libcurl会把收到的数据一块一块地传到这里。 size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* userp) { size_t total_size size * nmemb; userp-append((char*)contents, total_size); return total_size; // 必须返回实际处理的字节数 } int main() { CURL* curl; CURLcode res; std::string readBuffer; // 用于存储服务器响应 // 1. 全局初始化只需一次 curl_global_init(CURL_GLOBAL_DEFAULT); // 2. 初始化一个CURL句柄可以理解为一次网络会话的控制器 curl curl_easy_init(); if(curl) { // 3. 设置这个会话的各种选项 // 设置目标URL curl_easy_setopt(curl, CURLOPT_URL, http://httpbin.org/get); // 设置接收数据的回调函数和存放位置 curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, readBuffer); // 设置User-Agent非必须但建议 curl_easy_setopt(curl, CURLOPT_USERAGENT, MyLibCurlClient/1.0); // 4. 执行请求这是一个阻塞调用直到请求完成或出错。 res curl_easy_perform(curl); // 5. 检查执行结果 if(res ! CURLE_OK) { // 网络层面错误如无法连接、超时等 std::cerr curl_easy_perform() failed: curl_easy_strerror(res) std::endl; } else { // 请求成功发出并收到响应 long http_code 0; // 获取HTTP响应状态码 curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, http_code); std::cout HTTP Status Code: http_code std::endl; std::cout Response Body:\n readBuffer std::endl; } // 6. 清理这个会话句柄 curl_easy_cleanup(curl); } // 7. 全局清理 curl_global_cleanup(); return 0; }编译并运行假设文件名为simple_get.cppg -o simple_get simple_get.cpp -lcurl ./simple_get你应该能看到来自httpbin.org返回的JSON数据其中包含了你的请求头等信息。代码逐行解析curl_global_init(): 初始化libcurl的全局环境涉及底层网络库如OpenSSL的初始化。通常用CURL_GLOBAL_DEFAULT。curl_easy_init(): 创建最重要的CURL句柄对象。后续所有操作都围绕它进行。curl_easy_setopt(): 这是libcurl的灵魂函数。通过设置大量的CURLOPT_*选项来配置请求行为。例子中设置了URL、写回调和用户数据指针。WriteCallback: 一个符合CURLOPT_WRITEFUNCTION签名的函数。每当libcurl收到数据就会调用它。我们将数据追加到传入的std::string中。必须返回实际处理的字节数否则libcurl会认为出错。curl_easy_perform(): 执行所有设置好的操作。它是同步的、阻塞的。curl_easy_getinfo(): 在请求执行后用于获取会话信息如HTTP状态码(CURLINFO_RESPONSE_CODE)、总耗时、下载大小等。curl_easy_cleanup()和curl_global_cleanup(): 对称的清理工作防止内存泄漏。4. 进阶实战处理POST请求与JSON数据GET请求只是开胃菜真正的交互始于POST。我们来看如何提交表单数据和JSON数据。4.1 提交表单数据 (application/x-www-form-urlencoded)假设我们要登录一个系统提交用户名和密码。// ... 头文件和WriteCallback同上 ... int main() { CURL* curl; CURLcode res; std::string readBuffer; curl_global_init(CURL_GLOBAL_DEFAULT); curl curl_easy_init(); if(curl) { // 设置目标URL登录接口 curl_easy_setopt(curl, CURLOPT_URL, http://httpbin.org/post); // 准备要POST的数据 std::string post_data usernamemyuserpasswordmypass123; // 设置POST请求方法 curl_easy_setopt(curl, CURLOPT_POST, 1L); // 设置POST数据 curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_data.c_str()); // 设置POST数据长度libcurl可以自动计算但显式设置更安全 curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, post_data.length()); // 重要设置Content-Type为表单格式 struct curl_slist* headers NULL; headers curl_slist_append(headers, Content-Type: application/x-www-form-urlencoded); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, readBuffer); res curl_easy_perform(curl); // 清理自定义的头部列表 curl_slist_free_all(headers); if(res ! CURLE_OK) { std::cerr POST failed: curl_easy_strerror(res) std::endl; } else { std::cout Response:\n readBuffer std::endl; } curl_easy_cleanup(curl); } curl_global_cleanup(); return 0; }关键点在于CURLOPT_POST、CURLOPT_POSTFIELDS和CURLOPT_HTTPHEADER。我们通过curl_slist来构造一个自定义的HTTP头部链表。4.2 提交JSON数据 (application/json)现代API几乎都用JSON。提交JSON数据与提交表单类似但Content-Type和数据处理方式不同。#include iostream #include curl/curl.h #include nlohmann/json.hpp // 使用一个流行的JSON库如 nlohmann/json using json nlohmann::json; // WriteCallback 同上 ... int main() { CURL* curl; CURLcode res; std::string readBuffer; // 构造一个JSON对象 json j; j[username] test_user; j[action] login; j[timestamp] 1713168000; // 将JSON对象序列化为字符串 std::string post_data j.dump(); curl_global_init(CURL_GLOBAL_DEFAULT); curl curl_easy_init(); if(curl) { curl_easy_setopt(curl, CURLOPT_URL, http://httpbin.org/post); curl_easy_setopt(curl, CURLOPT_POST, 1L); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_data.c_str()); curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, post_data.length()); // 关键设置Content-Type为application/json struct curl_slist* headers NULL; headers curl_slist_append(headers, Content-Type: application/json); // 可以添加更多头部例如认证Token // headers curl_slist_append(headers, Authorization: Bearer xyz123); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, readBuffer); res curl_easy_perform(curl); curl_slist_free_all(headers); if(res ! CURLE_OK) { std::cerr JSON POST failed: curl_easy_strerror(res) std::endl; } else { std::cout Response:\n readBuffer std::endl; // 你可以进一步解析返回的JSON // json response_json json::parse(readBuffer); // std::cout Parsed value: response_json[json][username] std::endl; } curl_easy_cleanup(curl); } curl_global_cleanup(); return 0; }这里我们引入了nlohmann/json库来方便地处理JSON。注意post_data是j.dump()产生的字符串。头部设置为application/json是必须的否则服务器可能无法正确解析。注意事项在实际项目中处理网络返回的JSON时务必进行异常捕获。因为网络返回的数据可能不是合法的JSON服务器可能返回一个HTML错误页面。使用json::parse时要用try-catch包裹防止程序崩溃。5. 生产环境必备超时、重试与连接管理一个健壮的网络客户端绝不能假设网络是完美的。超时、断连、服务器临时错误都是家常便饭。5.1 设置超时没有超时设置的网络请求是灾难的源头。libcurl提供了多种超时设置// 在 curl_easy_perform 调用前设置 // 整个传输允许的最大时间秒 curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L); // 连接阶段允许的最大时间秒 curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 5L); // 在两次libcurl传输数据包之间允许的最大不活动时间秒 // 对于慢速网络或服务器这个值可以设大一点 curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, 1L); // 1 Byte/s curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, 30L); // 如果30秒内速度低于1B/s则超时CURLOPT_TIMEOUT(总超时)这是最重要的。防止一个请求永远挂起。CURLOPT_CONNECTTIMEOUT(连接超时)专门针对TCP连接建立阶段。网络不通时这个设置能让你快速失败。CURLOPT_LOW_SPEED_*(低速超时)对于大文件下载特别有用防止在极慢的网络下无意义地等待。5.2 自动重试与错误处理libcurl本身不提供自动重试但我们可以基于错误码来实现简单的重试逻辑。int max_retries 3; int retry_delay_sec 2; // 重试等待间隔 CURLcode res; for (int retry 0; retry max_retries; retry) { res curl_easy_perform(curl); if (res CURLE_OK) { // 成功跳出循环 break; } // 判断是否为可重试的错误 bool should_retry false; switch(res) { case CURLE_COULDNT_CONNECT: // 连接失败 case CURLE_OPERATION_TIMEDOUT: // 超时 case CURLE_GOT_NOTHING: // 服务器返回空 // 可以添加更多你认为可重试的错误码如部分HTTP 5xx错误 should_retry true; break; default: // 其他错误如域名解析失败(CURLE_COULDNT_RESOLVE_HOST)、 // URL格式错误(CURLE_URL_MALFORMAT)等重试通常没用 should_retry false; } if (should_retry retry max_retries) { std::cerr Request failed ( curl_easy_strerror(res) ), retrying in retry_delay_sec seconds... ( retry1 / max_retries ) std::endl; sleep(retry_delay_sec); // 等待一段时间再重试 // 可选指数退避算法增加等待时间 // retry_delay_sec * 2; } else { // 不再重试或重试次数用尽 break; } } if (res ! CURLE_OK) { // 最终失败处理 std::cerr Request failed after retries: curl_easy_strerror(res) std::endl; }对于HTTP状态码错误如502需要在curl_easy_perform成功后通过curl_easy_getinfo获取状态码来判断。long http_code 0; curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, http_code); if (http_code 500) { // 服务器错误可以考虑重试 std::cerr Server error: http_code std::endl; } else if (http_code 400) { // 客户端错误重试通常无意义需要检查请求 std::cerr Client error: http_code std::endl; }5.3 连接复用与多线程安全频繁创建和销毁TCP连接开销巨大。libcurl支持连接池Connection Pool可以在同一个CURL句柄多次执行请求时复用连接前提是目标主机和端口相同。更常见的是使用多句柄接口(curl_multi)进行异步或并发请求但这超出了本文基础范围。关于多线程curl_global_init()和curl_global_cleanup()在程序生命周期内只需调用一次且curl_global_init不是线程安全的应在主线程初始化。但CURL句柄本身curl_easy_init创建的不是线程安全的。每个线程应该使用自己独立的CURL句柄。一个简单的模式是使用线程局部存储(Thread Local Storage)来为每个线程缓存一个CURL句柄。6. 常见问题排查与调试技巧即使用了libcurl你依然会遇到各种问题。下面是一些“踩坑”实录。6.1 编译与链接问题undefined reference to curl_easy_init...这是最经典的链接错误。确保编译命令正确链接了libcurl库-lcurl。在Windows的Visual Studio中需要在项目属性中添加libcurl.lib或curl.lib到附加依赖项并确保库目录正确。SSL/TLS相关错误如果请求HTTPS网址出现CURLE_SSL_CONNECT_ERROR等错误可能是因为libcurl编译时没有SSL支持或者找不到SSL证书。可以尝试使用curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);和curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);来跳过证书验证仅用于测试环境生产环境极其危险。指定CA证书包路径curl_easy_setopt(curl, CURLOPT_CAINFO, /path/to/cacert.pem);。6.2 运行时问题请求卡住永不返回99%是因为没设置超时(CURLOPT_TIMEOUT)。立刻加上。返回乱码这通常是字符编码问题。HTTP响应头中的Content-Type可能指定了编码如charsetgbk但你的程序默认用UTF-8处理。你需要解析响应头或者使用libcurl的CURLOPT_ACCEPT_ENCODING选项并确保你的写回调或后续处理能正确转换编码。一个更简单粗暴的测试方法是先让libcurl将响应输出到文件用文本编辑器看看是否正确。CURLE_COULDNT_CONNECT(7)无法连接主机。检查URL是否正确、目标服务是否启动、防火墙是否阻止。CURLE_SSL_CACERT(60) /CURLE_PEER_FAILED_VERIFICATION(51)SSL证书验证失败。参考上面的SSL问题解决。收到HTTP 400/415错误检查你的Content-Type请求头是否与发送的Body格式匹配。发送JSON时一定要设application/json。收到HTTP 401/403错误缺少或错误的认证信息。检查是否需要添加Authorization头或者使用curl_easy_setopt(curl, CURLOPT_USERPWD, username:password)进行HTTP Basic认证。收到HTTP 502/503/504错误服务器或网关错误。你的程序需要实现重试机制和降级逻辑。6.3 使用libcurl内置调试libcurl提供了强大的调试输出功能能让你看到整个HTTP交互的细节包括发送的请求头和接收的响应头这对排查问题至关重要。// 启用详细模式 curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); // 将调试信息输出到标准错误stderr也可以指定一个FILE* curl_easy_setopt(curl, CURLOPT_STDERR, stderr);运行程序你会在控制台看到类似这样的输出* Trying 54.xxx.xxx.xxx:80... * Connected to httpbin.org (54.xxx.xxx.xxx) port 80 (#0) GET /get HTTP/1.1 Host: httpbin.org User-Agent: MyLibCurlClient/1.0 Accept: */* ... HTTP/1.1 200 OK Date: Mon, 15 Apr 2024 08:00:00 GMT Content-Type: application/json Content-Length: 273 Connection: keep-alive Server: gunicorn/19.9.0 Access-Control-Allow-Origin: * Access-Control-Allow-Credentials: true { [273 bytes data]开头的行是libcurl发送的开头的行是服务器返回的。通过这个你可以确认你的请求头是否正确设置服务器返回了什么。6.4 连接复用与“心跳”问题网络热词中提到了“心跳定时器”。在长连接如WebSocket或某些自定义TCP协议中心跳用于保活。对于HTTP/1.1默认是Connection: keep-alivelibcurl会尽量复用连接。但如果你长时间没有请求中间的网络设备如防火墙、负载均衡器可能会断开空闲连接。这时你需要使用连接复用保持CURL句柄用它发起多次请求。处理超时设置合理的CURLOPT_TIMEOUT和CURLOPT_CONNECTTIMEOUT。错误重试如上一节所述对连接类错误进行重试。对于纯粹的HTTP客户端通常不需要自己实现“心跳”。但如果你基于libcurl实现一个需要维持长连接的客户端比如用于HTTP/2或未来HTTP/3的流式请求则需要更复杂的连接状态管理。7. 封装与实践构建一个简单的HTTP客户端类将上述知识封装成一个易于使用的类是工程化的第一步。下面是一个极简的封装示例// simple_http_client.hpp #pragma once #include string #include map #include curl/curl.h class SimpleHttpClient { public: SimpleHttpClient(); ~SimpleHttpClient(); // 禁用拷贝 SimpleHttpClient(const SimpleHttpClient) delete; SimpleHttpClient operator(const SimpleHttpClient) delete; // 配置 void SetTimeout(long timeout_sec); // 总超时 void SetConnectTimeout(long timeout_sec); // 连接超时 void AddHeader(const std::string key, const std::string value); void ClearHeaders(); // 执行请求 bool Get(const std::string url, std::string response); bool Post(const std::string url, const std::string data, const std::string content_type, std::string response); bool PostJson(const std::string url, const std::string json_data, std::string response); // 获取最后一次请求的信息 long GetLastHttpCode() const { return last_http_code_; } CURLcode GetLastCurlCode() const { return last_curl_code_; } const std::string GetLastError() const { return last_error_; } private: static size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* output); bool PerformRequest(const std::string url, std::string response); void SetCommonOptions(); void UpdateLastError(CURLcode code); CURL* curl_handle_; struct curl_slist* headers_; long last_http_code_; CURLcode last_curl_code_; std::string last_error_; };// simple_http_client.cpp #include simple_http_client.hpp #include sstream SimpleHttpClient::SimpleHttpClient() : curl_handle_(nullptr), headers_(nullptr), last_http_code_(0), last_curl_code_(CURLE_OK) { curl_global_init(CURL_GLOBAL_DEFAULT); curl_handle_ curl_easy_init(); if (curl_handle_) { SetCommonOptions(); } } SimpleHttpClient::~SimpleHttpClient() { if (headers_) { curl_slist_free_all(headers_); headers_ nullptr; } if (curl_handle_) { curl_easy_cleanup(curl_handle_); curl_handle_ nullptr; } curl_global_cleanup(); } void SimpleHttpClient::SetCommonOptions() { curl_easy_setopt(curl_handle_, CURLOPT_WRITEFUNCTION, WriteCallback); // CURLOPT_WRITEDATA 在每次请求前设置 curl_easy_setopt(curl_handle_, CURLOPT_USERAGENT, SimpleHttpClient/1.0); // 默认启用跟随重定向最多5次 curl_easy_setopt(curl_handle_, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl_handle_, CURLOPT_MAXREDIRS, 5L); } size_t SimpleHttpClient::WriteCallback(void* contents, size_t size, size_t nmemb, std::string* output) { size_t total_size size * nmemb; output-append((char*)contents, total_size); return total_size; } void SimpleHttpClient::SetTimeout(long timeout_sec) { if (curl_handle_) { curl_easy_setopt(curl_handle_, CURLOPT_TIMEOUT, timeout_sec); } } void SimpleHttpClient::SetConnectTimeout(long timeout_sec) { if (curl_handle_) { curl_easy_setopt(curl_handle_, CURLOPT_CONNECTTIMEOUT, timeout_sec); } } void SimpleHttpClient::AddHeader(const std::string key, const std::string value) { std::string header_line key : value; headers_ curl_slist_append(headers_, header_line.c_str()); } void SimpleHttpClient::ClearHeaders() { if (headers_) { curl_slist_free_all(headers_); headers_ nullptr; } } void SimpleHttpClient::UpdateLastError(CURLcode code) { last_curl_code_ code; if (code ! CURLE_OK) { last_error_ curl_easy_strerror(code); } else { last_error_.clear(); } } bool SimpleHttpClient::PerformRequest(const std::string url, std::string response) { if (!curl_handle_) return false; response.clear(); curl_easy_setopt(curl_handle_, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl_handle_, CURLOPT_WRITEDATA, response); // 设置自定义头部 curl_easy_setopt(curl_handle_, CURLOPT_HTTPHEADER, headers_); CURLcode res curl_easy_perform(curl_handle_); UpdateLastError(res); if (res CURLE_OK) { curl_easy_getinfo(curl_handle_, CURLINFO_RESPONSE_CODE, last_http_code_); return (last_http_code_ 200 last_http_code_ 300); // 通常认为2xx为成功 } return false; } bool SimpleHttpClient::Get(const std::string url, std::string response) { if (curl_handle_) { curl_easy_setopt(curl_handle_, CURLOPT_HTTPGET, 1L); // 设置为GET方法 // 对于GET通常不需要POST相关字段确保清除 curl_easy_setopt(curl_handle_, CURLOPT_POSTFIELDS, nullptr); curl_easy_setopt(curl_handle_, CURLOPT_POSTFIELDSIZE, 0L); } return PerformRequest(url, response); } bool SimpleHttpClient::Post(const std::string url, const std::string data, const std::string content_type, std::string response) { if (!curl_handle_) return false; // 临时添加Content-Type头 struct curl_slist* temp_headers headers_; std::string ct_header Content-Type: content_type; temp_headers curl_slist_append(temp_headers, ct_header.c_str()); curl_easy_setopt(curl_handle_, CURLOPT_HTTPHEADER, temp_headers); curl_easy_setopt(curl_handle_, CURLOPT_POST, 1L); curl_easy_setopt(curl_handle_, CURLOPT_POSTFIELDS, data.c_str()); curl_easy_setopt(curl_handle_, CURLOPT_POSTFIELDSIZE, data.size()); bool success PerformRequest(url, response); // 移除临时添加的Content-Type头简化处理实际可能需要更精细的管理 // 这里我们直接恢复原来的headers_因为temp_headers是新链表 // 更健壮的做法是管理一个头部列表的副本 curl_slist_free_all(temp_headers); // 注意这会释放整个临时链表但原headers_未被修改 // 重新设置句柄使用原headers_ curl_easy_setopt(curl_handle_, CURLOPT_HTTPHEADER, headers_); return success; } bool SimpleHttpClient::PostJson(const std::string url, const std::string json_data, std::string response) { AddHeader(Content-Type, application/json); bool result Post(url, json_data, application/json, response); // 注意这个简单的实现会永久添加Content-Type: application/json头到headers_列表。 // 更好的实现是每次Post/PostJson时构造独立的头部链表。 return result; }这个类非常基础但它演示了如何封装curl句柄、管理头部、区分GET/POST。在实际项目中你需要考虑线程安全每个线程一个实例、更完善的错误处理、连接池、异步请求等。但以此为起点你已经可以构建出能满足大多数场景的C HTTP客户端了。