☰
cpp-httplib 进度回调完全指南:DownloadProgress 与 UploadProgress 的实现原理、取消机制与实战用法
2026/10/1 13:24:23 网站建设 项目流程
  • 后端
  • 网络

【免费下载链接】cpp-httplib

A C++ header-only HTTP/HTTPS server and client library

项目地址:https://gitcode.com/GitHub_Trending/cp/cpp-httplib
点击查看免费下载

cpp-httplib 是一个仅头文件(header-only)的 C++ HTTP/HTTPS 客户端与服务端库。当你的应用需要向用户展示"正在下载 xx%""正在上传 xx%"这类实时进度时,无需引入任何第三方 UI 库——httplib::Client内建了DownloadProgress与UploadProgress两种回调,一个(current, total)参数对即可完成下载/上传进度跟踪、百分比计算,甚至实现"取消按钮"式的传输中止。读完本文,你将掌握这两种回调的完整用法、total缺失时的防御写法、ContentReceiver与进度回调的搭配方式,以及如何基于回调返回值安全地取消进行中的传输。

一、回调签名与触发时机:先看类型定义

在 httplib.h 中,两种进度回调的类型定义完全一致:

using DownloadProgress = std::function<bool(size_t current, size_t total)>; using UploadProgress = std::function<bool(size_t current, size_t total)>;
  • 两个参数:current是当前已传输的字节数,total是传输的总字节数;
  • 返回值bool:返回true表示继续传输,返回false表示中止传输;
  • 调用线程:回调在客户端进行网络 I/O 的线程内同步调用,因此不要在回调里执行耗时操作,也不要在回调里直接操作需要线程安全的共享对象(如需跨线程共享标志位,请使用std::atomic,下文"取消传输"一节有示例)。

进度回调的触发时机是"每收到/每发送一批数据就回调一次",而不是"每秒回调一次"。因此对于大文件,回调可能被调用非常多次,回调体应当保持轻量(仅做百分比计算、打印或写入一个原子标志位)。

二、下载进度:接收 Content-Length 与百分比计算

在httplib::Client上调用Get()时传入一个DownloadProgress回调即可:

httplib::Client cli("http://localhost:8080"); auto res = cli.Get("/large-file", [](size_t current, size_t total) { auto percent = (total > 0) ? (current * 100 / total) : 0; std::cout << "\rDownloading: " << percent << "% (" << current << "/" << total << ")" << std::flush; return true; // return false to abort }); std::cout << std::endl;

关键细节:

  1. total来源于Content-Length响应头。如果服务器没有返回Content-Length(例如使用Transfer-Encoding: chunked分块传输),total可能为0。此时无法计算百分比,示例代码用(total > 0) ? ... : 0做了防御,退化为仅显示已接收字节数current。
  2. \r配合std::flush实现单行覆盖式刷新,适合命令行终端;GUI 程序则应在回调里更新进度条控件的值。
  3. 返回false会中止下载,此时调用返回的Result中res.error()为Error::Canceled(详见下文)。

从源码看下载进度回调的调用链

从 httplib.h 的重载声明可以看到,DownloadProgress被接入到Get()的多个重载中,从最简形式到完整形式一应俱全:

Result Get(const std::string &path, DownloadProgress progress = nullptr); Result Get(const std::string &path, ContentReceiver content_receiver, DownloadProgress progress = nullptr); Result Get(const std::string &path, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress = nullptr); Result Get(const std::string &path, const Headers &headers, DownloadProgress progress = nullptr); Result Get(const std::string &path, const Params &params, const Headers &headers, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress = nullptr);

也就是说,进度回调可以和自定义请求头(Headers)、查询参数(Params)、响应头回调(ResponseHandler)、流式接收器(ContentReceiver)任意组合,参数顺序始终是progress位于最后一个位置。

值得注意的是,仓库测试 test/test.cc 中特别注释到:DownloadProgress回调仅对带Content-Length的响应触发——测试用 httpbingo 的/bytes/524288这类固定长度端点来保证"回调在整段 body 收完之前被触发,从而给取消留出机会"。这印证了total的取值来源,也提醒我们:如果你的服务端用 chunked 编码响应,进度回调可能一次都不会被调用,此时应改用ContentReceiver做"已接收字节数"的累计显示。

三、上传进度:Post / Put / Patch 与多重重载

上传与下载对称:把UploadProgress作为Post()或Put()的最后一个参数传入即可:

httplib::Client cli("http://localhost:8080"); std::string body = load_large_body(); auto res = cli.Post("/upload", body, "application/octet-stream", [](size_t current, size_t total) { auto percent = current * 100 / total; std::cout << "\rUploading: " << percent << "%" << std::flush; return true; }); std::cout << std::endl;

与下载不同,上传时total通常总是可靠的——因为客户端在发起请求前就知道 body 的长度(std::string的大小、ContentProvider声明的内容长度或 multipart 表单的长度),它会写入Content-Length请求头。因此上传场景一般不需要total > 0的防御判断(不过从源码结构看,ContentProviderWithoutLength这类"长度未知"的重载存在,遇到未知长度时仍需谨慎处理)。

哪些重载支持 UploadProgress

从 httplib.h 的声明看,UploadProgress出现在以下几种上传形态的末尾:

  • Post(path, body, content_type, UploadProgress)/Put(path, body, content_type, UploadProgress):字符串 body,最常用;
  • Post(path, content_length, ContentProvider, content_type, UploadProgress):自定义内容提供者(流式/文件上传);
  • Post(path, ContentProviderWithoutLength, content_type, UploadProgress):无长度提供者;
  • Post(path, UploadFormDataItems, UploadProgress):multipart 表单上传;
  • 带Headers的对应全部变体,以及Put的同名重载;
  • Patch同样支持进度回调(测试 test/test.cc 中TestStringBodyUploadProgress对Post/Put/Patch三种方法做了统一验证)。

测试用例如何验证上传进度

仓库测试 test/test.cc 中的TestContentProviderUploadProgress给出了一个很有参考价值的验证模式:在回调里把每次的current值收集进std::vector<uint64_t>,传输结束后断言progress_values非空、且请求成功(res->status == 200)。multipart 表单上传也有对应的TestMultipartUploadProgress(test/test.cc),用包含文本字段与文件字段的UploadFormDataItems验证回调被多次触发。如果你要自测自己的上传进度逻辑,可以照此模式在回调中累计current,再对比最终发送的总字节数是否一致。

四、取消传输:回调返回 false 与原子标志位

进度回调的bool返回值承担了"取消"职责:返回false,传输即中止。文档给出的经典 UI 场景是"取消按钮"——按钮事件置位一个标志,下一次进度回调读取到该标志就返回false:

std::atomic<bool> cancelled{false}; auto res = cli.Get("/large-file", & { return !cancelled.load(); });

注意事项:

  • 由于回调在 I/O 线程中同步执行,而"取消按钮"通常在 UI 线程,标志位必须用std::atomic,避免数据竞争(文档示例即是如此)。
  • 取消的粒度是"下一次进度回调":回调只有在收到下一批数据时才会被触发,所以取消不是瞬时的,最多滞后一个数据块(对 Content-Length 响应通常为 8 KB 左右的缓冲块)。
  • 取消后,res不为空,但res->status无效,正确做法是检查res.error()。在 httplib.h 附近的响应读取逻辑中,进度回调返回false时错误被置为Error::Canceled,其对应的人类可读描述为"Connection handling canceled"(httplib.h)。

测试用例对取消行为的验证

仓库测试 test/test.cc 的CancelTest系列精确验证了这一行为:

  • NoCancel_Online:回调恒返回true,请求成功且 body 完整;
  • WithCancelSmallPayload_Online:回调恒返回false,断言!res成立且res.error() == Error::Canceled。

测试注释明确指出,取消测试必须选用/bytes/524288这类大 payload,因为DownloadProgress回调"只对带 Content-Length 的响应触发",payload 太小的话回调在 body 收完前根本不会被调用,取消也就无从谈起。这再次印证了"回调触发频率取决于网络数据到达节奏"这一本质。

五、与 ContentReceiver 搭配:边流式保存边显示进度

进度回调只负责"报数",数据本身不会交付给回调。如果你既要下载进度,又要边下载边把数据写进文件(而不是等整段 body 收完再一次性拿到res->body),就需要同时传入ContentReceiver和DownloadProgress:

httplib::Client cli("http://localhost:8080"); std::ofstream ofs("output.bin", std::ios::binary); if (!ofs) { std::cerr << "Failed to open file" << std::endl; return 1; } auto res = cli.Get("/large-file", & { // ContentReceiver:逐块收数据 ofs.write(data, len); return static_cast<bool>(ofs); // 写盘失败即中止下载 }, [](size_t current, size_t total) { // DownloadProgress:逐块报进度 auto percent = (total > 0) ? (current * 100 / total) : 0; std::cout << "\rDownloading: " << percent << "%" << std::flush; return true; });

从 httplib.h 的类型定义看,ContentReceiver的签名是:

using ContentReceiver = std::function<bool(const char *data, size_t data_length)>;

它把响应 body 切成一块块数据(data/data_length)交给你的回调,返回false同样可以中止下载。两种回调在Get()重载中的位置固定为:content_receiver在前、progress在后。此外还有带ResponseHandler的三回调版本——你可以在响应头到达后、body 开始前先读取Content-Length等信息(参见配套文档 C01. Get the response body / save to a file,其中给出了用ResponseHandler提前打印文件大小的完整示例)。

需要区分的是 httplib.h 中另一个类型ContentReceiverWithProgress——它的签名是bool(const char *data, size_t data_length, size_t offset, size_t total_length),把"数据块、块内偏移、总长度"合并进同一个回调,与"ContentReceiver+DownloadProgress双回调"是两种不同的风格,可按需选用。

六、实战要点小结

场景推荐做法关键注意事项
显示下载百分比cli.Get(path, DownloadProgress)total来自Content-Length,缺失时可能为0,需防御处理
显示上传百分比cli.Post/Put/Patch(..., UploadProgress)客户端自知 body 长度,total通常可靠
大文件边下边写cli.Get(path, ContentReceiver, DownloadProgress)ContentReceiver负责落盘,进度回调只负责展示
UI 取消按钮回调返回!cancelled.load()标志位必须用std::atomic;取消滞后最多一个数据块
判断传输是否被取消检查res.error() == httplib::Error::Canceled不要依赖res->status,此时 status 无效

进度回调的触发频率取决于网络数据块的到达/发送节奏而非时间,因此:回调体内不要做重活;对 chunked 响应不要依赖DownloadProgress(可用ContentReceiver自行累计);取消传输后务必通过Error::Canceled(描述为"Connection handling canceled")判断结果。只要遵循这些原则,cpp-httplib 的进度回调就能为你的下载器、上传器或桌面 GUI 提供稳定、可取消的实时进度能力。

  • 后端
  • 网络

【免费下载链接】cpp-httplib

A C++ header-only HTTP/HTTPS server and client library

项目地址:https://gitcode.com/GitHub_Trending/cp/cpp-httplib
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询