- 后端
- 网络
【免费下载链接】cpp-httplib
A C++ header-only HTTP/HTTPS server and client library
导读
在对接 S3 兼容存储 API、图片上传接口等场景时,往往需要把整个文件内容原样作为 HTTP 请求体发送,而不是包装成 multipart 表单。cpp-httplib 提供了httplib::make_file_body(),它把文件路径解析为「文件大小 +ContentProvider」的组合,让你通过Post()/Put()直接以Content-Length方式流式上传大文件。读完本文,你将掌握该 API 的调用方式、底层读取机制、失败处理要点及其适用边界,并能结合仓库源码理解它为何可以做到大文件不占用内存。
为什么需要「原始二进制」请求体
普通文本或小文件可以用cli.Post(path, body, content_type)直接把std::string作为请求体发送。但有两类需求不适合这种方式:
- 请求体必须精确等于文件字节内容,不能有多余的分隔符、boundary 或字段头——例如 S3 兼容 API 的
PUT /bucket/key上传,以及接收原始图片数据的端点; - 文件很大,把整个文件读进
std::string会带来显著内存开销。
multipart 上传(见 C07. Upload a file as multipart form data)适用于同时携带多个字段或文件;而本场景只需要「文件内容直通请求体」。make_file_body()正是为此设计。
基本用法
#include <httplib.h> #include <iostream> int main() { httplib::Client cli("https://storage.example.com"); // 返回 (文件大小, ContentProvider) 的组合 auto [size, provider] = httplib::make_file_body("backup.tar.gz"); if (size == 0) { std::cerr << "Failed to open file" << std::endl; return 1; } auto res = cli.Put("/bucket/backup.tar.gz", size, provider, "application/gzip"); if (res && res->status == 200) { std::cout << "Upload succeeded" << std::endl; } else { std::cerr << "Upload failed: " << httplib::to_string(res.error()) << std::endl; } return 0; }要点说明:
make_file_body()接受文件路径(std::string),返回std::pair<size_t, ContentProvider>:first是文件字节大小,second是内容提供器。- 将
size、provider直接传给Post()或Put(),文件内容便作为请求体原样发送。 - 第三个参数是
Content-Type,例如"application/gzip"、"application/octet-stream"、"image/jpeg",按目标端点要求填写即可。
这与仓库 README.md 中的示例用法一致:
auto [size, provider] = httplib::make_file_body("/path/to/data.bin"); auto res = cli.Post("/upload", size, provider, "application/octet-stream");从源码结构看,客户端为此提供了若干配套重载,例如(见 httplib.h):
Result Post(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr); Result Put(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr);即「显式content_length+ContentProvider+Content-Type(可选进度回调)」这一族 API,与make_file_body()的返回类型精确对接。
ContentProvider 是如何做到流式读取的
ContentProvider在 httplib.h 中定义为一个函数对象:
using ContentProvider = std::function<bool(size_t offset, size_t length, DataSink &sink)>;它接收offset(本次要发送的起始偏移)和length(本次要发送的字节数),通过sink.write(buf, n)把数据交出去,返回bool表示是否成功。DataSink(见 httplib.h)封装了write、done、is_writable等回调,并提供一个std::ostream os便于以流方式输出。
make_file_body()的实现(见 httplib.h)分两步:
- 探测文件大小:以
std::ios::binary | std::ios::ate打开文件,用tellg()取得大小并返回;打开失败则返回{0, ContentProvider{}}。 - 构造分块读取的 provider:真正的上传阶段,provider 每次最多读取8192 字节(
char buf[8192]),并把读到的内容立即交给sink.write():
ContentProvider provider = filepath -> bool { std::ifstream f(filepath, std::ios::binary); if (!f) { return false; } f.seekg(static_cast<std::streamoff>(offset)); if (!f.good()) { return false; } char buf[8192]; while (length > 0) { auto to_read = (std::min)(sizeof(buf), length); f.read(buf, static_cast<std::streamsize>(to_read)); auto n = static_cast<size_t>(f.gcount()); if (n == 0) { return false; } if (!sink.write(buf, n)) { return false; } length -= n; } return true; };因此无论文件多大,同一时刻内存中最多只驻留一个 8 KiB 的缓冲区,大文件上传也不会把整个文件载入内存——这正是本 API 相对「整体读入字符串再发送」的核心优势。
文件打不开时会发生什么
make_file_body()打开文件失败时,返回的size为0,provider为空函数对象(ContentProvider{})。
如果忽略检查而直接发送,请求体会是垃圾数据,必须始终先检查size:
auto [size, provider] = httplib::make_file_body("backup.tar.gz"); if (size == 0) { // 处理文件不存在 / 无权限等情况,而不是继续发送 }这一点在测试中有直接佐证。test/test.cc 的MakeFileBodyTest.Basic先写一个 4096 字节的临时文件,再make_file_body()后断言fb.first > 0,随后用cli.Post("/upload", fb.first, fb.second, "application/octet-stream")上传,服务端收到的req.body与文件内容完全相等,验证了「大小 + provider」组合端到端的正确性。
警告:Content-Length 是预先固定下来的
make_file_body()在返回前就用tellg()读了一次文件大小,这个值随后会作为Content-Length承诺给对端。因此:
- 若上传过程中文件被截短,provider 读到
n == 0(EOF 提前到来)时会返回false,传输以失败告终——实现中的注释也明确指出:调用方已把测量到的大小作为 Content-Length 发出,请求体无法补全时只能像其他错误一样失败(见 httplib.h); - 若文件在测量后被写大,则发送内容仍以测量时的大小为限。
也就是说,如果你无法保证上传期间文件大小不变,这个 API 就不适合。此时应改用不确定长度的分块传输(chunked)方案,或先在本地固定一份快照再上传。
test/test.cc 的MakeFileBodyTest.TruncatedFileMakesTheProviderFail精确复现了这一场景:先测量 100 字节的文件,随后把文件截短为 10 字节,再调用 provider 期望其返回false,且写入 sink 的只有 10 字节。而MakeFileBodyTest.WholeFileIsSent(test/test.cc)用 9000 字节内容(跨多个 8 KiB 读取)验证了 provider 能把整个文件完整送出,两次测试共同构成了该 API 的边界行为契约。
与 multipart 上传如何取舍
如果你需要的是「表单字段 + 文件」的组合,或一次携带多个文件,应该走 multipart 路线:使用httplib::make_file_provider()构造FormDataProviderItems,再调用cli.Post("/upload", {}, {}, providers)(见 README.md),详见 C07. Upload a file as multipart form data。
选择依据可以简单归纳为:
| 需求 | 推荐 API |
|---|---|
| 请求体 = 单个文件原始字节,且有确定的 Content-Length | make_file_body()+Post()/Put() |
| 需要多个字段/文件,或字段与文件混合 | make_file_provider()/FormDataProviderItems+ multipart |
| 文件大小无法预先固定、需要流式分块 | ContentProviderWithoutLength+ chunked 上传(见 C09. Chunked upload) |
小结
httplib::make_file_body(path)返回(size, ContentProvider),配合Post()/Put()即可把文件作为原始二进制请求体发送,Content-Type由你指定。- 底层按 8192 字节分块经
DataSink写出,超大文件也不会整体驻留内存。 - 文件打不开时返回
size == 0,必须显式检查后再发送。 - Content-Length 在调用前就已固定,上传期间文件大小可能变化时请改用其他方案。
- 多文件/字段场景请回到 multipart 路线(C07),分块流式场景可参考 C09。
- 后端
- 网络
【免费下载链接】cpp-httplib
A C++ header-only HTTP/HTTPS server and client library
相关推荐
cpp-httplib 实战:用 make_file_body 将文件作为原始二进制请求体直接上传
cpp httplib 实战:用 make_file_body 将文件作为原始二进制请求体直接上传 本篇指南围绕 cpp httplib 提供的 make_fi
后端网络cpp-httplib 基础客户端实战:用 httplib::Client 发送 GET / POST 与文件上传
cpp httplib 基础客户端实战:用 httplib::Client 发送 GET / POST 与文件上传 cpp httplib 不仅是一个 head
后端网络WinUI TabView v2 增强指南:TabWidthMode Compact 与 CloseButtonOverlayMode 完全解析
WinUI TabView v2 增强指南:TabWidthMode Compact 与 CloseButtonOverlayMode 完全解析 导读 本文以微
后端网络
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考