ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C++ HTTP客户端restclient-cpp:像requests一样发请求的实战指南

C++ HTTP客户端restclient-cpp:像requests一样发请求的实战指南 简介restclient-cpp 是一份面向 C 开发者的轻量级 HTTP REST 客户端库封装了基于 libcurl 的网络传输支持 GET、POST、PUT、DELETE、PATCH 等常见请求方法并内置请求头管理、参数编码、响应解析与错误处理机制适合需要快速接入 RESTful 服务的桌面或后端项目。压缩包共 388 个文件约 1.17MB涵盖核心头文件.h、实现源码.cc/.cpp、HTML 文档、JavaScript 脚本、Python 测试辅助脚本以及 Visual Studio / Xcode / CMake 等多平台工程配置既能直接集成也可作为学习 libcurl 封装与跨平台构建的范例。资源目前已获 1003 人学习代码树包含 gtest 单元测试与 automake 构建体系读者可据此了解库的接口设计、测试组织方式并借助 packagecloud 等渠道快速纳入自身项目依赖。 先交代一下背景我之前用C做内部服务的时候最烦的一件事就是调别人的HTTP接口。项目里倒是不缺HTTP库但要么太底层对着libcurl写回调要么太重引一个Boost.Beast进来编译时间直接翻倍。后来同事扔给我一个restclient-cpp的封装说“你就当它是C的requests”我用完第一次就记住了这个库。今天这篇就把它的用法、原理和坑从头到尾理一遍给同样被libcurl回调折磨过的C开发者一个参考。适合谁不想碰libcurl回调、只需要同步阻塞式请求、在写内部工具/脚本/微服务客户端/CI工具的C开发者。能做什么一行函数发GET/POST/PUT/DELETE/HEAD/PATCH请求拿到状态码、响应头和响应体。不适合谁要异步非阻塞、要连接池、要HTTP/2、要下载上百MB大文件的场景——这个最后一线我会专门说。1. 为什么在C里发个HTTP请求会这么别扭先把问题摆出来。libcurl是C语言接口功能确实强但开发体验一言难尽。想发一个最简单的GET请求你得经历这些CURL *curl curl_easy_init(); std::string response; curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, response); CURLcode res curl_easy_perform(curl); curl_easy_cleanup(curl);每次都要setopt一堆选项再传一个回调函数把响应体接出来。写一次两次还能忍当你的代码里有十几个请求、每个还要带不同的header、处理不同的超时你会发现大部分时间都花在“配置curl”而不是“处理业务”上。restclient-cpp就是来解决这个痛点的。它把libcurl常见操作封装成了几个同步函数直接以RestClient::get(url)、RestClient::post(url, content_type, data)这种形式暴露出来。返回值是一个统一的RestClient::Response结构体里面就三个字段状态码、响应体、响应头。你不需要关心CURLOPT_这些宏也不需要用回调函数把数据拼出来——它全帮你做完了。有人会说那用cpp-httplib不就行了cpp-httplib确实是header-only很优秀但它有自己的HTTP协议栈实现某些环境里要处理SSL、要处理平台差异出了问题你得自己啃协议代码。restclient-cpp是建立在libcurl之上的也就是说SSL、DNS、代理、HTTP协议版本这些脏活累活还是交给libcurl它只做一层轻薄封装。踩坑概率比从零实现的库低不少。当然这个库的定位也很明确它是给“业务侧调用”用的不是通用网络框架。它没有异步回调没有连接池请求之间完全不共享连接。遇到高并发、长连接复用这些场景它解决不了你得另想方案。这个我放到最后一部分详细说先看怎么让它跑起来。2. 从源码到跑通第一个GET请求2.1 依赖与版本restclient-cpp最早是mrtazz在GitHub上开源的代码量不大核心就restclient.h和restclient.cpp两个文件再加一个jsoncpp的封装头文件。它唯一的硬依赖是libcurl如果你要解析JSON响应可以配合jsoncpp用不强求——很多场景里响应体直接用字符串处理就够了。源码里我看到的核心接口大概是这样的基于0.5.x版本命名空间RestClient返回结构体RestClient::Response包含code、body、headers请求函数get、post、put、del、head、patch全局配置函数init、disableExpectContinue、setTimeout注意一下删除请求的函数名是del不是delete。因为delete是C关键字命名上只能避开。2.2 集成方式我试过两种集成方式各有适用场景。第一种vcpkg装。如果你项目已经在用vcpkg管理依赖直接一条命令vcpkg install restclient-cpp然后在CMake里链接就行。优点是不用管源码缺点是版本可能不是最新的但因为这个库本身更新不频繁老版本反而稳定。第二种源码直接拷进工程。因为整个库就两个核心文件我更喜欢这种方式方便改源码。比如后面要说到的重定向跟随、连接复用问题都需要在源码里加几行拷进工程后改起来毫无心理负担。目录结构大概这样third_party/restclient-cpp/ include/restclient.h src/restclient.cppCMake里只需要找到libcurlcmake_minimum_required(VERSION 3.10) project(http_demo) find_package(CURL REQUIRED) add_executable(demo main.cpp third_party/restclient-cpp/src/restclient.cpp) target_include_directories(demo PRIVATE third_party/restclient-cpp/include) target_link_libraries(demo PRIVATE ${CURL_LIBRARIES})命令行编译更简单确认系统装了libcurl开发库之后g main.cpp restclient.cpp -lcurl -o demo2.3 第一个请求安装好了直接写个demo#include iostream #include restclient.h int main() { // 我一般用 httpbin.org 这个服务做测试你那边如果网络慢换成内网接口也一样 RestClient::Response r RestClient::get(https://httpbin.org/get); if (r.code 200) { std::cout 请求成功 std::endl; std::cout r.body std::endl; // httpbin 会返回请求方的HTTP信息JSON } else { std::cerr HTTP错误码: r.code std::endl; } return 0; }编译运行终端会输出一堆JSON。第一次跑通的时候确实有点感动原来在C里发HTTP请求也可以像Python的requests一样只写一行。这个Response结构体的三个字段在实际调试中非常有用。r.code是返回的HTTP状态码r.body是响应体r.headers是一个std::mapstd::string, std::string装着所有响应头。排查问题的时候先看code再看headers最后看body基本能定位80%的问题。3. 核心API背后Response结构体和五类请求封装3.1 Response结构体为什么这么设计看源码你会发现restclient-cpp内部用libcurl的时候做了这几件事设置URL、设置请求方法、设置headers、设置write回调收集响应体、设置header回调收集响应头、执行请求、清理handle。所有这些细节都收敛到RestClient::Response里。Response的定义大致是这个样子typedef struct { int code; std::string body; HeaderFields headers; } Response;其中HeaderFields是std::mapstd::string, std::string。为什么headers要用map而不是list因为99%的业务场景里你是“按名字取某个响应头”比如Content-Type、Set-Cookie。用map的话一行就能取到。代价是如果响应里出现多个同名header比如多个Set-Cookie只会保留最后一个。我遇到过一次服务器连发两个Set-Cookie结果第一个丢失坑了一小会儿。后来看库里源码发现它就是一个一个塞进map后面的覆盖前面的知道了这个行为之后遇到多cookie场景就自己改用curl了。3.2 请求函数的完整签名这些函数签名我在项目里几乎天天用整理成一张表函数形式说明GETRestClient::get(url)/RestClient::get(url, headers)获取资源POSTRestClient::post(url, content_type, data)/ headers提交数据content_type单独一个参数PUTRestClient::put(url, content_type, data)/ headers整体更新资源DELETERestClient::del(url)/RestClient::del(url, headers)删除HEADRestClient::head(url)/ headers只拿头PATCHRestClient::patch(url, content_type, data)/ headers局部更新可以看到POST/PUT/PATCH都要求单独传一个content_type参数。这个设计不是多余的REST接口里媒体类型决定服务端怎么解析body是表单、JSON还是纯文本。你把它单独拎出来调用者一眼就知道当前请求是什么格式而不是埋在headers地图里翻。3.3 三个全局配置函数restclient-cpp有三个看起来不起眼、但关键时刻能救命的全局函数RestClient::init()初始化libcurl的全局状态建议程序启动时调用一次。RestClient::disableExpectContinue()关闭curl的Expect: 100-continue机制。这个我第五章细说是个高频坑。RestClient::setTimeout(seconds)设置请求总超时时间单位秒。这三个函数修改的都是库内部的全局变量所以一定要在发起第一次请求之前调用并且不要在多个线程里同时调。源码实现里超时设置最终会映射到curl的CURLOPT_TIMEOUT这个超时是整个请求的完成时间包括连接、发送、等待响应、接收body。如果你在弱网环境里调外部接口务必设置超时不然后果很酸爽。3.4 请求之间无状态继续唠叨一个容易被忽略的实现细节restclient-cpp每次发请求都是新建一个curl handle请求结束就清理不会跨请求复用TCP连接。这意味着每个请求都是独立的TCP连接不会有HTTP keep-alive连接复用。好处是线程安全压力小不同线程各调各的只要不碰全局配置函数基本不会打架。我实测过开8个线程并发调内部接口没有出现crashes。坏处就是如果你在循环里连发上千个小请求每次都新建和关闭TCP连接性能会明显差于连接复用方案。遇到这种场景要么自己管理一个curl共享句柄要么换个库。这个选型问题我在第六章展开。4. 实战一次带鉴权、JSON与超时的完整请求接下来用一个接近真实业务的例子把前面说的API串起来。假设我在调公司内部一个用户服务需要带Bearer Token请求体是JSON并且要求5秒内完成。#include iostream #include restclient.h #include json/json.h int main() { RestClient::init(); RestClient::setTimeout(5); // 组装请求头 RestClient::HeaderFields headers; headers[Authorization] Bearer your_token_here; headers[Accept] application/json; // 组装JSON请求体 Json::Value payload; payload[username] alice; payload[age] 30; Json::StreamWriterBuilder builder; std::string body Json::writeString(builder, payload); // 发起POST请求 RestClient::Response r RestClient::post( https://your-service.com/api/user, application/json, body, headers ); // 先看状态码 if (r.code ! 200 r.code ! 201) { std::cerr 请求失败: r.code std::endl; std::cerr r.body std::endl; return 1; } // 解析响应 Json::CharReaderBuilder readerBuilder; Json::Value resp; std::string errs; std::istringstream iss(r.body); if (Json::parseFromStream(readerBuilder, iss, resp, errs)) { std::cout 服务端返回: resp[message].asString() std::endl; std::cout 用户ID: resp[userId].asString() std::endl; } else { std::cerr JSON解析失败: errs std::endl; } return 0; }这里有几个细节值得说道。第一RestClient::setTimeout(5)一定要在发请求前调用。它有全局生效的特点调用一次之后所有请求都受这个超时约束。如果是程序的不同模块各自调接口有些请求需要长超时有些需要短超时就不太方便了——要么改源码支持“每请求超时”要么用多个进程处理。我一般是在程序启动时设一个全局兜底真正的业务里再判断一下要不要自定义。第二content_type和Authorization是分开设置的这是好事。JSON接口必须把Content-Type: application/json传给服务端否则很多框架直接返回415或解析不了body。而鉴权头是放headers里的不需要混在一起。第三响应打印中文乱码的问题。restclient-cpp拿到的是原始字节流不帮你做字符集转换。如果服务端返回UTF-8的JSON而你在Windows控制台直接std::cout r.body大概率看到乱码因为Windows控制台的代码页可能是GBK。解决方式不是改库而是看代码页设置或用支持UTF-8的工具。这个坑不算restclient-cpp的但初用者很容易误以为库编解码有问题。第四再看一个细节很多REST接口要求URL里的query参数做URL编码比如用户名里带了、、中文。restclient-cpp没有直接暴露URL编码函数我建议需要的时候自己调用curl的curl_easy_escape或者简单封装一层。千万不要直接把用户输入拼进URL尤其是参数里可能含有特殊字符时服务端会因为非法URL直接回400。5. 高频踩坑与排查链路这部分是我想重点分享的。restclient-cpp用起来简单但它藏着的libcurl行为很多不熟悉的人会踩一遍。我按“现象→根因→排查→解决”的方式列出来。5.1 POST之后服务端迟迟不返回Expect: 100-continue现象调用一个POST接口请求发出去之后服务端要等1秒甚至更久才返回。抓包发现请求头里带了一个Expect: 100-continue。根因libcurl在POST body超过一定大小我记得是1KB以上时默认会带上Expect: 100-continue头意思是“我先发header等服务器确认接收我再发body”。这是HTTP协议里的一个规范行为但很多内部服务端没处理这个头就会一直等到超时或者返回非预期状态。排查方式在命令行里用curl复现同样的POSTcurl -v -X POST https://your-service.com/api/user \ -H Content-Type: application/json \ -d {username:alice,age:30}观察输出里有没有Expect: 100-continue和随后的等待。解决在程序启动时调用一次RestClient::disableExpectContinue()。我看过restclient-cpp源码这个函数就是设置一个全局标志后面每个请求都不会再添加Expect头。调用之后明显感觉到POST接口响应快了很多。5.2 HTTP 301/302没有自动跟随现象请求一个接口返回码是301或302但r.body是空的业务以为服务端没响应。根因libcurl默认不自动跟随重定向。restclient-cpp封装的时候也没有额外开启CURLOPT_FOLLOWLOCATION。所以服务器返回一个重定向地址库里只是把302原样返回给你。排查方式看r.headers里的Location字段确认服务端确实给了跳转地址。解决两个思路。一是改源码在restclient.cpp里找到设置CURLOPT_FOLLOWLOCATION的位置加一行代码就行。二是手动处理重定向拿到Location后再发一次请求。如果只是临时调接口第二种方法更快不用重新编译。如果项目里大量接口都有重定向我建议直接改源码一劳永逸。5.3 大响应体内存暴涨现象接口返回一个几十MB的文件或数据程序内存占用量直接飙高甚至OOM。根因restclient-cpp设计上是把整个响应体存在std::string里一次性返回。它内部用write回调往string里append所以响应体全部在内存里。下载文件场景是最不适合用它的。排查方式看接口的Content-Length如果比你的合理内存预算大就不该用这个库。解决下载文件或大流量的场景直接用libcurl自己写用CURLOPT_WRITEFUNCTION配合文件指针边收边写内存占用会低很多。restclient-cpp适合的是JSON这种几百KB以内的结构化数据。5.4 多线程启动时偶发崩溃现象程序启动时多个线程同时第一次发请求偶尔出现崩溃。根因libcurl的curl_global_init不是线程安全的restclient-cpp的RestClient::init()会执行全局初始化。如果多个线程同时触发首次请求内部可能还没完成初始化就开始并发使用。排查方式看堆栈几乎都在curl初始化阶段。解决程序启动时在创建线程之前串行调用一次RestClient::init()确保全局状态只初始化一次。这是最简单也最稳妥的做法不要依赖库里“按需初始化”的逻辑。5.5 URL里有中文或特殊字符导致400现象请求一个带query参数的接口参数值里有中文或者符号服务端回400日志里URL是乱码。根因URL语法要求非ASCII字符必须做百分号编码。restclient-cpp不会自动帮你编码query参数只是原样拼接发送。排查方式把程序里的URL打印出来看是不是有中文用浏览器或curl直接访问带编码后的URL能通访问原样URL就报错基本确认。解决写一个url_encode工具函数用curl的curl_easy_escape把参数编码后再拼URL。比如std::string url_encode(const std::string str) { CURL* curl curl_easy_init(); char* encoded curl_easy_escape(curl, str.c_str(), (int)str.size()); std::string result(encoded); curl_free(encoded); curl_easy_cleanup(curl); return result; }然后在拼URL的时候对每个参数值单独编码。这里要提醒一点query参数对和是有特殊含义的如果整个值编码再拼会出错必须“只对值编码”。这些坑单个看都不大但连在一起踩能把一个下午耗光。总结下来就一句话restclient-cpp是一个薄封装它隐藏了libcurl的使用复杂度但没有隐藏libcurl的行为细节。用的时候脑子里要始终有一根弦底层还是curl。6. 技术选型边界什么时候别硬用它我把话说直白一点restclient-cpp有清晰的适用边界越界使用写代码的时候会很憋屈。第一个边界异步非阻塞场景。restclient-cpp是同步阻塞封装发一个请求线程就在那儿等着。如果你的程序要同时发起几十个请求每个请求耗时几百毫秒用同步方案就得开几十个线程调度和内存压力都不小。这时候直接上libcurl的多线程或者异步方案比如curl_multi接口更合适。restclient-cpp帮不了你。第二个边界连接池和连接复用。前面说过每次请求都是独立TCP连接不能复用。对内部微服务之间高频调用短连接开销会体现在延迟和CPU上。想要连接池要么自己维护共享curl handle要么换cpp-httplib或纯curl封装。我自己的经验是在请求量达到每秒几百次的时候这种每个请求新建TCP的方式的弊端会很明显。第三个边界HTTP/2和高级协议特性。restclient-cpp依赖libcurl的客户端能力但封装里没有暴露比较精细的协议选项。如果你的服务已经升级到HTTP/2并且依赖多路复用这个库是没有相关封装的。类似的需求可以考虑Boost.Beast或nghttp2。不过这并不意味着这个库不好。恰恰相反在它适合的位置上它极其好用内部API调用、脚本工具、自动化测试框架、小型服务客户端这些场景的数据量不大、并发不高、同步调用完全够用用restclient-cpp能把代码写得非常干净。这年头能少写一行是一行。最后分享一个小技巧如果你决定长期使用这个库建议你直接fork一份源码到自己的仓库里。原因有两个一是这个库本身更新慢fork一份不受上游变动影响二是你可以根据自己的业务需求在里面加自定义header回调、加连接复用逻辑、加密超时配置把这些改造控制在几百行以内。我自己的项目里就是fork了一份把重定向跟随和请求级超时都加上了用到现在一直很顺手。本文还有配套的精品资源点击获取
返回列表