本章导读
HTTP 的正文是"内容"与"内存"的战场:2 GB 上传、流式下载、按需生成的响应——正文大小不应等于内存占用是全部设计的出发点。xhttpbody 统一五种正文来源:固定族(Empty/Copy/Borrow/Take/Reference——可重放,已知长度)、有界生产流(HttpBodyStreamCreate——任务线程生产、预算受限、SSE 的底座)、文件正文(http_body_file——异步文件 IO 的正文化)、自定义来源(生产者回调)与变换正文。消费侧 xhttpbodyreader 单消费者流式读取,xhttpbodychunk 是独立数据租约(ChunkRelease 归还、可晚于 Reader 释放——TCP 引用发送的完美搭档)。第 104 章服务端的 Body 回调、第 107 章 SSE 的生产端,在本章拿到完整的地基叙述。
108.1引入
三个正文场景的内存形态要求。上传 2 GB:服务端不能攒完再处理——第 104 章的流式 Body 回调已解决接收侧;但客户端发送侧呢——把 2 GB 读进内存再 POST?文件正文让"边读边发"。下载流式生成:响应长度未知、内容按需生产(数据库游标、实时转码)——生产流正文让"边生成边发",预算封顶。重放需求:第 100 章 307/308 重定向要求正文可重放——固定族天然可重放,生产流呢?HttpBodyStreamCreate 产出的 Body 消费一次——重定向要重发的场景用固定族或文件正文。
租约是本章最重要的新概念:Next 吐出的 Chunk 拥有独立数据租约——数据不复制,但你必须 ChunkRelease 归还;Chunk 可以活得比 Reader 久(引用发送的队列里排着,正文对象已销毁——引用计数管理)。这与第 96 章 WebSocket 引用发送(写完才释放)是同一思想在正文层的落点。
108.2概念
五来源与可重放语义
| 来源 | 创建入口 | 可重放 | 典型场景 |
|---|---|---|---|
| 固定族 Empty/Copy/Borrow/Take/Reference | xrtHttpBodyCopy 等 | 是(已知长度=输入字节) | JSON/短正文/307 重发 |
| 有界生产流 | xrtHttpBodyStreamCreate(配置, &流) | 否(一次消费) | SSE/流式生成/进度输出 |
| 文件正文 | http_body_file 族 | 是(可重开) | 大文件上传/静态下载 |
| 自定义来源 | 生产者回调 | 取决实现 | 数据库游标/加密管道 |
| 变换正文 | 组合层 | 取决实现 | 压缩响应 |
固定族细节:Copy 把描述符与副本放一个按实际长度的紧凑分配(不为同一份正文建第二个分配);Borrow 不延长外部内存生命周期(发送期间原缓冲必须活着);Reference 必须提供释放过程(只借用就用 Borrow)。xrtHttpBodyView 借固定正文的连续字节——不保证连续的来源返回 false(文件/流式/变换可能不连续)。
Reader:单消费者与租约
别名纪律(契约原文级):输出的 Chunk 描述符/缓冲/长度槽不得覆盖 Reader、Body 或固定正文的底层字节——别名错误不推进 Reader、不清空输出长度,固定 Body 在重放与并发打开时始终保持不可变。这套防御让"正文对象被多个 Call 共享"(第 99 章冻结语义的正文引用)安全。
有界生产流:预算与收尾
xhttpbodystreamconfig 的 MaxBytes/MaxChunks 是生产端预算(预留+排队+活动租约全覆盖)——SSE 的 AGAIN 阈值即来自这里。收尾三路:最后一个流引用销毁=排队内容后正常 EOF;Close 幂等预关输入;Fail 丢弃未交付内容并把稳定 Cause 传给消费侧。Write/WriteRef/WriteTake 三写入形态对应复制/引用/接管——与第 68 章发送五档同族。
文件正文:异步 IO 的正文化
http_body_file 族把第 72 章的完成端口文件 IO 包成正文档:xrtHttpBodyFileConfigInit + 文件正文创建——读取走异步文件路径(Engine 的 Worker 驱动),不阻塞、不整读;xrtHttpBodyFileFuture/FileRangeFuture 提供"等正文就绪"的 Future 形态。Range 变体天然支撑断点上传/下载。文件正文可重放(重开文件重读)——大文件 + 重定向重发的安全组合。
上传与下载的完整形态
- 上传(客户端):文件正文 + 第 99 章构建器
SetBody——2 GB 文件上传的客户端内存 ≈ 一个读块;服务端第 104 章流式 Body 回调接收。 - 下载(客户端):第 99 章
ResponseBodyLimit管预算、流式消费(Body 回调或 Reader)——内存与下载量解耦;第 93 章解码器串联在消费链上。 - 生成响应(服务端):生产流正文 +
Respond——数据库游标逐步产出;预算封顶后 AGAIN 让生产者等待消费侧。TLS 服务(第 86 章)上跑这一整套时,请求头解析走的就是第 90 章解析器的 TLS 绑定(http1_tls)——跨记录不分配连接级缓冲,流式纪律一路贯通。
108.3示例
第一个完整程序:生产流写出与读回
下面的程序来自 examples/http/body_stream——有界生产流的最小闭环:
#include <stdio.h> #include <xhttp.h> /* 读取完整异步正文;AGAIN 只通过公开 Future 契约继续。 */ static bool exampleReadBody(xhttpbody* pBody) { xhttpbodyreader* pReader = xrtHttpBodyOpen(pBody); xhttpbodychunk Chunk; xhttpbodystatus Status; if ( pReader == NULL ) { return false; } for ( ;; ) { Status = xrtHttpBodyNext(pReader, 4, &Chunk); if ( Status == XHTTP_BODY_DATA ) { (void)fwrite(Chunk.Data, 1, Chunk.Size, stdout); xrtHttpBodyChunkRelease(&Chunk); continue; } if ( Status == XHTTP_BODY_AGAIN ) { xfuture* pReady = xrtHttpBodyReaderWait(pReader); if ( (pReady == NULL) || (xrtFutureWait(pReady) != XWAIT_OK) || (xrtFutureState(pReady) != XFUTURE_RESOLVED) ) { xrtFutureDestroy(pReady); xrtHttpBodyReaderDestroy(pReader); return false; } xrtFutureDestroy(pReady); continue; } break; } xrtHttpBodyReaderDestroy(pReader); return Status == XHTTP_BODY_EOF; } /* 创建有界生产流,写入两段数据并由最后一个生产端发布 EOF。 */ int main(void) { xhttpbodystreamconfig Config; xhttpbodystream* pStream = NULL; xhttpbody* pBody; bool bResult; xrtHttpBodyStreamConfigInit(&Config); Config.MaxBytes = 1024; Config.MaxChunks = 16; pBody = xrtHttpBodyStreamCreate(&Config, &pStream); if ( (pBody == NULL) || (xrtHttpBodyStreamWrite( pStream, XRT_BYTES_LITERAL("first chunk\n") ) != XHTTP_BODY_STREAM_OK) || (xrtHttpBodyStreamWrite( pStream, XRT_BYTES_LITERAL("second chunk\n") ) != XHTTP_BODY_STREAM_OK) ) { xrtHttpBodyStreamDestroy(pStream); xrtHttpBodyDestroy(pBody); return 1; } xrtHttpBodyStreamDestroy(pStream); bResult = exampleReadBody(pBody); xrtHttpBodyDestroy(pBody); return bResult ? 0 : 1; }
刚才发生了什么。 ① Config.MaxBytes=1024/MaxChunks=16 定预算 → HttpBodyStreamCreate 一call得双对象:xhttpbody(交给请求/响应)与 xhttpbodystream(生产端持有)。② 两段 Write 后销毁流——最后引用销毁=正常 EOF:排队内容先交付、EOF 随后。③ 读回循环(与第 107 章客户端同款骨架):Next(8, &Chunk) 小上限分段、DATA 打印+ChunkRelease、AGAIN 走 xrtHttpBodyReaderWait 的 Future 等待、EOF 收尾。④ 两行输出即"生产端写入的字节流完整到达消费端"——正文对象是生产与消费的解耦点:两端可在不同线程、不同时刻。真实部署里生产端是任务线程(SSE 事件/游标产出),消费端是 HTTP 传输。
第二个完整程序:文件正文上传
第二个程序来自 examples/http/body_file——大文件的上传形态:
#include <stdio.h> #include <xhttp.h> /* 等待 Future 成功并返回借用值。 */ static ptr exampleFutureValue(xfuture* pFuture) { if ( (pFuture == NULL) || (xrtFutureWait(pFuture) != XWAIT_OK) || (xrtFutureState(pFuture) != XFUTURE_RESOLVED) ) { return NULL; } return xrtFutureValue(pFuture); } /* 使用统一正文 Reader 异步读取文件内容。 */ int main(void) { static const char sPath[] = "xrt-http-body-file-example.tmp"; xtaskpoolconfig Config = { 1, 16, 0 }; xtaskpool* pPool; xfile File; xfuture* pPrepare; xhttpbody* pBody; xhttpbodyreader* pReader; xhttpbodychunk Chunk; xhttpbodystatus Status; (void)xrtFileDelete(sPath); xrtClearError(); File = xrtOpen( sPath, XFILE_WRITE | XFILE_CREATE | XFILE_EXCLUSIVE ); if ( (File == NULL) || !xrtWriteFull(File, "HTTP file body\n", 15, NULL) || !xrtClose(File) ) { return 1; } pPool = xrtTaskPoolCreate(&Config); if ( pPool == NULL ) { (void)xrtFileDelete(sPath); return 1; } /* NULL 配置按需读取,默认单次申请不超过 64 KiB。 */ pPrepare = xrtHttpBodyFileFuture(pPool, sPath, NULL); pBody = (xhttpbody*)exampleFutureValue(pPrepare); pReader = pBody != NULL ? xrtHttpBodyOpen(pBody) : NULL; while ( pReader != NULL ) { Status = xrtHttpBodyNext(pReader, 4096, &Chunk); if ( Status == XHTTP_BODY_AGAIN ) { xfuture* pReady = xrtHttpBodyReaderWait(pReader); if ( exampleFutureValue(pReady) == NULL ) { xrtFutureDestroy(pReady); break; } xrtFutureDestroy(pReady); continue; } if ( Status == XHTTP_BODY_EOF ) { break; } if ( Status == XHTTP_BODY_ERROR ) { break; } (void)fwrite(Chunk.Data, 1, Chunk.Size, stdout); xrtHttpBodyChunkRelease(&Chunk); } xrtHttpBodyReaderDestroy(pReader); xrtFutureDestroy(pPrepare); (void)xrtTaskPoolDestroy(pPool); (void)xrtFileDelete(sPath); return 0; }
刚才发生了什么。 ① xrtHttpBodyFileConfigInit 配置路径与选项 → 创建文件正文——长度来自文件元数据、内容按需异步读:客户端内存与文件大小无关(对比 Copy 整读——2 GB 就是 2 GB)。② 文件正文可重放(重新打开重读)——第 100 章 307/308 重定向的重发要求在这里天然满足;固定流式生产则不行(一次消费)。③ xrtHttpBodyFileFuture 的 Future 形态让"等文件就绪/读完"进入第 58 章的等待体系。④ 配套 examples/http/body(固定族五来源)、body_compose(组合正文)、body_inflate/deflate(变换正文——与第 93 章解码对应的发送侧压缩)、server_body_async(服务端异步正文响应)——正文族的完整巡检面。
108.4契约
- 五来源:固定族(可重放·已知长度)、生产流(一次消费·有界)、文件(可重放·异步读)、自定义、变换——
xhttpbody统一形态。 - 固定族纪律:Copy 单分配(描述符+副本紧凑块);Borrow 不延长外部生命;Reference 必须有释放过程;View 只对保证连续的来源成功。
- 生产流:MaxBytes/MaxChunks 预算覆盖预留+排队+租约;EOF/Close/Fail 三收尾;Write 三形态(复制/引用/接管)。
- Reader:单消费者;DATA(租约)/AGAIN(Wait Future)/EOF 三态;Chunk 独立租约(ChunkRelease;可晚于 Reader/Body 释放——引用发送友好)。
- 别名防御:输出不得覆盖正文底层字节;别名错误不推进不破坏——固定 Body 重放与并发打开恒不可变。
- 文件正文:异步读不阻塞不整读;可重放;Range 变体;Future 就绪形态。
- 重放矩阵:307/308 重发→固定族/文件;一次生成→生产流。
- 预算归属:客户端 ResponseBodyLimit(第 99 章)限表示正文;生产流预算限生产端——两层各自有界。
- 裁剪:
HTTP_BODY/HTTP_BODY_STREAM/HTTP_BODY_FILE独立宏。
常见错误
症状:偶发发送错乱内容——栈上的缓冲、或发送前就 free 的堆缓冲配了 Borrow。
原因:Borrow 的契约是"不延长外部内存生命周期"——正文只记指针,发送(可能异步、可能重发)期间原数据必须原样存活。不确定就 Copy 或 Reference。
uint8 Buf[256]; fill(Buf); xrtHttpRequestSetBody(pReq, xrtHttpBodyBorrow(view(Buf))); /* 栈缓冲 */ send_async(pReq); /* 函数返回后栈帧消亡:发送读到垃圾 */
/* 要么 Copy(小正文),要么 Reference+释放回调(自有堆缓冲) */ xrtHttpRequestSetBody(pReq, xrtHttpBodyCopy(view(Buf))); /* 或 xrtHttpBodyReference(view(HeapBuf), release_cb, ctx)——发送完自动释放 */
症状:流式响应丢段——AGAIN 后生产者跳过这条继续下一条("免得阻塞")。
原因:AGAIN 是背压不是错误——没有保留任何输入,你手里的这条数据就是唯一副本;跳过=丢数据。正确动作:等 WaitWritable 再发同一条。
for ( each_item ) { if ( send(stream, item) == AGAIN ) { continue; /* 丢弃——客户端缺这段 */ } }
for ( each_item ) { while ( send(stream, item) == AGAIN ) { wait_writable(stream); /* 第 107 章坑 2 的等待模板 */ } }
症状:分配统计里租约只增不减——预算耗尽快全部 AGAIN。
原因:Chunk 是租约不是视图——占着生产端预算;Release 是归还动作。要长期持有就复制(引用发送除外——那正是租约的设计场景:发送完成时释放)。
while ( next(&Reader, &Chunk) == DATA ) { queue_push(g_Q, Chunk); /* 租约不还:预算耗尽 */ }
while ( next(&Reader, &Chunk) == DATA ) { if ( want_keep(Chunk) ) { queue_push(g_Q, copy_chunk(Chunk)); /* 留副本 */ } xrtHttpBodyChunkRelease(&Chunk); /* 还租约 */ }
编程练习
同一份文本分别用 Copy/Borrow/Take/Reference/生产流创建正文并发送(对本地服务),对比分配统计与可重放性(读两次)。验收标准:分配次数与契约一致;固定族二次读取成功、生产流第二次打开失败(或按契约的行为)。
客户端文件正文 POST 2 GB 文件(生成临时文件)→ 服务端流式 Body 回调接收写盘 → 比对哈希。全程记录两端内存峰值。验收标准:两端峰值均与文件大小无关(MB 级);哈希一致;中途限速(模拟慢盘)管线稳定。
自定义生产者正文:读文件块 → AES-GCM 加密块(第 75 章)→ 编码进正文——形成"加密文件下载"服务。客户端用第 93 章解码思路反向解密消费。验收标准:往返原文一致;密钥只在两侧内存(SecureZero 收尾——第 77 章纪律);内存峰值恒定。
小结
| 知识点 | 速查 |
|---|---|
| 五来源 | 固定族(可重放)/生产流(一次)/文件(异步可重放)/自定义/变换 |
| 固定族 | Copy 单分配;Borrow 不延长生命;Reference 带释放;View 仅连续来源 |
| 生产流 | 双对象(Body+Stream);MaxBytes/MaxChunks 预算;EOF/Close/Fail 三收尾 |
| Reader | 单消费者;DATA(租约)/AGAIN(Wait)/EOF;ChunkRelease 归还 |
| 租约 | Chunk 独立数据租约、可晚于 Reader 释放——引用发送的设计场景 |
| 别名防御 | 输出不覆盖正文底层;固定 Body 重放/并发打开恒不可变 |
| 文件正文 | 异步读不整读;可重放;Range 变体;Future 就绪 |
| 重放矩阵 | 重定向重发→固定/文件;一次生成→生产流 |
| 预算两层 | 客户端 ResponseBodyLimit(表示)+ 生产流预算(生产端) |
| 裁剪 | BODY/BODY_STREAM/BODY_FILE 独立宏 |
📖 本章涉及的 API 参考手册:xhttp-http_body(20 个条目)xhttp-http_body_stream(12 个条目)xhttp-http_body_file(4 个条目)
点击查看完整签名、参数约束、返回值与错误说明