第 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 返回,所以长度字符串放在栈数组里再 xrtStrView
  • xrtHttp1ResponseWrite 只生成头字节(写入调用方缓冲),不发送——生成与传输解耦
  • 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、正文跳过,在传输层统一处理
chunkedChunkWrite 打包逐块发送,终止块收尾
xrt 延伸阅读