第 15 章 / 共 51 章
卷三 · 协议开发
响应构造:头、chunked 与 keep-alive
响应是你亲手写出的字节流。本章把 demo 里那套被 routebench 万级路由验证过的响应辅助拆开重装一遍——它是你 modules/http.h 的原型。
本章导读
站位:读完本章你能从零写出文本 / JSON / HTML / chunked 四种响应,并理解 HEAD 与 keep-alive 为什么必须由你(而不是框架)负责。
15.1 引入
"没有 Response 对象"意味着三件事由你完成:生成头字节(xrtHttp1ResponseWrite)、写入传输层(ConnSend 辅助)、维护协议语义(Content-Length 必须准确、HEAD 不发正文)。这看起来繁琐,但换来的是零隐藏行为——而且这些代码写一次就成了你的 modules/http.h,全应用复用。
15.2 完整响应:ReplyRaw 模式
modules/http.h 的核心三件实机验证出处:demo-single/modules/http.h(节选)
/* 1. 传输辅助:按请求实际使用的传输层发送 */
static bool ConnSend(XS_HttpReq* pReq, const void* pData, size_t iSize)
{
size_t iWritten = 0;
if ( pReq->tls != NULL ) {
/* TLS 允许成功短写:只有整段被受理才算完成 */
return xrtTlsStreamSend(pReq->tls, pData, iSize,
&iWritten) == XTLS_OK && iWritten == iSize;
}
return pReq->tcp != NULL &&
xrtNetStreamSend(pReq->tcp, pData, iSize) == XNET_RESULT_OK;
}
/* 2. 完整响应:头 + 正文一次写出 */
static bool ReplyRaw(XS_HttpReq* pReq, uint16 iStatus,
const char* sContentType, const void* pBody, size_t iBodySize)
{
char arrHead[1024], arrLength[32];
xhttpfield arrField[12];
size_t iFieldCount = 0, iHeadSize = 0;
snprintf(arrLength, sizeof(arrLength), "%llu",
(unsigned long long)iBodySize);
arrField[iFieldCount].Name = XRT_STR_LITERAL("Content-Length");
arrField[iFieldCount].Value = xrtStrView(arrLength);
iFieldCount++;
/* ... 可选 Content-Type 与附加头 ... */
if ( !xrtHttp1ResponseWrite(XHTTP_VERSION_1_1, iStatus,
xrtHttpStatusText(iStatus), arrField, iFieldCount,
arrHead, sizeof(arrHead), &iHeadSize) ) return false;
if ( !ConnSend(pReq, arrHead, iHeadSize) ) return false;
return pReq->head->MethodCode == XHTTP_METHOD_HEAD ||
ConnSend(pReq, pBody, iBodySize);
}
/* 3. 语义化包装 */
static bool ReplyText(XS_HttpReq* pReq, uint16 iStatus, const char* sText)
{
return ReplyRaw(pReq, iStatus, "text/plain; charset=utf-8",
sText, strlen(sText));
}
刚才发生了什么——四个关键点:
xhttpfield是 {Name, Value} 视图对——视图必须存活到 ResponseWrite 返回,所以长度字符串放在栈数组里再 xrtStrViewxrtHttp1ResponseWrite只生成头字节(写入调用方缓冲),不发送——生成与传输解耦- Content-Length 由你计算并转成字符串——它是 keep-alive 正确性的锚点
- HEAD 分支:保留头(含 Content-Length)但跳过正文发送
15.3 HEAD 与 keep-alive
- keep-alive:连接默认复用(HTTP/1.1 语义);客户端发
Connection: close时本次响应后关闭。驱动在 XS_OK 后消费掉 body 余量、继续读下一请求——前提是你的 Content-Length 精确,多了少了都会让下一请求的解析错位 - HEAD:语义是"GET 的元数据"——头字段应与 GET 一致(含 Content-Length),但不写正文。ReplyRaw 里一行判断完成
- 受限制 pipeline:静态文件未排空前不解析下一条请求——应用响应遵循同样的"写完再还连接"纪律即可
15.4 chunked 流式响应
长度未知或分块生成的内容用 chunked 编码——每块由 xrtHttp1ChunkWrite 打包,终止块收尾:
chunked 响应骨架
/* 头:Transfer-Encoding: chunked(无 Content-Length)*/
xhttpfield arrField[1] = {
{ XRT_STR_LITERAL("Transfer-Encoding"),
XRT_STR_LITERAL("chunked") } };
xrtHttp1ResponseWrite(XHTTP_VERSION_1_1, 200,
xrtHttpStatusText(200), arrField, 1, arrHead, sizeof(arrHead), &iHeadSize);
ConnSend(pReq, arrHead, iHeadSize);
/* 逐块 */
while ( HasMore(&tGen) ) {
char arrChunk[4096];
size_t iChunkSize = NextChunk(&tGen, arrChunk, sizeof(arrChunk));
char arrFrame[4096 + 32];
size_t iFrameSize = 0;
if ( xrtHttp1ChunkWrite(arrChunk, iChunkSize,
arrFrame, sizeof(arrFrame), &iFrameSize) )
ConnSend(pReq, arrFrame, iFrameSize);
}
/* 终止块(0 长度 chunk)——同样经 ChunkWrite 生成 */
chunked 响应里 HEAD 同样不写正文(连头都不必含 Transfer-Encoding,可退化为空 GET 语义头——按你的策略选择并保持一致)。
15.5 契约
- 长度线:Content-Length 必须精确等于正文字节数——keep-alive 的生命线。
- HEAD 线:头同 GET、正文跳过;在传输函数里统一处理,不要在每个 handler 写一遍。
- 复用线:响应辅助进 modules/http.h,所有权注释齐全(ReplyJSON 只借用 pObject)。
常见错误
Content-Length 与实际不符
- 症状
- keep-alive 下后续请求解析错位、客户端挂起或报畸形响应。
- 原因
- 多算/漏算正文长度(如把 strlen 用于含 \0 的二进制、发送前又改了内容)。
忽略 TLS 短写
- 症状
- HTTPS 下偶发响应截断。
- 原因
- 直接用 xrtTlsStreamSend 的返回值判断成功——TLS 允许成功短写;必须核对 written == size(ConnSend 的写法)。
✗ 错误
xrtTlsStreamSend(pReq->tls, p, n,
&iWritten); /* 没看 iWritten */✓ 正确
== XTLS_OK && iWritten == n编程练习
基础四种 Reply
基于本章模式实现 ReplyText / ReplyHTML / ReplyJSON / ReplyBinary(Content-Disposition 附件头)。
验收:四种响应 curl 均正确解析;二进制做 md5 对比。
进阶HEAD 通道验证
用 curl -I 验证你的路由对 HEAD 的行为;若 405,结合第 17 章方法掩码修复。
验收:HEAD 返回与 GET 一致的头字段(含 Content-Length)且无正文。
挑战无限流(预热)
用 chunked 实现 /counter:从 1 数到 100,每秒一块。为第 22 章的 SSE 式推送打底。
验收:curl 逐步收到 100 块;中途 Ctrl+C 服务端不崩(提示:发送失败路径的清理)。
小结
| 要点 | 结论 |
|---|---|
| 三件套 | ConnSend(传输)/ ReplyRaw(头+体)/ 语义包装 |
| 生成与传输解耦 | ResponseWrite 只产头字节到你的缓冲 |
| Content-Length | 精确计算——keep-alive 生命线;TLS 短写要核对 |
| HEAD | 头同 GET、正文跳过,在传输层统一处理 |
| chunked | ChunkWrite 打包逐块发送,终止块收尾 |
xrt 延伸阅读
- xrt 参考 · http1 — ResponseWrite / ChunkWrite / HttpStatusText 完整签名
- xrt 参考 · http_trailer — chunked trailer 的读写(≤8 字段)