本章导读
第 89 章的地基之上,本章组装完整的 HTTP/1 消息层:xrtHttp1RequestParse/xrtHttp1ResponseParse 把线上的请求/响应头解析为"起始行 + 字段数组"的结构(三态返回、借用视图),xrtHttp1ResponseWrite 把状态行与字段一次封包,字段检索直接用地基层的 xrtHttpFieldFind/FieldNext(对解析出的字段数组做大小写不敏感查找)。设计的关键词是绑定数组:xhttp1head 绑定调用方提供的字段数组——一个线程一个 Head 结构反复复用,零逐请求分配,这是 xhttp 运行时与自研网关的地基路径。正文(chunked 等)是第 91 章的分帧主题;本章只到"头块结束的空行"为止。
90.1引入
一个 HTTP/1 服务端每秒处理几万请求,每个请求头约 1 KB、十几行字段。如果每次解析都分配 一个字段数组、解析完 free——分配器的锁与碎片先成为瓶颈,OOM 后的恢复路径再补一刀。XRT 的答案在 API 形状上就能看出来:xrtHttp1HeadInit(&Head, Fields, 8) 把调用方的栈数组绑给 Head——解析只是往数组里填视图;下一个请求复用同一个 Head 与数组,分配次数为零。
第二个设计决定是三态返回:XHTTP1_READY(完整头已到)、XHTTP1_MORE(输入不足,等更多数据)、XHTTP1_ERROR(结构非法)。配合"失败不推进"的约定,流式服务端可以无脑地"收一段、试解析、NEXT 就再收"——第 72 章分帧循环的 HTTP 版。
90.2概念
解析:从字节到结构
xhttp1head 解析后的字段:Method/Target(请求)或 Status/Reason(响应)、Version、字段数组(Fields/FieldCount——绑定数组的填充区)、Bytes(头块总字节数——含终止空行,正文从这里开始)。三态返回模型:
请求与响应共用 Head 结构(角色由调用入口决定:RequestParse/ResponseParse)。ResponseParse 带方法参数——因为响应语义依赖请求方法(HEAD 的响应没有正文;这个上下文在第 91 章的 Plan 里变成硬规则)。
解析器的传输绑定(http1_net / http1_tls)
RequestParse/ResponseParse 的输入是连续内存——测试向量与完整缓冲的形态。真实收包路径拿到的是引擎的缓冲链(头可能被切在两块里,第 65 章),TLS 连接拿到的是跨记录的明文流(第 87 章)。绑定层把解析器接到这两种真实来源上:xrtHttp1RequestParseBuffer/xrtHttp1ResponseParseBuffer 消费 xnetbuf(Head 的视图借用 Buffer,跨块时才按需分配连续前缀——"函数不消费输入,调用方处理完成后消费 Head.Bytes");xrtHttp1RequestParseTls/xrtHttp1ResponseParseTls 直接吃 xtlsstream(不消费明文,跨记录只合并 Header,不分配连接级固定缓冲)。四个函数只有两个,不对——每个绑定两个,但签名与三态语义和内存版完全一致:绑定层薄是因为解析器从设计起就把"字节从哪来"留给了入参——上限防御(limits)、三态返回、零分配纪律原样有效。第 104 章的 xhttp 服务端与第 108 章的流式服务,底层走的就是这两个绑定。
查找与遍历
解析后的常见操作:对 Head.Fields 数组直接用第 89 章的 xrtHttpFieldFind/FieldNext(大小写不敏感——HTTP 字段名语义大小写不敏感;同名字段多值按序遍历,Set-Cookie 类)、xrtHttpFieldGet/FieldGetUnique(取值视图与唯一性语义)。这些与第 89 章的 token 迭代器组合,覆盖"查 Connection 是否含 upgrade"这类复合判断——第 94 章的 HTTP 升级(WebSocket 握手)就用这个组合。
封包:状态行 + 字段一次写
xrtHttp1ResponseWrite(版本, 状态码, 短语, 字段数组, 数量, 输出, 容量, &长度)——状态行与字段块一次写出:"HTTP/1.1 200 OK\r\nContent-Type: ...\r\n\r\n"。容量原子性同第 89 章:不够就零写入。请求侧对应 xrtHttp1RequestWrite。为什么不用 printf 拼:拼接路径的每一步都可能中途失败(半报文)、格式细节(状态行空格、字段终止)散落各处——一次封包把这些收进一个经过变异测试的入口。
上限与防御
xhttp1limits(可选传入 Parse)控制:字段数上限(绑定数组容量即天然上限)、头块总长上限、方法/目标长度上限。为什么必须设:恶意客户端可以发送 2 GB 的头块——没有上限的解析器是内存放大器。默认值保守、代理场景按需放宽——上限是部署决策不是全局常数。
Message 与 Body 的分层预告
本章 API 到"头块结束"为止。第 91 章的 MessageParse 组合"头 + 正文"一次到位(适合小消息),Body 族流式读正文(适合代理边收边转)——两层共享本章的头解析。
90.3示例
第一个完整程序:请求解析与响应封包
下面的程序来自 examples/http/http1/main.c——最短路径的"收请求、发响应":
#include <stdio.h> #include <xrt.h> */ /* 展示零拷贝解析与直接响应封包的基础路径。 */ int main(void) { static const char Request[] = "GET /health HTTP/1.1\r\nHost: localhost\r\n\r\n"; static const xhttpfield ResponseFields[] = { { XRT_STR_INIT("Content-Type"), XRT_STR_INIT("application/json") }, { XRT_STR_INIT("Content-Length"), XRT_STR_INIT("11") } }; xhttpfield RequestFields[8]; xhttp1head Head; xbytesview Input; char Response[256]; size_t iSize; Input.Data = (cbytes)Request; Input.Size = sizeof(Request) - 1u; xrtHttp1HeadInit(&Head, RequestFields, 8); if ( xrtHttp1RequestParse( Input, &Head, NULL, NULL ) != XHTTP1_READY ) { return 1; } printf("%.*s %.*s\n", (int)Head.Method.Size, Head.Method.Data, (int)Head.Target.Size, Head.Target.Data); if ( !xrtHttp1ResponseWrite( XHTTP_VERSION_1_1, 200, XRT_STR_LITERAL("OK"), ResponseFields, 2, Response, sizeof(Response), &iSize ) ) { return 2; } fwrite(Response, 1, iSize, stdout); fwrite("{\"ok\":true}", 1, 11, stdout); return 0; }
刚才发生了什么。 ① 输入是完整请求字节(静态数组模拟一次到达);HeadInit 绑定 8 元素的栈数组——解析零分配。② RequestParse 返回 READY:Head.Method/Target 是借用视图(指向输入内部),打印 GET /health。③ ResponseWrite 一次封包:状态行(版本/状态/短语)+ 两个字段 + 空行;iSize 是完整字节数——随后正文 {"ok":true} 直接跟在后面写。这就是服务端回包的全部:没有逐行 printf、没有手工拼 CRLF。真实服务里输入来自第 67 章的接收缓冲(NEXT→继续收),输出交给发送路径(第 68 章写预算)。
第二个完整程序:Head 全特性巡检
第二个程序来自 examples/http1/head_tour/main.c——五行为你覆盖上限、查找、迭代、chunk 行与 Message:
*/ #include <stdio.h> #include <string.h> #include <xrt.h> int main(void) { static const char sFixed[] = "POST /a HTTP/1.1\r\n" "Host: a.test\r\n" "Content-Length: 2\r\n" "\r\n" "ok"; static const char sChunked[] = "POST /b HTTP/1.1\r\n" "Host: a.test\r\n" "Transfer-Encoding: gzip, chunked\r\n" "\r\n"; xhttp1limits Limits; xhttp1bodylimits BodyLimits; xhttp1bodyplan Plan; xhttp1body Body; xhttp1message Message; xhttp1errorinfo Error; xhttp1head Head; xhttpfield Fields[8]; xhttpfield Trailers[4]; xhttp1transfercoding Coding; const xhttpfield* pField; xbytesview View; xhttpnext Next; size_t iOffset = 0; size_t iSize = 0; size_t iBytes = 0; size_t iCount = 0; uint8 arrLine[16]; int iResult = 1; /* ---- 限额默认值 + request-target 合法性正反。 ---- */ xrtHttp1LimitsInit(&Limits); if ( (Limits.MaxStartLine != 8192u) || (Limits.MaxHead != 65536u) || (Limits.MaxFields != 100u) || xrtHttp1TargetValid(XRT_STR_LITERAL("/a b")) || xrtHttp1TargetValid(XRT_STR_LITERAL("/a#f")) || xrtHttp1TargetValid(XRT_STR_LITERAL("")) || !xrtHttp1TargetValid(XRT_STR_LITERAL("/api?v=1")) ) { goto Cleanup; } printf("http1: limits + target valid +/- ok\n"); /* ---- 字段查找 + Transfer-Encoding 迭代。 ---- */ xrtHttp1HeadInit(&Head, Fields, 8); if ( (xrtHttp1RequestParse( (xbytesview) { (const uint8*)sChunked, sizeof(sChunked) - 1u }, &Head, &Limits, &Error) != XHTTP1_READY) || ((pField = xrtHttp1Field(&Head, XRT_STR_LITERAL("host"))) == NULL) || (pField->Value.Size != 6u) || (memcmp(pField->Value.Data, "a.test", 6u) != 0) || (xrtHttp1Field(&Head, XRT_STR_LITERAL("x-nope")) != NULL) ) { goto Cleanup; } /* TE 迭代须在 Transfer-Encoding 字段值上进行。 */ if ( (pField = xrtHttp1Field(&Head, XRT_STR_LITERAL("transfer-encoding"))) == NULL ) { goto Cleanup; } iOffset = 0; if ( ((Next = xrtHttp1TransferCodingNext( pField->Value, &iOffset, &Coding)) != XHTTP_NEXT_ITEM) || (Coding.Name.Size != 4u) || (memcmp(Coding.Name.Data, "gzip", 4u) != 0) || (xrtHttp1TransferCodingNext(pField->Value, &iOffset, &Coding) != XHTTP_NEXT_ITEM) || (Coding.Name.Size != 7u) || (memcmp(Coding.Name.Data, "chunked", 7u) != 0) || (xrtHttp1TransferCodingNext(pField->Value, &iOffset, &Coding) != XHTTP_NEXT_END) ) { goto Cleanup; } printf("http1: field lookup + transfer-coding iteration ok\n"); /* ---- 分帧计划:chunked 头优先于 Content-Length。 ---- */ if ( !xrtHttp1RequestBodyPlan(&Head, &Plan) || (Plan.Mode != XHTTP1_BODY_CHUNKED) ) { goto Cleanup; } xrtHttp1HeadInit(&Head, Fields, 8); if ( (xrtHttp1RequestParse( (xbytesview) { (const uint8*)sFixed, sizeof(sFixed) - 1u }, &Head, &Limits, &Error) != XHTTP1_READY) || !xrtHttp1RequestBodyPlan(&Head, &Plan) || (Plan.Mode != XHTTP1_BODY_FIXED) || (Plan.Length != 2u) ) { goto Cleanup; } printf("http1: request body plan fixed/chunked ok\n"); /* ---- chunk-size 行:裸 size 与带扩展两种形态。 ---- */ if ( !xrtHttp1ChunkLineWrite(5u, XRT_STR_LITERAL(""), arrLine, sizeof(arrLine), &iSize) || (iSize != 3u) || (memcmp(arrLine, "5\r\n", 3u) != 0) || !xrtHttp1ChunkLineWrite(5u, XRT_STR_LITERAL(";x"), arrLine, sizeof(arrLine), &iSize) || (iSize != 5u) || (memcmp(arrLine, "5;x\r\n", 5u) != 0) ) { goto Cleanup; } printf("http1: chunk line 5 -> \"5\\r\\n\" (+ext) ok\n"); * BodyLimits 必须先初始化——消息解析会按限额约束正文。 ---- */ xrtHttp1BodyLimitsInit(&BodyLimits); * Head 容量不足时解析器返回 FIELDS。 */ xrtHttp1MessageInit(&Message, Fields, 8, NULL, 0); if ( (xrtHttp1RequestMessageParse( (xbytesview) { (const uint8*)sFixed, sizeof(sFixed) - 1u }, true, &Message, &Limits, &BodyLimits, &Error) != XHTTP1_READY) || ((View = xrtHttp1MessageBodyView(&Message)).Size != 2u) || (memcmp(View.Data, "ok", 2u) != 0) ) { goto Cleanup; } printf("http1: message parse -> borrowed body \"ok\" ok\n"); * 描述符 → 读入 trailer 区;另演示独立 TrailersParse。 ---- */ { static const char sChunkWire[] = "5\r\nhello\r\n0\r\n"; static const char sTrailer[] = "X-Sum: 3\r\n\r\n"; xhttp1bodystatus BodyStatus; xbytesview Data; /* 换回 chunked 请求头重建 Plan。 */ xrtHttp1HeadInit(&Head, Fields, 8); if ( (xrtHttp1RequestParse( (xbytesview) { (const uint8*)sChunked, sizeof(sChunked) - 1u }, &Head, &Limits, &Error) != XHTTP1_READY) || !xrtHttp1RequestBodyPlan(&Head, &Plan) || (Plan.Mode != XHTTP1_BODY_CHUNKED) || !xrtHttp1BodyInit(&Body, &Plan, NULL, 0u, &BodyLimits) ) { goto Cleanup; } /* 数据块与终止块分两步喂入。 */ BodyStatus = xrtHttp1BodyRead(&Body, (xbytesview) { (const uint8*)sChunkWire, sizeof(sChunkWire) - 1u }, false, &iBytes, &Data, &Error); if ( (BodyStatus != XHTTP1_BODY_DATA) || (Data.Size != 5u) || (memcmp(Data.Data, "hello", 5u) != 0) ) { goto Cleanup; } BodyStatus = xrtHttp1BodyRead(&Body, (xbytesview) { (const uint8*)sChunkWire + 8u, 5u }, false, &iBytes, &Data, &Error); if ( (BodyStatus != XHTTP1_BODY_MORE) || (iBytes != 5u) || /* 终止块后处于 trailer 态——此刻才能重绑。 */ !xrtHttp1BodyTrailers(&Body, Trailers, 4u) ) { goto Cleanup; } BodyStatus = xrtHttp1BodyRead(&Body, (xbytesview) { (const uint8*)sTrailer, sizeof(sTrailer) - 1u }, true, &iBytes, &Data, &Error); if ( (BodyStatus != XHTTP1_BODY_DONE) || (Body.TrailerCount != 1u) || !xrtHttp1BodyDone(&Body) ) { goto Cleanup; } /* 独立 trailer 区解析(不经 Body Reader)。 */ iCount = 0; if ( (xrtHttp1TrailersParse( (xbytesview) { (const uint8*)sTrailer, sizeof(sTrailer) - 1u }, Trailers, 4u, &BodyLimits, &iBytes, &iCount, &Error) != XHTTP1_READY) || (iCount != 1u) || (Trailers[0].Value.Size != 1u) ) { goto Cleanup; } } printf("http1: body trailers rebind + trailer parse ok\n"); iResult = 0; Cleanup: return iResult; }
刚才发生了什么。 ① 第 1 行验证上限模型:target 长度在限内通过、超限拒绝——防御参数的正负两侧。② 第 2 行是查找组合拳:按名找字段 + transfer-coding 的 token 迭代(第 89 章 TokenNext 在 Head 上的应用)。③ 第 3 行给第 91 章打样:请求正文的 Plan(定长/chunked 判定)在头解析后立即可得。④ 第 4 行验证 chunk size 行生成(5\r\n——可带扩展)。⑤ 第 5 行是 Message 层:一次解析"头+正文",正文借用视图 ok——小消息的最省路径。⑥ 第 6 行验证 trailer 数组重绑与 trailer 块解析——第 91 章预告。六行合起来是本章+下一章的 API 面。
完整程序:ParseBuffer——缓冲链上的请求/响应解析
来自仓库范例 examples/http1/parse_buffer/main.c:
*/ #include <stdio.h> #include <xrt.h> int main(void) { static const char sRequest[] = "GET /x HTTP/1.1\r\nHost: a.test\r\n\r\n"; static const char sResponse[] = "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok"; xnetbufpool* pPool = xrtNetBufPoolCreate(NULL); xnetbuf ReqBuf; xnetbuf RspBuf; xhttpfield Fields[8]; xhttp1head Head; bool bOk = false; if ( pPool == NULL ) { return 1; } /* 请求链:分两块追加(头跨块场景的最小化模拟)。 */ if ( xrtNetBufInit(&ReqBuf, pPool) && xrtNetBufAppend(&ReqBuf, sRequest, 12u) && xrtNetBufAppend(&ReqBuf, sRequest + 12u, sizeof(sRequest) - 13u) ) { xrtHttp1HeadInit(&Head, Fields, 8); if ( xrtHttp1RequestParseBuffer(&ReqBuf, &Head, NULL, NULL) == XHTTP1_READY ) { printf("request: %.*s %.*s\n", (int)Head.Method.Size, Head.Method.Data, (int)Head.Target.Size, Head.Target.Data); bOk = true; } xrtNetBufClear(&ReqBuf); } /* 响应链:单块。 */ if ( bOk && xrtNetBufInit(&RspBuf, pPool) && xrtNetBufAppend(&RspBuf, sResponse, sizeof(sResponse) - 1u) ) { xrtHttp1HeadInit(&Head, Fields, 8); if ( xrtHttp1ResponseParseBuffer(&RspBuf, &Head, NULL, NULL) == XHTTP1_READY ) { printf("response: %u\n", (unsigned)Head.Status); bOk = Head.Status == 200u; } xrtNetBufClear(&RspBuf); } xrtNetBufPoolDestroy(pPool); return bOk ? 0 : 2; }
刚才发生了什么。 ① 请求字节刻意分两块追加进缓冲链——头跨块的最小化模拟,ParseBuffer 证明链上解析无需调用方拼接。② 解析结果与内存版同构:Head.Method/Target 借用视图直接打印。③ 响应侧同走 ResponseParseBuffer——请求/响应两个入口、一套 Head。④ ParseTls 版本无独立示例:它与 Buffer 版只差"流来源"这个参数(xtlsstream 替代 xnetbuf),契约行里说清即可,不硬造示例。
90.4契约
- 绑定数组:Head 绑定调用方字段数组;字段数天然受数组容量限制;复用 Head 前无需清理(每次 Parse 重建)。
- 三态返回:
READY/NEXT/ERROR;NEXT不消费不报错;失败不推进(重试语义同第 72 章分帧)。 - 视图借用:Method/Target/Status/Reason/字段全部借用输入;
Bytes是含终止空行的头块长度(正文起点)。 - 查找语义:字段名大小写不敏感;同名字段多值按序遍历;找不到返回明确失败非 NULL 值。
- 封包原子性:状态行+字段一次写出;容量不足零写入;成功发布总长。
- 上限防御:limits 控制头块总长/方法/目标长度;无上限的解析是内存放大器;默认保守。
- 方法语义:响应解析携带请求方法上下文(HEAD/204 无正文的规则在第 91 章由 Plan 执行)。
- 零逐请求分配:解析与封包路径无堆分配(Head/数组/缓冲全由调用方提供)。
- 裁剪:
HTTP1_HEAD/HTTP1_BODY/HTTP1_MESSAGE独立特性宏,依赖 HTTP 地基。
常见错误
症状:流式服务端"收一段试一段"写成死循环或重复解析同一批字节——CPU 空转或字段重复。
原因:NEXT 的语义是"这些输入不够"——正确动作是把新数据追加到同一缓冲再 Parse(输入从头开始重试,解析器保证 NEXT 路径无副作用);或者配合返回的消费偏移推进。把它当"错误"丢缓冲重来,就回到拼接地狱。
while ( recv(buf) > 0 ) { if ( Parse(buf) == XHTTP1_MORE ) { continue; /* 只试新收的字节:永远不完整 */ } break; }
/* 追加式缓冲(第 65 章):收到的都攒在同一段里 */ while ( recv_append(&In) > 0 ) { if ( xrtHttp1RequestParse(view(&In), &Head, NULL, NULL) == XHTTP1_READY ) { break; /* Bytes 之后的字节是正文/下一请求 */ } }
症状:稍长的请求(合法但字段多两条)解析失败——服务端对一部分客户端"随机"拒绝。
原因:绑定数组容量是硬上限。健康浏览器的请求头 15-25 个字段很常见;代理链还会追加 X-Forwarded-* 系列。
xhttpfield Fields[4]; /* 拒绝大多数真实请求 */ xrtHttp1HeadInit(&Head, Fields, 4);
xhttpfield Fields[32]; /* 经验值:真实请求 25 + 转发追加余量 */ xrtHttp1HeadInit(&Head, Fields, 32); 代理按需放大或显式拒绝(这本身是防滥用决策) */
症状:自写的响应没带 Content-Length 也没声明 chunked——客户端不知道正文何时结束,挂着等到超时或连接关闭。
原因:HTTP/1 的正文必须有定界声明(Content-Length、chunked、或"连接关闭即结束"——最后一种只对 HTTP/1.0 语义可靠)。封包 API 帮你写字段,但定界字段是你必须提供的语义。
xrtHttp1ResponseWrite(XHTTP_VERSION_1_1, 200, OK, NoFields, 0, Out, sizeof(Out), &iSize); send(Out, iSize); send(Body, BodySize); /* 无定界:客户端不知道何时结束 */
static const xhttpfield F[] = { { XRT_STR_INIT("Content-Length"), XRT_STR_INIT("11") } }; xrtHttp1ResponseWrite(XHTTP_VERSION_1_1, 200, XRT_STR_LITERAL("OK"), F, 1, Out, sizeof(Out), &iSize); send(Out, iSize); /* 头声明 11 字节 */ send(Body, 11); /* 正文恰好 11:定界完整 */
编程练习
把 http1 示例的请求换成 POST(带 Content-Length: 5 与 5 字节正文),解析后打印 Head.Bytes 并验证"正文从 Bytes 开始"。验收标准:Bytes 与手数一致;正文视图与原字节对应。
把 41 字节请求按 1..N 字节随机分片"到达"(模拟 TCP 分段),每片追加后 Parse——统计从第几片开始 READY。再故意在中间插入畸形(两个连续冒号的字段名)验证 ERROR 且此前状态未污染。验收标准:任意分片顺序最终 READY;错误输入在含畸形的分片到达时报 ERROR。
一段缓冲里粘着两个完整请求(HTTP/1.1 pipelining):解析请求一(Bytes 定位边界)→ 消费 → 在剩余缓冲上解析请求二。实现 parse_pipelined(输入, 回调) 循环直到 NEXT。验收标准:两个请求各自正确解析;第二个不完整时停在 NEXT 且缓冲状态可继续;全程一个 Head/一个数组零分配。
小结
| 知识点 | 速查 |
|---|---|
| 核心入口 | RequestParse / ResponseParse(三态);ResponseWrite / RequestWrite(一次封包) |
| 绑定数组 | HeadInit 绑调用方字段数组;容量即字段上限;复用零分配 |
| 三态 | READY(Bytes=正文起点)/ MORE(等数据,无副作用)/ ERROR(不推进) |
| 查找 | HeadFind 大小写不敏感;多值字段按序遍历;配 TokenNext 做列表判断 |
| 封包 | 状态行+字段一次写;容量不足零写入;定界字段(CL/chunked)是调用方语义责任 |
| 上限 | limits 控头块/方法/目标长度;默认保守;防内存放大 |
| 方法上下文 | 响应解析带请求方法;HEAD/204 无正文规则由 Body Plan 执行 |
| Message 层 | 头+正文一次到位(小消息);Body 层流式(第 91 章) |
| 零分配 | 解析/封包路径无堆分配;Head/数组/缓冲全由调用方出 |
| 传输绑定 | ParseBuffer 吃缓冲链 / ParseTls 吃 TLS 流——签名与三态同内存版;跨块/跨记录才合并 |
📖 本章涉及的 API 参考手册:http1(39 个条目)http(72 个条目)http1_net(2 个条目)http1_tls(2 个条目)
点击查看完整签名、参数约束、返回值与错误说明