Ориентация главы
Глава 91 решила «границы тела»; эта глава решает «кодирование содержимого тела». Что читает слой кадрирования из ответа с Content-Encoding: gzip — сжатые байты, а бизнесу нужен открытый текст. xrtHttpDecodeCreate строит потоковый декодер прямо из полей заголовка ответа (автосцепление многоуровневых кодирований — декларации вроде gzip, br), xrtHttpDecodeWrite кормит сегмент за сегментом с проталкиванием расшифрованного текста в обратный вызов, xrtHttpDecodeDone сверяет окончание (CRC и хвостовую длину gzip — против тихой порчи). Дизайнерская поза та же, что в главе 90: сегмент прибыл — декодирован, открытый текст сразу потребителю — нигде не копится полный ответ, память не зависит от размера тела. В паре с согласованием содержимого на отправителя (q-значения Accept-Encoding) замыкается полный цикл сжатия — HTTP-ядро тома 9 здесь завершается.
93.1引入
Скачивается 50 МБ gzip-сжатого JSON: наивная реализация — «принять весь ответ → распаковать одним блоком → получить 80 МБ текста» — пиковая память 130 МБ, и обработка начинается лишь после последнего байта. Потоковая реализация — «каждый сжатый сегмент прибыл → сразу декодирован отрезок текста → протолкнут парсеру» — память постоянна (порядка одного скользящего окна), задержка первого байта примерно равна первой сетевой порции. Разница не в алгоритме gzip (декомпрессор главы 29 уже потоковый), а в коммутации: кто соединяет «сегменты, выплёвываемые HTTP-слоем кадрирования» со «входом декомпрессора», кто обрабатывает сцепление многоуровневых кодирований, кто сверяет окончание?
Модуль http_decode и есть этот провод: вход — представление Data, выплёвываемое BodyRead главы 91, выход — ваш обратный вызов, а посередине автоматически сцепляется каждый уровень, объявленный Content-Encoding. Что декларирует поле заголовка, то и декодируется — включая identity (без декодирования) и многоуровневые цепочки; объявленная, но не включённая в сборку кодировка (например br) при создании явно отказывает — никакого тихого пропуска (тихо = отдать бизнесу сжатые данные как текст; дальше всё неверно).
93.2概念
Построение из заголовка: контракт Create
xrtHttpDecodeCreate(字段数组, 数量, 可选配置) (массив полей, количество, опциональная конфигурация) сканирует поле Content-Encoding (несколько вхождений сцепляются в порядке появления) и строит декодер: gzip (gzip-контейнер семейства Deflate главы 29), deflate (обёртка zlib), identity (сквозной), многоуровневые цепочки (даже патологическая, но легальная декларация gzip поверх gzip обрабатывается верно). Отсутствие поля Content-Encoding = сквозной.Семантика отказа: неизвестная или не включённая кодировка — создание возвращает NULL — вызывающий решает деградировать (отбросить ответ) или отказать; никогда «угадать похожее и декодировать».
Конфигурация (опционально; NULL берёт умолчания) различает режимы compat и safe: исторические серверы (особенно старые реализации deflate) выдают местами неканонический вывод; compat принимает его через окно снисходительности (браузерно-совместимое поведение), safe строго отвергает — умолчание стоит на безопасной стороне, ослабление явно при стыковке с legacy-бэкендами. Это стандартный компромисс дисциплины «строгого разбора» перед историческим багажом: строго по умолчанию, снисходительность только явно.
Потоковая подача: Write и обратный вызов
Параметр bFinal помечает «это последний сегмент» — завершающий блок gzip-потока (с CRC32 и исходной длиной) сверяется здесь; Done можно вызвать и отдельно после Write (у некоторых транспортов «подача закончена» отделена от «последнего сегмента»).Смысл формы обратного вызова: декодер не распределяет буфер за вас — текст проталкивается вам, и стратегия буферизации (парсить сразу? писать в канал? на диск?) целиком ваша. В одном ряду с обратными вызовами декомпрессии главы 29 и представлениями Body главы 91.
Замкнутый цикл с согласованием содержимого
Декодирование — сторона ответа; сторона запроса заявляет «что я умею»: Accept-Encoding: gzip;q=1.0, deflate (q-значение через xrtHttpAcceptEncodingQuality — модель целых-в-тысячных, та же, что в главах 90/91). Сервер выбирает из заголовка запроса приемлемое для обеих сторон кодирование и пишет его в Content-Encoding. Дисциплина клиента: заявляйте только то, что реально умеете декодировать — вписать br в Accept-Encoding без собранного brotli-декодера означает: сервер выберет br, и ваш Create откажет. Модуль http_encoding даёт разбор и построение на стороне запроса.
Панорама композиции: конвейер потребления одного полного ответа
Четыре слоя, каждый на своём посту, между слоями только представления и обратные вызовы — это полный скелет, собранный первыми пятью главами тома 9; рантайм xhttp главы 98 упакует его в высокоуровневое API.
93.3示例
Первая полная программа: от заголовка ответа к обратному вызову с текстом
Программа ниже из examples/http/decode/main.c — поток gzip на 42 байта декодирован целиком:
#include <xrt/http_decode.h> #include <stdio.h> /* 把解码片段直接写到标准输出,实际网络代码可改为业务消费回调。 */ static bool printBody(xbytesview Data, ptr pData) { FILE* pOutput = (FILE*)pData; return fwrite(Data.Data, 1, Data.Size, pOutput) == Data.Size; } */ /* 展示从 HTTP/1 Header 建立流式 gzip 解码器。 */ int main(void) { static const uint8 Gzip[] = { 0x1F, 0x8B, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xFF, 0xCB, 0x48, 0xCD, 0xC9, 0xC9, 0x57, 0x48, 0xCE, 0xCF, 0x2D, 0x28, 0x4A, 0x2D, 0x2E, 0x4E, 0x4D, 0x51, 0x28, 0xCF, 0x2F, 0xCA, 0x49, 0x01, 0x00, 0xA1, 0x2D, 0x94, 0x53, 0x16, 0x00, 0x00, 0x00 }; static const xhttpfield Fields[] = { { XRT_STR_INIT("Content-Encoding"), XRT_STR_INIT("gzip") } }; xhttpdecode* pDecode = xrtHttpDecodeCreate(Fields, 1, NULL); bool bSuccess; if ( pDecode == NULL ) { return 1; } bSuccess = xrtHttpDecodeWrite( pDecode, (xbytesview){ Gzip, sizeof(Gzip) }, true, printBody, stdout ) && xrtHttpDecodeDone(pDecode); xrtHttpDecodeDestroy(pDecode); return bSuccess ? 0 : 1; }
Что только что произошло. (1) Вход — встроенный 42-байтовый поток gzip (с магией 1F 8B в начале) и поле Content-Encoding: gzip — имитация «массив заголовков + сжатое тело», уже выплюнутая главой 90. (2) DecodeCreate сканирует поле и строит декодер: один уровень gzip. (3) DecodeWrite кормит один раз (bFinal=true — здесь сегмент один; в потоковых сценах на каждый сегмент false, на последний true), и декодированный текст проталкивается прямо в обратный вызов printBody — в сетевой форме сюда ставится JSON-парсер или писатель в канал, память не растёт с телом. (4) DecodeDone сверяет окончание: несовпадение хвостовых CRC32 и длины gzip-потока отказывает здесь — сжатые данные испорчены в передаче: лучше ошибка, чем неверные данные (тихая порча — самая коварная неисправность цепочки сжатия). (5) Destroy освобождает — декодер единственный владеющий объект модуля.
Вторая полная программа: полный обход возможностей декодера
Вторая программа из examples/http/decode_tour/main.c — три строки покрывают режимы, контентный режим и сброс:
*/ #include <stdio.h> #include <string.h> #include <xrt.h> /* 一段 gzip 压缩的 "identity-passthrough-check"。 */ static const uint8 arrGzip[46] = { 0x1F, 0x8B, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x0A, 0xCB, 0x4C, 0x49, 0xCD, 0x2B, 0xC9, 0x2C, 0xA9, 0xD4, 0x2D, 0x48, 0x2C, 0x2E, 0x2E, 0xC9, 0x28, 0xCA, 0x2F, 0x4D, 0xCF, 0xD0, 0x4D, 0xCE, 0x48, 0x4D, 0xCE, 0x06, 0x00, 0x9A, 0xFB, 0x0B, 0xA2, 0x1A, 0x00, 0x00, 0x00 }; /* 输出回调:累计并比对内容。 */ typedef struct exampleout { size_t iBytes; char arrText[64]; } exampleout; static bool exampleOutput(xbytesview Data, ptr pUserData) { exampleout* pOut = (exampleout*)pUserData; if ( pOut->iBytes + Data.Size > sizeof(pOut->arrText) ) { return false; } memcpy(pOut->arrText + pOut->iBytes, Data.Data, Data.Size); pOut->iBytes += Data.Size; return true; } int main(void) { xhttpdecodeconfig Compat; xhttpdecodeconfig Safe; xhttpdecode* pDecode = NULL; xhttpfield Fields[2]; exampleout Out; int iResult = 1; /* ---- 两套配置 + gzip 字段建解码器。 ---- */ xrtHttpDecodeConfigInit(&Compat); xrtHttpDecodeConfigInitSafe(&Safe); Fields[0].Name = XRT_STR_LITERAL("Content-Encoding"); Fields[0].Value = XRT_STR_LITERAL("gzip"); pDecode = xrtHttpDecodeCreate(Fields, 1u, &Compat); if ( (pDecode == NULL) || /* 兼容配置不设输出上限;安全配置有显式上限。 */ (Compat.OutputLimit != XHTTP_DECODE_OUTPUT_UNLIMITED) || (Safe.OutputLimit == Compat.OutputLimit) ) { goto Cleanup; } printf("decode: config compat vs safe + create ok\n"); /* ---- gzip 模式 + 输入输出计量。 ---- */ memset(&Out, 0, sizeof(Out)); if ( (xrtHttpDecodeMode(pDecode) != XHTTP_DECODE_CONTENT) || !xrtHttpDecodeWrite(pDecode, (xbytesview) { arrGzip, sizeof(arrGzip) }, true, exampleOutput, (ptr)&Out) || !xrtHttpDecodeDone(pDecode) || (Out.iBytes != 26u) || (memcmp(Out.arrText, "identity-passthrough-check", 26u) != 0) || (xrtHttpDecodeInputSize(pDecode) != 46u) || (xrtHttpDecodeOutputSize(pDecode) != 26u) ) { goto Cleanup; } printf("decode: gzip content mode + input/output sizes ok\n"); /* ---- Reset 换 identity 字段:窗口复用 + 透传模式。 ---- */ Fields[0].Value = XRT_STR_LITERAL("identity"); memset(&Out, 0, sizeof(Out)); if ( !xrtHttpDecodeReset(pDecode, Fields, 1u, &Compat) || (xrtHttpDecodeMode(pDecode) != XHTTP_DECODE_IDENTITY) || !xrtHttpDecodeWrite(pDecode, (xbytesview) { arrGzip, 4u }, true, exampleOutput, (ptr)&Out) || !xrtHttpDecodeDone(pDecode) || (Out.iBytes != 4u) || (xrtHttpDecodeInputSize(pDecode) != 4u) ) { goto Cleanup; } printf("decode: reset to identity passthrough ok\n"); iResult = 0; Cleanup: xrtHttpDecodeDestroy(pDecode); return iResult; }
Что только что произошло. (1) Строка 1 противопоставляет два режима конфигурации и проверяет создание — развилка compat/safe это «строго по умолчанию, ослабление явно» из главы 91. (2) Строка 2 проверяет контентный режим gzip и учёт размеров входа/выхода — input/output sizes дают данные, готовые для расчёта степени сжатия и метрик мониторинга. (3) Строка 3 сбрасывает в identity-сквозной — следующий ответ на том же соединении может быть несжатым: состояние декодера обнуляется, режим переключается, объект переиспользуется без пересоздания (ещё одна ступень вниз по стоимости на ответ для служб с множеством соединений).
93.4契约
- Построение из заголовка:
DecodeCreateсканирует Content-Encoding (несколько вхождений сцепляются по порядку); нет поля = сквозной; неизвестная/невключённая кодировка — отказ создания, без догадок и тихих пропусков. - Режимы: safe по умолчанию (строго); compat явно ослабляет (окно снисходительности к историческим серверам); конфигурация
NULLберёт безопасное умолчание. - Потоковость: Write по сегментам (bFinal помечает последний); текст немедленно в обратный вызов; отказ обратного вызова — отказ обработки сегмента — декодер не буферизует за вас.
- Финальная сверка: Done проверяет CRC и хвостовую длину gzip; отказ = содержимое испорчено; правильное действие — отбросить весь ответ, а не использовать часть.
- Сброс: Reset обнуляет и переключает в identity — один объект на многоответное соединение.
- Согласование содержимого: заявка Accept-Encoding обязана соответствовать возможностям декодирования; модуль
http_encoding(AcceptEncodingParse/Quality/Select) — разбор/построение на стороне запроса; q целыми-в-тысячных. - Объектная модель: Create/Destroy владеющие (единственная точка распределения модуля); Write/Done без дополнительных распределений.
- Обрезка:
XRT_MODULE_HTTP_DECODEзависит от COMPRESS и фундамента HTTP — не декодирующие клиенты снимают в ноль. - Граница композиции: вверх — представление Data из Body (глава 91); вниз — ваш обратный вызов: ноль копий насквозь.
Типичные ошибки
Симптом: постоянно true — с второго сегмента многосегментного потока всё неверно (декодер завершающе сверяет каждый сегмент как полный поток); постоянно false — Done вечно сообщает «поток неполон», проверка CRC фиктивна.
Причина: bFinal — сигнал «транспортный слой знает: это последний сегмент», он приходит из вашего суждения о состоянии DONE Body главы 91, а не заполняется как попало.
while ( read_chunk(&Data) ) { DecodeWrite(pDec, Data, true, cb, ctx); /* каждый сегмент как последний */ }
while ( (St = xrtHttp1BodyRead(&Body, In, bEof, &iUsed, &Data, NULL)) != XHTTP1_BODY_DONE ) { if ( St != XHTTP1_BODY_DATA ) { return false; } /* последний сегмент: слой Body вот-вот DONE (следующий раунд и есть DONE) */ DecodeWrite(pDec, Data, false /* всё, кроме настоящего последнего */, cb, ctx); } xrtHttpDecodeDone(pDec); /* сверка слоя декодирования после закрытия слоя кадрирования */
Симптом: проверка CRC отказала (порча в передаче), но обратный вызов уже протолкнул 90% текста вниз по течению — там неполные данные приняли за полные.
Причина: естественное напряжение потоковости — данные уже ушли, когда выяснилось, что финал плох. Решение — и низ по течению работает транзакционно: при отказе CRC он обязан уметь аннулировать (отбросить/откатить), либо выбирается консервативный режим «сначала всё забуферить, подтвердить, потом принять» (потоковость меняется на безопасность).
/* обратный вызов пишет текст прямо в базу */ static bool onPlain(xbytesview Data, ptr pCtx) { db_append((db*)pCtx, Data); /* при отказе Done уже записано: грязные данные */ return true; }
/* обратный вызов пишет в аннулируемый стейджинг (файл/сегмент памяти); коммит после успешного Done */ static bool onPlain(xbytesview Data, ptr pCtx) { staging_append((staging*)pCtx, Data); return true; } /* ... */ if ( !xrtHttpDecodeDone(pDec) ) { staging_discard(&Staging); /* целиком аннулировать: чистый отказ */ } else { staging_commit(&Staging); }
Симптом: сервер по согласованию выбирает brotli; клиентский DecodeCreate отказывает — функция загрузки «случайно» недоступна на части сайтов.
Причина: заявка согласования содержимого — это обещание возможностей. Каждая кодировка, вписанная в Accept-Encoding, обязана реально декодироваться вашей сборкой. XRT поддерживает gzip/deflate/identity — br не писать.
/* "стандартный заголовок", списанный откуда-то */ send_header("Accept-Encoding: gzip, deflate, br, zstd"); /* сервер выбирает br -> Create отказывает */
/* заявка соответствует возможностям: эта сборка поддерживает gzip/deflate */ send_header("Accept-Encoding: gzip, deflate"); /* br не заявлен -> сервер его не выберет */
Практические задания
Подайте 42-байтовый поток образца decode тремя сегментами 7/13/22 байтов (первые два с bFinal=false) и проверьте неизменность вывода. Затем нарочно инвертируйте один байт последнего сегмента и проверьте отказ Done. Критерии приёмки: сегментированный вывод совпадает с цельным; отказ CRC пойман и отчётён как ошибка декодирования.
Сцепите главы 90/91/93 в полную цепочку потребления: полный байтовый поток ответа chunked+gzip (заготовьте сами; можно сгенерировать curl-ом) → ResponseParse → BodyPlan/Body потоково → DecodeCreate/Write → обратный вызов считает суммарную длину текста → двойная сверка Done. Критерии приёмки: текст совпадает с выводом curl --compressed; вся цепочка без буферизации целого ответа (сверка со статистикой главы 6); при отказе любого слоя последующие не исполняются.
Реализуйте fetch(Url, 支持的编码列表) (Url, список поддержанных кодировок): сторона запроса строит Accept-Encoding по возможностям (q: gzip 1000, deflate 900); сторона ответа — полный конвейер; для ответа с Content-Encoding вне возможностей (сервер игнорирует согласование) — явная ошибка, а не неверное декодирование. Критерии приёмки: корректно для трёх ответов gzip/deflate/без кодирования; внеполосная кодировка сообщает "unsupported content-encoding"; 8 параллельных загрузок при постоянной памяти.
Итоги
| Тема | Кратко |
|---|---|
| Три входа | Create (строится из полей заголовка) / Write (по сегментам, проталкивание в обратный вызов) / Done (финальная сверка CRC) |
| Поддержка кодировок | gzip / deflate / identity / многоуровневые цепочки; неизвестная или невключённая — отказ создания |
| Режимы | safe по умолчанию строго; compat явно ослабляет снисходительность к legacy; конфигурация NULL берёт безопасное умолчание |
| bFinal | пометка последнего сегмента - из суждения DONE слоя Body; всегда-истина/всегда-ложь одинаково ошибочны |
| Форма обратного вызова | текст проталкивается немедленно; стратегия буферизации ваша; декодер распределяет ноль |
| Семантика финала | отказ Done = аннулировать весь ответ; нижнее течение обязано уметь аннулировать (режим staging) |
| Сброс | Reset обнуляет и переключает в identity - переиспользование на многоответном соединении |
| Согласование содержимого | заявляйте только то, что реально декодируете; q целыми-в-тысячных; модуль encoding строит сторону запроса |
| Граница композиции | вверх - представление Data из Body, вниз - ваш обратный вызов - ноль копий насквозь |
| Обрезка | XRT_MODULE_HTTP_DECODE зависит от COMPRESS; не декодирующие снимают в ноль |
📖 Справочники API в этой главе:http_decode (16 записей)http_encoding (19 записей)http1 (39 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки