Ориентация главы
Глава 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; шлюз — всё. Семейства не тянут зависимостей друг от друга — обрезка по-настоящему независима.
Расслоение специализированных семейств
92.3示例
Первая полная программа: итерация параметров и декодирование кавычек
Программа ниже из examples/http/param/main.c — стандартное лекарство ловушки quoted-string:
#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; }
Что только что произошло. (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 — четыре строки самопроверки блока полей, поиска, токенов и записи:
*/ #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; }
Что только что произошло. (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 семейств — только заимствованные представления + буферы вызывающего.
Типичные ошибки
Симптом: граница 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 целиком */ } }
Симптом: проверка 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 семейства */ }
Симптом: 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(字段值, &主类型, &子类型, 参数数组, 容量) (значение поля, &главный тип, &подтип, массив параметров, ёмкость): вырезать два сегмента multipart/form-data и все параметры (декодирование кавычек). Проверить на text/plain (без параметров) и multipart/mixed; boundary="x;y"; charset=UTF-8 (многопараметрический с кавычками и точкой с запятой). Критерии приёмки: оба вывода сходятся с ручной выкладкой; главный/подтип сравниваются после нормализации регистра.
Реализуйте обработку 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 (повтор = ошибка) |
| Expect | ExpectParse выносит ожидание 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 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки