本章导读

第 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(头块总字节数——含终止空行,正文从这里开始)。三态返回模型:

输入不足等待更多数据(不消费、不报错) →XHTTP1_MORE
结构完整视图发布,Bytes 给出正文偏移 →XHTTP1_READY
结构非法字段数超限/起始行非法/头不完整语义 →XHTTP1_ERROR

请求与响应共用 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——最短路径的"收请求、发响应":

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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/http/http1/main.c -lws2_32 -liphlpapiGET /healthHTTP/1.1 200 OKContent-Type: application/jsonContent-Length: 11

刚才发生了什么。 ① 输入是完整请求字节(静态数组模拟一次到达);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:

examples/http1/head_tour/main.c源码 ↗



























 */

#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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/http1/head_tour/main.c -lws2_32 -liphlpapihttp1: limits + target valid +/- okhttp1: field lookup + transfer-coding iteration okhttp1: request body plan fixed/chunked okhttp1: chunk line 5 -> "5\r\n" (+ext) okhttp1: message parse -> borrowed body "ok" okhttp1: body trailers rebind + trailer parse ok

刚才发生了什么。 ① 第 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:

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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/http1/parse_buffer/main.c -lws2_32 -liphlpapirequest: GET /xresponse: 200

刚才发生了什么。 ① 请求字节刻意分两块追加进缓冲链——头跨块的最小化模拟,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 地基。

常见错误

坑 1:NEXT 之后重新 Parse 全量(或不推进偏移)

症状:流式服务端"收一段试一段"写成死循环或重复解析同一批字节——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 之后的字节是正文/下一请求 */
	}
}
坑 2:字段数组按"恰好够"开

症状:稍长的请求(合法但字段多两条)解析失败——服务端对一部分客户端"随机"拒绝。

原因:绑定数组容量是硬上限。健康浏览器的请求头 15-25 个字段很常见;代理链还会追加 X-Forwarded-* 系列。

错误写法
xhttpfield Fields[4];   /* 拒绝大多数真实请求 */
xrtHttp1HeadInit(&Head, Fields, 4);
正确写法
xhttpfield Fields[32];   /* 经验值:真实请求 25 + 转发追加余量 */
xrtHttp1HeadInit(&Head, Fields, 32);

   代理按需放大或显式拒绝(这本身是防滥用决策) */
坑 3:响应封包忘了 Content-Length 之外的定界

症状:自写的响应没带 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 个条目)

点击查看完整签名、参数约束、返回值与错误说明