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

Глава 89 рассказала, «как разобрать одно поле»; эта глава — «как употреблять семейство полей». В HTTP-заголовках есть партия полей с собственной структурной грамматикой: параметры через точку с запятой у Content-Type (charset=UTF-8; boundary="part;42" — значения могут содержать кавычки и точки с запятой), token-списки Connection/Upgrade (согласование возможностей соединения — решающий вход рукопожатия WebSocket из главы 95), Expect (ожидания клиента — поток 100 Continue), TE/Trailer (транспортное кодирование и возможности trailer, глава 91). У каждого семейства свои ловушки разбора (экранирование quoted-string, регистр токенов, повторяемые поля), и XRT даёт каждой семье независимый малый модуль — ParamNext итерацию параметров, FieldTokenFind поиск токена, xrtHttpTeParse сводное согласование. Повседневный ящик инструментов прокси, посредников и реализаторов протоколов.

92.1引入

Реализуя разборщик multipart-форм, первый шаг — вытащить boundary из Content-Type: multipart/form-data; boundary="part;42" — значение с кавычками, и внутри кавычек точка с запятой (boundary может её содержать!). Режущий strchr(';') здесь ломается: part;42 разрезается пополам. Или пишете проверку WebSocket-обновления: содержит ли Connection токен upgrade? Регистр, пробелы, кавычки — ручная проверка опять куча граничных случаев.

Общее у двух сцен: грамматика значения сложнее, чем «похоже на текст» — RFC 9110 определяет для разных семейств разную грамматику (token-list, параметры через точку с запятой, quoted-string), у каждой — канонические правила разбора и исторические дыры реализаций. Специализированный слой XRT реализует правила каждого семейства малыми независимыми API: декодирование кавычек на слое параметров, регистронезависимый поиск на слое токенов, согласование q-значений на слое TE (глава 91 уже показывала). Глава проходит по семействам: какова грамматика, как пользоваться API, где ямы.

92.2概念

Слой параметров: мир точек с запятой и quoted-string

Грамматика «параметров через точку с запятой» у значений полей (Content-Type, Content-Disposition и др.): 主值; key=value; key2="quoted string" (главное значение; key=value; key2="строка в кавычках"). API-тройка:

  • xrtHttpParamNext(值, &偏移, &参数) (значение, &смещение, &параметр): итерация с выдачей xhttpparam — представление Name, Flags (XHTTP_PARAM_HAS_VALUE — разрешены параметры без значения), исходное представление Value.
  • xrtHttpParamValueWrite(&参数, 输出, 容量, &长度) (&параметр, вывод, ёмкость, &длина) (&параметр, вывод, ёмкость, &длина): декодирует quoted-string и пишет значение — кавычки сняты, \" разэкранировано, атомарность ёмкости. boundary="part;42" декодируется в part;42 — точка с запятой благополучно живёт.
  • xrtHttpParamCount/ParamFind/ParamBuild: счёт, поиск по имени, построение новой строки параметров.

Почему «итерация выдаёт исходные представления, декодирование отдельным шагом»: большинству сцен потребления нужно лишь сравнить имя параметра или длину — не трогать кавычки стоит ноль; декодирование — только когда содержимое значения действительно нужно.

Слой токенов: возможности соединения

Connection/Upgrade/Transfer-Encoding — token-списки (вотчина TokenNext главы 89); специализированный слой добавляет семантический поиск: xrtHttpFieldTokenFind — есть ли указанный токен в значении поля (без учёта регистра). «Содержит ли Connection upgrade» — ответ одной строкой; на этом стоят проверка рукопожатия WebSocket главы 95 и поэлементное отслаивание полей прокси (поля, перечисленные в Connection, надо удалить перед пересылкой — итерацию этого списка даёт xrtHttpConnectionNext).

Expect и 100 Continue

Перед отправкой большого тела клиент спрашивает «примет ли сервер»: Expect: 100-continue. xrtHttpExpectFields/ExpectValid решают, несёт ли запрос легальное ожидание 100-continue — сервер отвечает 100 Continue и принимает тело либо отказывает 417.Почему заслуживает отдельного семейства: тайминг 100 переплетается с потоком тела (сервер может ответить 100, принять тело, затем дать финальный статус), и реализаторы легко упускают снисходительный путь «тело отправлено без ожидания 100» — специализированный слой стандартизирует вердикт, тайминг остаётся слою соединения.

Полное семейство поиска по массиву полей

Массив полей, разобранный в главе 89, получает семейство поиска (реализовано в фундаментном слое, переиспользуется слоем заголовков): xrtHttpFieldFind (первый по имени), xrtHttpFieldNext (итерация одноимённых многозначных — сцена Set-Cookie), xrtHttpFieldGet/FieldGetUnique (взятие значения/семантика уникальности — GetUnique для полей, «семантически появляющихся однажды»; повтор — ошибка), xrtHttpFieldNameEqual (сравнение без учёта регистра), xrtHttpFieldTokenCursorInit+FieldTokenCount (счёт токенов и курсор). Это семейство — полный словарь «спросить заголовок», фундамент решений посредника по Route/Cache-Control.

Поверхность обрезки семейств

По макросу возможностей на семейство (HTTP_HOST/HTTP_PARAM/HTTP_TARGET/HTTP_TE/HTTP_CONNECTION/HTTP_EXPECT/HTTP_TRAILER/HTTP_UPGRADE...): встроенный клиент несёт только Host+Param без обновления и Expect; шлюз — всё. Семейства не тянут зависимостей друг от друга — обрезка по-настоящему независима.

Расслоение специализированных семейств

1Фундамент (глава 89)FieldParse - представления полей + TokenNext - итерация списков
2Семейство параметровParamNext/ValueWrite - параметры через ; и декодирование quoted-string
3Семейство семантического поискаFieldFind/FieldGetUnique/FieldTokenFind - регистронезависимый поиск
4Специализированные семействаTE/Encoding/Upgrade/Expect/Trailer/Connection - независимо обрезаемые малые модули согласования

92.3示例

Первая полная программа: итерация параметров и декодирование кавычек

Программа ниже из examples/http/param/main.c — стандартное лекарство ловушки quoted-string:

examples/http/param/main.c源码 ↗
#include <stdio.h>

#include <xrt.h>






















 */


/* 逐项读取参数,并在需要时解码 quoted-string。 */
int main(void)
{
	xstrview Text = XRT_STR_LITERAL(
		"charset=UTF-8; boundary=\"part;42\""
	);
	xhttpparam Param;
	xhttpnext Next;
	char Value[64];
	size_t iOffset = 0;
	size_t iSize;

	while ( (Next = xrtHttpParamNext(
		Text, &iOffset, &Param
	)) == XHTTP_NEXT_ITEM ) {
		if ( (Param.Flags & XHTTP_PARAM_HAS_VALUE) == 0 ) {
			printf("%.*s\n", (int)Param.Name.Size, Param.Name.Data);
			continue;
		}
		if ( !xrtHttpParamValueWrite(
			&Param, Value, sizeof(Value), &iSize
		) ) {
			return 1;
		}
		printf("%.*s = %.*s\n",
			(int)Param.Name.Size, Param.Name.Data,
			(int)iSize, Value);
	}
	return (Next == XHTTP_NEXT_END) ? 0 : 2;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/http/param/main.c -lws2_32 -liphlpapicharset = UTF-8boundary = part;42

Что только что произошло. (1) Вход charset=UTF-8; boundary="part;42" — два параметра. (2) ParamNext выдаёт их по одному: первый со значением (флаг HAS_VALUE), Name=charset, исходное Value=UTF-8 (голый токен — декодировать не нужно, но и через ValueWrite корректно); второй Name=boundary, исходное значение — в кавычках "part;42". (3) ParamValueWrite декодирует: кавычки сняты, внутренняя точка с запятой сохранена целиком — вывод part;42.Это изнанка места крушения метода strchr: точка с запятой в кавычках не режется, парность кавычек проверяется, последовательности экранирования восстанавливаются — всё делает декодер. Нехватка ёмкости — ноль записей, в согласии с общебиблиотечной атомарностью.

Вторая полная программа: обход поиска и обратной записи

Вторая программа из examples/http/field_tour/main.c — четыре строки самопроверки блока полей, поиска, токенов и записи:

examples/http/field_tour/main.c源码 ↗


















 */

#include <stdio.h>
#include <string.h>
#include <xrt/http.h>

#define SV(x) XRT_STR_LITERAL(x)

int main(void)
{
	static const char sBlock[] =
		"Connection: keep-alive\r\n"
		"Cache-Control: max-age=60\r\n"
		"Connection: Upgrade\r\n";
	static const xhttpfield Fields[3] = {
		{ XRT_STR_INIT("Connection"), XRT_STR_INIT("keep-alive, Upgrade") },
		{ XRT_STR_INIT("Cache-Control"), XRT_STR_INIT("max-age=60") },
		{ XRT_STR_INIT("Accept-Encoding"), XRT_STR_INIT("gzip, deflate") }
	};
	xhttpfield Field;
	size_t iOffset = 0;
	size_t iCount = 0;
	size_t iSize = 0;
	char Buffer[256];
	size_t iFound;

	/* FieldBlockCount:整块严格计数。 */
	if ( !xrtHttpFieldBlockCount(SV(sBlock), &iCount) || (iCount != 3u) ) {
		return 1;
	}
	printf("block-count=%zu", iCount);

	/* FieldNext:游标式逐字段(走完块为止)。 */
	{
		size_t iSeen = 0;

		while ( xrtHttpFieldNext(SV(sBlock), &iOffset, &Field) ==
			XHTTP_NEXT_ITEM ) {
			iSeen++;
		}
		printf(" next=%zu\n", iSeen);
	}

	/* Find / Get / Count / GetUnique / NameEqual / ValueValid。 */
	iFound = xrtHttpFieldFind(Fields, 3u, SV("Connection"), 0u);
	if ( iFound != 0u ) { return 1; }
	{
		const xhttpfield* pGet = xrtHttpFieldGet(Fields, 3u,
			SV("Connection"));

		printf("get=%.*s",
			pGet ? (int)pGet->Value.Size : 6,
			pGet ? pGet->Value.Data : "(null)");
	}
	printf(" value-valid=%d",
		xrtHttpFieldValueValid(Fields[0].Value) ? 1 : 0);
	(void)xrtHttpFieldCount(Fields, 3u, SV("nope"));
	printf(" count=%zu",
		xrtHttpFieldCount(Fields, 3u, SV("Connection")));
	{
		const xhttpfield* pUnique = NULL;

		printf(" unique=%s",
			xrtHttpFieldGetUnique(Fields, 3u, SV("Accept-Encoding"),
				&pUnique) == XHTTP_NEXT_ITEM ? "hit" : "miss");
	}
	printf(" find=%d find2=%zu\n",
		xrtHttpFieldNameEqual(SV("connection"), SV("Connection")) ? 1 : 0,
		xrtHttpFieldFind(Fields, 3u, SV("Accept-Encoding"), 0u));

	/* FieldToken 游标:聚合同名 Connection 字段的 token。 */
	{
		xhttpfieldtokencursor Cursor;
		xstrview Token;
		static const xhttpfield Dup[2] = {
			{ XRT_STR_INIT("Accept-Encoding"), XRT_STR_INIT("gzip") },
			{ XRT_STR_INIT("accept-encoding"), XRT_STR_INIT("deflate") }
		};

		xrtHttpFieldTokenCursorInit(&Cursor);
		printf("tokens:");
		while ( xrtHttpFieldTokenNext(Dup, 2u, SV("Accept-Encoding"),
			&Cursor, &Token) == XHTTP_NEXT_ITEM ) {
			printf(" %.*s", (int)Token.Size, Token.Data);
		}
		{
			size_t iTokens = 0;

			(void)xrtHttpFieldTokenCount(Dup, 2u,
				SV("Accept-Encoding"), &iTokens);
			printf(" count=%zu", iTokens);
		}
		printf(" find=%d\n",
			xrtHttpFieldTokenFind(Dup, 2u, SV("Accept-Encoding"),
				SV("deflate")) == XHTTP_NEXT_ITEM ? 1 : 0);
	}

	/* FieldWrite / FieldBlockWrite。 */
	{
		xstrview Block = SV(sBlock);

		(void)xrtHttpFieldWrite(&Fields[0], Buffer, sizeof(Buffer), &iSize);
		printf("write=%c", Buffer[0]);
		(void)xrtHttpFieldBlockWrite(Fields, 2u, Buffer, sizeof(Buffer),
			&iSize);
		printf(" blockwrite=%s\n",
			iSize > 0u ? "ok" : "fail");
		(void)Block;
	}
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/http/field_tour/main.c -lws2_32 -liphlpapiblock-count=3 next=3get=keep-alive, Upgrade value-valid=1 count=1 unique=hit find=1 find2=2tokens: gzip deflate count=2 find=1write=C blockwrite=ok

Что только что произошло. (1) Строка 1 — счёт и итерация блока полей: поверхность пакетной обработки массива многих полей. (2) Строка 2 прогоняет семейство поиска: значение по имени, легальность представления, счёт, семантика уникальности GetUnique (позитивное употребление ловушки 3 главы), два Find проверяют возврат одноимённых значений по порядку (find2=2 — второе поле с тем же именем). (3) Строка 3 — семейство токенов: итерация списка Accept-Encoding, счёт и поиск указанного токена — исполняемая сноска «семантического поиска токенов». (4) Строка 4 — цикл записи (write=C одно поле, blockwrite весь блок) — повторная проверка атомарных соглашений главы 89. Сопроводительный examples/http/small_fields покрывает совместный обход пяти семейств TE/Encoding/Upgrade/Expect/Trailer, examples/http/host — разбор authority: у каждого API главы есть исполняемый образец.

92.4契约

  • Итерация параметров: исходные представления вперёд (Name/Value в кавычках), флаг HAS_VALUE отличает параметры без значения; декодирование (ValueWrite) по надобности — снятие кавычек и восстановление экранирования, атомарность ёмкости.
  • Семантический поиск токенов: FieldTokenFind без учёта регистра; имена полей из списка Connection итерирует ConnectionNext — основа поэлементного отслаивания hop-by-hop.
  • Семантика уникального значения: FieldGetUnique для однозначных полей — повтор это ошибка, а не «взять первый» (молчаливый выбор = согласие на двусмысленность).
  • Expect: ExpectFields/ExpectValid выносят наличие и легальность ожидания; тайминг 100 — слой соединения.
  • Объявление Trailer: список, анонсированный полем Trailer, доступен запросу — сверка с массивом чтения главы 91.
  • Согласование q: целые-в-тысячных (TE/Encoding — общая модель); неупомянутое q=0.
  • Независимая обрезка семейств: макросы семейств взаимно независимы; ParamValueCursorInit/ValueNext дают итерацию значений без декодирования.
  • Ноль распределений: API семейств — только заимствованные представления + буферы вызывающего.

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

Ловушка 1: параметры режутся strchr — точка с запятой в кавычках ломает:Ловушка 1: параметры режутся strchr — точка с запятой в кавычках ломает

Симптом: граница multipart извлечена неверно — boundary="part;42" разрезано на boundary="part и 42", разбор формы полностью неверен.

Причина: в грамматике параметров ; (точка с запятой) внутри quoted-string — не разделитель. Ручная резка не знает контекста кавычек.

错误写法
char* p = strtok(Value, ";");   /* ; из part;42 принята за разделитель */
正确写法
xhttpparam Param; size_t iOff = 0;
while ( (N = xrtHttpParamNext(Value, &iOff, &Param)) ==
		XHTTP_NEXT_ITEM ) {
	char Buf[64]; size_t iSize;
	if ( (Param.Flags & XHTTP_PARAM_HAS_VALUE) &&
			xrtHttpParamValueWrite(&Param, Buf,
				sizeof(Buf), &iSize) ) {
		use(Param.Name, Buf, iSize);   /* декодированное значение: part;42 целиком */
	}
}
Ловушка 2: поиск токена с учётом регистра:Ловушка 2: поиск токена с учётом регистра

Симптом: проверка Connection: keep-alive, Upgrade через strstr(value, "upgrade") возвращает NULL — рукопожатие WebSocket отвергнуто.

Причина: семантика HTTP-токенов регистронезависима, а регистр в сети разнообразен (Upgrade/upgrade/UPGRADE — всё легально).

错误写法
if ( strstr(Field.Value, "upgrade") != NULL ) {
	/* регистр "Upgrade" не попадает: легальное рукопожатие отвергнуто */
}
正确写法
if ( xrtHttpFieldTokenFind(Fields, iCount,
		XRT_STR_LITERAL("upgrade")) ) {
	/* без учёта регистра + контекст кавычек - дело API семейства */
}
Ловушка 3: обработка повторов однозначного поля «взять первое»:Ловушка 3: обработка повторов однозначного поля «взять первое»

Симптом: Content-Length: 11 и Content-Length: 12 вместе; код обрабатывает первое, игнорируя второе — фронт и тыл понимают по-разному: вариант контрабанды.

Причина: семантически единственное поле, встретившееся дважды, — ошибочный вход. «Взять первое» молча распускает двусмысленность — выбирает понимание за атакующего.

错误写法
xstrview V = first_field("Content-Length", ...);
parse_length(V);   /* второй CL остаётся без спроса */
正确写法
xstrview V;
if ( !xrtHttpFieldGetUnique(Fields, iCount,
		XRT_STR_LITERAL("Content-Length"), &V) ) {
	reject("conflicting content-length");  /* повтор = отказ */
}

Практические задания

Базовое: прогнать обход пяти семейств

Прогоните образцы small_fields и param, помечая для каждой строки вывода семейство и API. Замените вход param на boundary="a\"b" (экранированная кавычка) и проверьте декодирование. Критерии приёмки: восстановление экранирования верно (a"b); путь параметра без значения проходит ветку HAS_VALUE-ложь.

Продвинутое: потребитель Content-Type

Реализуйте content_type(字段值, &主类型, &子类型, 参数数组, 容量) (значение поля, &главный тип, &подтип, массив параметров, ёмкость): вырезать два сегмента multipart/form-data и все параметры (декодирование кавычек). Проверить на text/plain (без параметров) и multipart/mixed; boundary="x;y"; charset=UTF-8 (многопараметрический с кавычками и точкой с запятой). Критерии приёмки: оба вывода сходятся с ручной выкладкой; главный/подтип сравниваются после нормализации регистра.

Вызов: отслаиватель hop-by-hop полей

Реализуйте обработку hop-by-hop прокси: итерация Connection на список имён + стандартное hop-by-hop множество (Connection/Keep-Alive/TE/Trailer/Upgrade), удаление попаданий из массива полей (со сжатием массива), затем FieldBlockWrite обратно. Критерии приёмки: после отслаивания запроса Connection: keep-alive, X-Push поле X-Push исчезло, Connection сохранён, но без удалённого пункта; сквозные поля (Accept и др.) нетронуты; ноль распределений кучи.

Итоги

ТемаКратко
Слой параметровитерация ParamNext (исходное представление + HAS_VALUE); ValueWrite декодирует (снятие кавычек/экранирование/атомарность)
quoted-string; в кавычках не режется, \" восстанавливается - ручная резка strchr источник аварий
Поиск токеновFieldTokenFind без учёта регистра; ConnectionNext итерирует имена hop-by-hop
Семейство поискаFieldFind/FieldNext (многозначность)/FieldGet (значение)/FieldGetUnique (повтор = ошибка)
ExpectExpectParse выносит ожидание 100-continue; тайминг - слой соединения
Объявление Trailerанонсированный полем Trailer список доступен; сверка с массивом чтения
Согласование qцелые-в-тысячных; TE/Encoding общая модель; неупомянутое q=0
Семейство Upgradeитерация/счёт/построение - детали рукопожатия главы 95
Обрезкапо макросу на семейство (HOST/PARAM/TE/CONNECTION/EXPECT/TRAILER/UPGRADE...)
Ноль распределенийвсе семейства - заимствованные представления + буферы вызывающего

📖 Справочники API в этой главе:http (72 записей)http_te (16 записей)http_connection (7 записей)http_expect (13 записей)http_trailer (6 записей)

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