Ориентация главы

Поверх фундамента главы 89 эта глава собирает полный слой сообщений HTTP/1: xrtHttp1RequestParse/xrtHttp1ResponseParse разбирают заголовки запроса/ответа из сети в структуру «стартовая строка + массив полей» (трёхзначный возврат, заимствованные представления), xrtHttp1ResponseWrite упаковывает строку состояния и поля за один раз, а поиск по полям напрямую использует фундаментные xrtHttpFieldFind/FieldNext (поиск без учёта регистра по разобранному массиву). Ключевое слово дизайна — привязанный массив: xhttp1head привязывает предоставленный вызывающим массив полей — один Head на поток, используемый вновь и вновь, ноль распределений на запрос; это фундаментный путь рантайма xhttp и самодельных шлюзов. Тело (chunked и прочее) — тема кадрирования главы 91; эта глава останавливается на «пустой строке, завершающей блок заголовков».

90.1引入

Сервер HTTP/1 обрабатывает десятки тысяч запросов в секунду, каждый заголовок около 1 КБ с дюжиной полей. Если каждый разбор распределяет массив полей и освобождает его после — блокировки и фрагментация аллокатора становятся узким местом раньше всего, а путь восстановления после OOM добавляет удар. Ответ XRT виден в самой форме API: xrtHttp1HeadInit(&Head, Fields, 8) привязывает стековый массив вызывающего к Head — разбор лишь заполняет массив представлениями; следующий запрос переиспользует тот же Head и массив, распределений ноль.

Второе проектное решение — трёхзначный возврат: XHTTP1_READY (полный заголовок прибыл), XHTTP1_MORE (входа мало, ждать данных), XHTTP1_ERROR (структура нелегальна). С соглашением «отказ не продвигает» потоковый сервер может бездумно «принял отрезок — попробовал разобрать — MORE: принял ещё» — HTTP-редакция цикла кадрирования главы 72.

90.2概念

Разбор: от байтов к структуре

xhttp1head после разбора: Method/Target (запрос) или Status/Reason (ответ), Version, массив полей (Fields/FieldCount — зона заполнения привязанного массива), Bytes (общее число байтов блока заголовков с завершающей пустой строкой — тело начинается отсюда). Модель трёхзначного возврата:

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

Запрос и ответ делят структуру Head (роль определяет вход вызова: RequestParse/ResponseParse). ResponseParse принимает параметр метода — семантика ответа зависит от метода запроса (ответ HEAD без тела; этот контекст в Plan главы 91 станет жёстким правилом).

Транспортные привязки парсера (http1_net / http1_tls)

Вход RequestParse/ResponseParse — непрерывная память — форма тестовых векторов и целых буферов. Настоящий приёмный путь выдаёт цепочку буферов движка (заголовок может быть разрезан на два блока, глава 65), а TLS-соединение — поток открытого текста через записи (глава 87). Слой привязок подключает парсер к этим двум настоящим источникам: xrtHttp1RequestParseBuffer/xrtHttp1ResponseParseBuffer потребляют xnetbuf (представления Head заимствуют буфер; только при заголовке на стыке выделяется непрерывный префикс по надобности — «функция не потребляет вход; вызывающий потребляет Head.Bytes после обработки»); xrtHttp1RequestParseTls/xrtHttp1ResponseParseTls едят xtlsstream напрямую (открытый текст не потребляется; на стыке записей объединяется только Header — без буфера фиксированного размера на соединение). В каждой привязке по две функции, но сигнатуры и трёхзначная семантика полностью совпадают с версиями в памяти: привязка тонка, потому что парсер с самого дизайна оставил «откуда приходят байты» параметру — защита лимитов, трёхзначные возвраты и дисциплина нуля выделений переносятся целиком. Сервер xhttp главы 104 и потоковый сервис главы 108 в своей основе идут именно по этим двум привязкам.

Поиск и обход

Типовые операции после разбора: по массиву Head.Fields напрямую применяются xrtHttpFieldFind/FieldNext главы 89 (без учёта регистра — имена HTTP-полей семантически регистронезависимы; одноимённые многозначные поля обходятся по порядку, стиль Set-Cookie), xrtHttpFieldGet/FieldGetUnique (взятие значения и семантика уникальности). В комбинации с токен-итератором главы 89 они покрывают составные суждения вида «содержит ли Connection upgrade» — HTTP-обновление главы 94 (рукопожатие WebSocket) использует именно эту комбинацию.

Упаковка: строка состояния + поля одной записью

xrtHttp1ResponseWrite(版本, 状态码, 短语, 字段数组, 数量, 输出, 容量, &长度) (версия, код состояния, фраза, массив полей, количество, вывод, ёмкость, &длина) — строка состояния и блок полей пишутся за один раз: "HTTP/1.1 200 OK\r\nContent-Type: ...\r\n\r\n". Атомарность ёмкости как в главе 88: нехватка — ноль записей. Запросная сторона зеркально — xrtHttp1RequestWrite.Почему не собирать printf-ом: каждый шаг пути сборки может отказать на середине (полсообщения), а детали формата (пробелы строки состояния, терминаторы полей) расползаются по коду — однократная упаковка собирает их в один вход, прогнанный через мутационное тестирование.

Потолки и оборона

xhttp1limits (опционально передаётся в Parse) управляет: потолком числа полей (ёмкость привязанного массива — естественный потолок), потолком общей длины блока заголовков, потолками длины метода/цели.Почему обязательно: злой клиент может прислать блок заголовков на 2 ГБ — парсер без потолков это усилитель памяти. Умолчания консервативны, прокси-сценарии расширяют по надобности — потолки это решение развёртывания, а не глобальная константа.

Предвестие слоёв Message и Body

API этой главы — до «конца блока заголовков». MessageParse главы 91 комбинирует «заголовок + тело» за один раз (для малых сообщений), семейство 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

Что только что произошло. (1) Вход — полные байты запроса (статический массив имитирует однократное прибытие); HeadInit привязывает 8-элементный стековый массив — разбор без распределений. (2) RequestParse возвращает READY: Head.Method/Target — заимствованные представления (указывают внутрь входа), печатается GET /health. (3) ResponseWrite упаковывает одной записью: строка состояния (версия/код/фраза) + два поля + пустая строка; iSize — полное число байтов, после которого тело {"ok":true} пишется сразу.Это весь ответ сервера: никакого построчного printf, никакой ручной сборки CRLF. В реальной службе вход приходит из приёмного буфера главы 67 (MORE → принимать дальше), вывод уходит в путь отправки (бюджет записи главы 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) Строка 1 проверяет модель потолков: длина target в пределе проходит, сверх — отказ; обе стороны параметра обороны. (2) Строка 2 — комбинация поиска: поле по имени + токен-итерация transfer-coding (TokenNext главы 89 на Head). (3) Строка 3 задаёт тон главе 90: Plan тела запроса (вердикт фиксированная длина/chunked) доступен сразу после разбора заголовка. (4) Строка 4 проверяет генерацию строки размера chunk (5\r\n — допустимы расширения). (5) Строка 5 — слой Message: однократный разбор «заголовок+тело», тело — заимствованное представление ok — самый экономный путь малых сообщений. (6) Строка 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

Что только что произошло. (1) Байты запроса нарочно дописываются в два блока — минимальная имитация заголовка на стыке; ParseBuffer доказывает: разбор по цепочке не требует сшивки на стороне вызывающего. (2) Результат изоморфен версии в памяти: Head.Method/Target заимствуют представления и печатаются напрямую. (3) Сторона ответа идёт через ResponseParseBuffer — входы запрос/ответ, один Head. (4) У варианта ParseTls нет отдельного образца: он отличается от версии Buffer только «откуда поток» (xtlsstream вместо xnetbuf); строка контракта это говорит, и образец не выдумывается.

90.4契约

  • Привязанный массив: Head привязывает массив полей вызывающего; число полей естественно ограничено ёмкостью массива; перед переиспользованием Head чистить не нужно (каждый Parse строит заново).
  • Трёхзначный возврат: READY/NEXT/ERROR; NEXT ничего не потребляет и не сообщает об ошибке; отказ не продвигает (семантика повтора как в кадрировании главы 72).
  • Заимствование представлений: Method/Target/Status/Reason/поля заимствуют вход; Bytes — длина блока заголовков с завершающей пустой строкой (старт тела).
  • Семантика поиска: имена полей без учёта регистра; одноимённые многозначные поля обходятся по порядку; промах — явный отказ, не значение NULL.
  • Атомарность упаковки: строка состояния + поля одной записью; нехватка ёмкости — ноль записей; успех публикует общую длину.
  • Потолочная оборона: limits управляют общей длиной блока/метода/цели; разбор без потолков — усилитель памяти; умолчания консервативны.
  • Семантика метода: разбор ответа несёт контекст метода запроса (правило «HEAD/204 без тела» исполняет Plan главы 91).
  • Ноль распределений на запрос: пути разбора и упаковки без кучи (Head/массив/буферы предоставляет вызывающий).
  • Обрезка: HTTP1_HEAD/HTTP1_BODY/HTTP1_MESSAGE — независимые макросы возможностей, зависящие от фундамента HTTP.

Типичные ошибки

Ловушка 1: после NEXT повторный Parse всего объёма (или не продвинули смещение):Ловушка 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: массив полей открыт «впритык»:Ловушка 2: массив полей открыт «впритык»

Симптом: чуть более длинный запрос (легален, но полей на два больше) не разбирается — сервер «случайно» отвергает часть клиентов.

Причина: ёмкость привязанного массива — жёсткий потолок. У здоровых браузеров 15-25 полей в заголовке — обычное дело; прокси-цепочки ещё дописывают семейство X-Forwarded-*.

错误写法
xhttpfield Fields[4];   /* отвергает большинство реальных запросов */
xrtHttp1HeadInit(&Head, Fields, 4);
正确写法
xhttpfield Fields[32];   /* практика: реальные запросы 25 + запас на форвардинг */
xrtHttp1HeadInit(&Head, Fields, 32);

   прокси расширяет или явно отвергает (само по себе решение против злоупотреблений) */
Ловушка 3: упакованный ответ забыл ограничение содержимого, кроме Content-Length:Ловушка 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 записей)

Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки