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

Глава 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 и обратный вызов

1ПрибытиеBodyRead выплёвывает сжатое представление Data (глава 91)
2ПодачаDecodeWrite(декодер, Data, последний ли сегмент, обратный вызов, контекст)
3Проталкиваниедекодированный отрезок текста немедленно входит в обратный вызов (ноль накопления) - бизнес обрабатывает по прибытии
4ФиналDecodeDone сверяет CRC и хвостовую длину gzip - против тихой порчи

Параметр 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 даёт разбор и построение на стороне запроса.

Панорама композиции: конвейер потребления одного полного ответа

1ЗаголовокResponseParse (глава 90) -> массив полей
2КадрированиеResponsePlan + потоковое чтение Body (глава 91) -> сжатые представления Data
3ДекодированиеDecodeCreate (из заголовка) -> Write по сегментам -> обратный вызов с текстом
4Потреблениев обратном вызове бизнес (инкрементальный разбор JSON / на диск / пересылка)
5СверкаDecodeDone + BodyDone - двойное подтверждение содержимого и границы

Четыре слоя, каждый на своём посту, между слоями только представления и обратные вызовы — это полный скелет, собранный первыми пятью главами тома 9; рантайм xhttp главы 98 упакует его в высокоуровневое API.

93.3示例

Первая полная программа: от заголовка ответа к обратному вызову с текстом

Программа ниже из examples/http/decode/main.c — поток gzip на 42 байта декодирован целиком:

examples/http/decode/main.c源码 ↗
#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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/http/decode/main.c -lws2_32 -liphlpapihello compressed world

Что только что произошло. (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 — три строки покрывают режимы, контентный режим и сброс:

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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/http/decode_tour/main.c -lws2_32 -liphlpapidecode: config compat vs safe + create okdecode: gzip content mode + input/output sizes okdecode: reset to identity passthrough ok

Что только что произошло. (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); вниз — ваш обратный вызов: ноль копий насквозь.

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

Ловушка 1: bFinal постоянно true (или постоянно false):Ловушка 1: bFinal постоянно true (или постоянно false)

Симптом: постоянно 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);   /* сверка слоя декодирования после закрытия слоя кадрирования */
Ловушка 2: после отказа Done «использовать уже вытолкнутую часть»:Ловушка 2: после отказа Done «использовать уже вытолкнутую часть»

Симптом: проверка 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);
}
Ловушка 3: Accept-Encoding заявляет br, а в сборке нет brotli:Ловушка 3: Accept-Encoding заявляет br, а в сборке нет brotli

Симптом: сервер по согласованию выбирает 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 записей)

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