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

После трёх автоматических поведений главы 100 кэш — самое большое и самое колоритное из них. Слой http_client_cache после подключения Store (xhttpcache) в конфигурацию Client делает поиск, доставку при попадании, оценку свежести, условную валидацию, сохранение, сборку частичных ответов и инвалидацию unsafe-методов полностью автоматически — ваш код не меняется ни на строку, второй запрос того же URL выходит из локального хранилища. Протокольная сторона полностью следует RFC 9111: Cache-Control запроса (no-store/no-cache/only-if-cached/возрастные ограничения) и хранимость ответа обрабатываются единым публичным парсером политики кэша — клиент не изобретает приближённых семантик. Объекты хранения остаются открытыми — приложение, прокси, собственный персистентный бэкенд используют тот же контракт Store. Глава разбирает конфигурацию, четыре режима вызова, контракт хранения и условную конкурентную фиксацию.

101.1引入

Зачем клиенту встроенный кэш, а не «бизнес сам сохранит ответы»? Три причины.Корректность: суждение о хранимости (no-store/Vary: */ответы с аутентификацией/правила общих и приватных), расчёт свежести (max-age/Expires/поправка возраста), условная валидация (слияние 304 по ETag/Last-Modified) — каждое тонкое правило RFC; самописное «положу в Map, истеку по времени» на реальных заголовках ответов ошибается кто во что горазд.Компонуемость: сборка частичного содержимого Range-запросов (локальные фрагменты + дозапрос к источнику) требует согласованности со структурой хранения — снаружи не навесить.Инвалидация: после успешного unsafe-метода инвалидация связанных представлений того же источника (Location/Content-Location) — часть протокола кэша. xhttp реализует этот свод правил один раз — сконфигурировал Store, выиграли все.

101.2概念

Сборка и режимы вызова

1СборкаxrtHttpCacheCreate -> ClientConfig.Cache.Store -> после Create свою ссылку можно отпустить
2Прибыл запросопределение режима (четыре режима) -> поиск в кэше (главный ключ = метод + действующий URI + раздел + объявление Vary)
3Попадание и свежестьпрямая доставка (без выхода в сеть)
4Попадание, но истеклоусловная валидация (ETag/If-Modified-Since) -> при 304 слияние записи и доставка
5Промах/выключенок источнику -> ответ по хранимости решает, входить ли в Store

Четыре режима на уровне вызова: XHTTP_CLIENT_CACHE_DEFAULT (соблюдать политику запроса/ответа/хранения — обычный путь), DISABLED (на этот раз обойти чтение, запись и инвалидацию), RELOAD (принудительная валидация у источника или перезагрузка; успешный ответ всё же может обновить кэш — кнопка «обновить»), ONLY (сеть запрещена; без записи синтезируется 504 — офлайн-режим). Четыре режима складываются с семантикой заголовков запроса без конфликта — заголовок only-if-cached обрабатывает тот же парсер политики.

Контракт хранения: что входит в Store

  • По умолчанию приватный кэш; Shared явно переключает общую семантику (поля и правила авторизации различаются).
  • Не входит в Store: no-store, нехранимые коды состояния, Vary: *, запрещённые к общему хранению аутентифицированные/приватные ответы.
  • Эвристика свежести: без явной свежести можно включить ограниченную эвристику по Last-Modified — по умолчанию 10%, максимум сутки, можно выключить целиком (консервативный дефолт, приватность и корректность прежде всего).
  • Рост по надобности: кодированное тело растёт только после реального прибытия, на каждый Call ничего не резервируется; MaxBody (по умолчанию 8 MiB) — жёсткий потолок захвата одного дозапроса — сверх него захват прекращается, но по умолчанию fail-open (сетевой ответ продолжает доставляться, просто не кэшируется); малые тела стартуют с 256 Б, удваиваясь.
  • Дисциплина освобождения: после сохранения или повторной выдачи кандидат-запись, снимки полей и захваченное тело освобождаются до публикации финала — не живут в удерживаемом вами результате.

Условная фиксация и конкурентность

Слияние 304, обновление метаданных HEAD, сборка Range-фрагментов — без обычного перезаписывающего Put — они используют текущую версию Store Replace/RemoveRecord: при конфликте фиксации 304 (победил конкурентный обновлятель) текущий запрос всё равно доставляет собственный проверенный снимок, а кэш сохраняет результат конкурентного обновлятеля — не терять обновление ради сохранения своего взгляда. Этот оптимистический параллелизм — та же мысль, что «правила инвалидации» контейнеров главы 18: изменили после вашего чтения — условие записи не сошлось, каждый идёт своей дорогой.

Сборка Range и инвалидация unsafe

Локальная сборка Range: запрошен Range: bytes=0-99, а локально есть запись 0-199 — режется прямо из кэша (без выхода в сеть); если локально только фрагмент, сложность «локальный фрагмент + дозапрос недостающего» ограничивается MaxRanges (по умолчанию 16) — сортировочная работа, временные массивы и multipart-разрастание ограничены.Инвалидация unsafe: успешные POST/PUT/DELETE инвалидируют целевой URI вместе с односайтовыми Location/Content-Location представлениями, которым ответ разрешил — согласованность «записал-прочитал» гарантируется протоколом, а не напоминанием приложения.

Ключ раздела

PartitionKey копируется при фиксации и служит изоляции одного URI по пользователям/сайтам/арендаторам — главный ключ содержит раздел.Почему нельзя полагаться только на Vary: Vary различает по заголовкам запроса (например Vary: Authorization), но «какие заголовки различать» объявляет отвечающий — не объявил, и доступ общ shared; ключ раздела — активная изоляция вызывающего (базовая линия многотенантного клиента). Изоляция приватных данных — решение вызывающего; протокольный инструмент лишь инструмент.

101.3示例

Первая полная программа: сборка кэша и конфигурация ёмкости

Программа ниже из examples/http/client_cache — подключение Store и потолки:

extlibs/xhttp/examples/http/client_cache/main.c源码 ↗
#include <stdio.h>
#include <xrt/http_client_runtime.h>



/* 配置高层 HTTP Client 使用共享缓存,并展示调用级分区与只读模式。 */
int main(void)
{
	xnetengineconfig EngineConfig;
	xhttpclientconfig ClientConfig;
	xhttpcalloptions CallOptions;
	xhttpcache* pCache;
	xnetengine* pEngine;
	xhttpclient* pClient;

	pCache = xrtHttpCacheCreate(NULL);
	if ( pCache == NULL ) {
		return 1;
	}
	xrtNetEngineConfigInit(&EngineConfig);
	pEngine = xrtNetEngineCreate(&EngineConfig);
	if ( (pEngine == NULL) || !xrtNetEngineStart(pEngine) ) {
		xrtNetEngineDestroy(pEngine);
		xrtHttpCacheRelease(pCache);
		return 2;
	}

	xrtHttpClientConfigInit(&ClientConfig);
	ClientConfig.Cache.Store = pCache;
	ClientConfig.Cache.MaxBody = 16u * 1024u * 1024u;
	ClientConfig.Cache.Strict = false;
	pClient = xrtHttpClientCreate(pEngine, &ClientConfig);
	xrtHttpCacheRelease(pCache);
	if ( pClient == NULL ) {
		xrtNetEngineDestroy(pEngine);
		return 3;
	}

	xrtHttpCallOptionsInit(&CallOptions);
	CallOptions.Cache.Mode = XHTTP_CLIENT_CACHE_ONLY;
	CallOptions.Cache.PartitionKey =
		XRT_STR_LITERAL("tenant:example");
	printf(
		"cache=%s mode=%u partition=%.*s\n",
		xrtHttpClientCache(pClient) != NULL ?
			"enabled" : "disabled",
		(unsigned int)CallOptions.Cache.Mode,
		(int)CallOptions.Cache.PartitionKey.Size,
		CallOptions.Cache.PartitionKey.Data
	);

	xrtHttpClientDestroy(pClient);
	if ( !xrtNetEngineDestroy(pEngine) ) {
		return 4;
	}
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -DXHTTP_MODULE_ALL -I single -I single/extlibs -include xrt.h -include xhttp.h impl.c extlibs/xhttp/examples/http/client_cache/main.c -lws2_32 -lbcrypt -ladvapi32 -liphlpapi(示例校验缓存装配与回源-命中路径后正常退出)

Что только что произошло. (1) xrtHttpCacheCreate(NULL) создаёт Store в памяти (NULL берёт конфигурацию по умолчанию — ёмкость и вытеснение в xhttpcacheconfig); кладётся в ClientConfig.Cache.Store — Client с создания держит ссылку. (2) Cache.MaxBody = 16 MiB перекрывает умолчальные 8 MiB — потолок захвата одного дозапроса есть якорь планирования ёмкости: он решает, «какой величины ответ стоит кэшировать», в паре с общей ёмкостью Store (другая конфигурация). (3) Дальнейший код запроса неотличим от бескэшевого — попадание без выхода в сеть, истекшее автоматически валидируется; нулевые Wire-байты в Info и есть попадание (так диагностическая грань опознаёт cache hit). (4) HttpClientCache(pClient) одалживает тот же дескриптор (статистика, явное вытеснение); без Store слой вовсе не входит в горячий путь.

Вторая полная программа: инспекция политики кэша

Вторая программа из examples/http/cache_policy — протокольный слой разбора и суждения политики:

extlibs/xhttp/examples/http/cache_policy/main.c源码 ↗
#include <xrt/http_cache_policy.h>

#include <stdio.h>
#include <string.h>



/* 展示共享缓存对带 Authorization 的新鲜响应作出存储和复用计划。 */
int main(void)
{
	static const xhttpfield ResponseFields[] = {
		{
			XRT_STR_INIT("Cache-Control"),
			XRT_STR_INIT("public, max-age=60")
		}
	};
	xhttpcachecontrol Request;
	xhttpcachecontrol Response;
	xhttpcachetime Time;
	xhttpcacheage Age;
	xhttpcachefreshness Freshness;
	xhttpcachestoreinput StoreInput;
	xhttpcachestoreplan StorePlan;
	xhttpcacheuseinput UseInput;
	xhttpcacheuseplan UsePlan;

	xrtHttpCacheControlInit(&Request);
	xrtHttpCacheTimeInit(&Time);
	memset(&Age, 0, sizeof(Age));
	Freshness.Lifetime = UINT64_C(60000000);
	Freshness.Source = XHTTP_CACHE_FRESHNESS_MAX_AGE;
	if ( !xrtHttpCacheControlParse(
		ResponseFields, 1, &Response
	) || !xrtHttpCacheStoreInputInit(
		&StoreInput,
		XRT_STR_LITERAL("GET"),
		200,
		true
	) ) {
		return 1;
	}
	StoreInput.Flags |=
		XHTTP_CACHE_STORE_AUTHORIZATION;
	if ( xrtHttpCacheStorePlan(
		&Request, &Response, &Time,
		&StoreInput, &StorePlan
	) != XHTTP_CACHE_STORE_KEEP ) {
		return 1;
	}
	if ( !xrtHttpCacheUseInputInit(
		&UseInput, 200, true
	) || (xrtHttpCacheUsePlan(
		&Request, &Response,
		&Age, &Freshness,
		&UseInput, &UsePlan
	) != XHTTP_CACHE_USE_STORED) ) {
		return 1;
	}
	printf(
		"store=%d use=%d actions=0x%08x\n",
		(int)StorePlan.Decision,
		(int)UsePlan.Decision,
		(unsigned int)UsePlan.Actions
	);
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -DXHTTP_MODULE_ALL -I single -I single/extlibs -include xrt.h -include xhttp.h impl.c extlibs/xhttp/examples/http/cache_policy/main.c -lws2_32 -lbcrypt -ladvapi32 -liphlpapi(输出策略解析与可存储性判定的自检结果)

Что только что произошло. (1) Политика кэша — независимый протокольный модуль (http_cache_policy): директивы Cache-Control запроса, хранимость ответа, свежесть и возраст — слой клиентского кэша лишь его пользователь (тот же паттерн, что переиспользование плана тела главы 91 и слоя согласования сжатия главы 97). (2) Пример валидирует суждения политики офлайн: даны заголовки запроса/ответа — хранимо? насколько свежо? нужна ли валидация? — каждый шаг кэш-поведения — чистая функция, пригодная для юнит-теста; это инженерная гарантия «автоматика не выходит из-под контроля». (3) Семья инспекций: cache_control (разбор директив), cache_time (расчёт возраста), cache_validate (выбор валидатора), cache_range (сборка Range), cache_store (контракт хранения), cache_status (генерация статусного заголовка) — шесть протокольных модулей держат автоматический кэш, все независимо обрезаются.

101.4契约

  • Сборка: Store в конфигурацию Client — и всё автоматически (поиск/доставка/валидация/хранение/сборка/инвалидация); Client держит ссылку; без Store слой не входит в горячий путь.
  • Четыре режима: DEFAULT/Disabled/Reload/Only (без записи синтезируется 504); с семантикой заголовков запроса унифицированы одним парсером, без самодеятельных приближений.
  • Главный ключ: метод + действующий URI + ключ раздела + фактически объявленные записью поля Vary.
  • Не хранится: no-store/незранимые коды состояния/Vary:*/запрещённые к общему хранению аутентифицированные приватные ответы.
  • Эвристика: по умолчанию 10%·Last-Modified·максимум сутки·можно выключить.
  • Потолки: MaxBody — жёсткий потолок захвата одного дозапроса (по умолчанию 8 MiB), сверх — fail-open; MaxRanges по умолчанию 16 ограничивает сложность сборки; рост по надобности, без резервирования.
  • Условная фиксация: 304/HEAD/Range через версионное условие Replace — при конфликте доставляется свой снимок, сохраняется конкурентный обновлитель.
  • Инвалидация: успех unsafe инвалидирует целевой URI + разрешённые ответом односайтовые Location/Content-Location.
  • Разделы: PartitionKey копируется при фиксации; приватная изоляция — разделом, не Vary.
  • Обрезка: HTTP_CLIENT_CACHE зависит от общего семейства протокольных модулей кэша (policy/control/time/validate/range/store/status независимы).

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

Ловушка 1: принять MaxBody за общую ёмкость Store:Ловушка 1: принять MaxBody за общую ёмкость Store

Симптом: установили MaxBody = 16 MiB и решили, что кэш займёт максимум 16 MiB — через часы работы память растёт до сотен MiB.

Причина: MaxBody — потолок захвата одного дозапроса (насколько велики записи, которые ещё кэшируются); общая ёмкость Store и вытеснение — в xhttpcacheconfig (конфигурация при создании Store). Две ручки ведают двумя разными вещами.

错误写法
ClientConfig.Cache.MaxBody = 16u * 1024u * 1024u;   /* решили, что это общая ёмкость */
pCache = xrtHttpCacheCreate(NULL);                    /* общая ёмкость по умолчанию не тронута */
正确写法
xhttpcacheconfig CacheConfig;
xrtHttpCacheConfigInit(&CacheConfig);
CacheConfig.MaxBytes = 64u * 1024u * 1024u;   /* общая ёмкость Store + вытеснение */
pCache = xrtHttpCacheCreate(&CacheConfig);
ClientConfig.Cache.Store = pCache;
ClientConfig.Cache.MaxBody = 16u * 1024u * 1024u;   /* потолок одного захвата */
Ловушка 2: многопользовательский клиент делит кэш без разделов:Ловушка 2: многопользовательский клиент делит кэш без разделов

Симптом: приватный ресурс, запрошенный пользователем A, при том же URL попадает напрямую пользователю B — межпользовательская утечка данных.

Причина: главный ключ кэша по умолчанию не содержит пользовательского измерения; Vary: Authorization действует, только если отвечающий объявил — не объявил, и доступ общ. Многопользовательским/многотенантным обязательно задавайте ключ раздела.

错误写法
/* все пользователи - один Client и один Store, без разделов */
pClient = xrtHttpClientCreate(pEngine, &Config);   /* B попадает в приватный ответ A */
正确写法
/* на пользователя (или арендатора) свой ключ раздела - главный ключ содержит раздел */
xrtHttpCallOptionsInit(&Options);
Options.Cache.PartitionKey = user_partition(user);
pCall = xrtHttpClientDo(pClient, pReq, &Options, on_done, pCtx);
/* приватная изоляция - активное решение вызывающего; инструмент за вас не угадывает */
Ловушка 3: обрабатывать синтетический 504 режима ONLY как сетевую ошибку:Ловушка 3: обрабатывать синтетический 504 режима ONLY как сетевую ошибку

Симптом: в офлайн-режиме (CACHE_ONLY) без записи возвращается 504 — код идёт по пути «шлюз упал» с алертом.

Причина: 504 — синтезированная локальная семантика: «сеть запрещена, кэша нет» — это не сетевая ошибка, а детерминированный результат офлайн-политики. Обработка должна идти в ветку «офлайн, нет содержимого» (подсказка/деградированный UI), не в повтор или алерт.

错误写法
if ( status == 504 ) {
	alert_gateway_down();   /* офлайн-промах принят за сетевой сбой */
}
正确写法
if ( offline_mode && status == 504 ) {
	show_offline_empty(url);   /* локальный синтетический 504: офлайн и без кэша */
}

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

Базовое: сверка попадания и дозапроса

Дважды подряд запросите один URL локального сервиса: сравните Info.ResponseWireBytes второго (должно быть 0 — попадание) и возрастную информацию в заголовках ответа. Затем третий раз в режиме RELOAD. Критерии приёмки: Wire-байты трёх прогонов согласуются с семантикой режимов; тело попадания байт-в-байт совпадает с первоисточником.

Продвинутое: наблюдение условной валидации

Сервер возвращает ответ с ETag; после истечения кэша (или принудительного RELOAD) запросите снова — захватом пакетов или журналом сервера убедитесь: второй запрос несёт условные заголовки, ответ 304, клиент доставляет слитое полное представление (код состояния 200, не 304). Критерии приёмки: вызывающий никогда не видит внутренний 304 (проверка контракта); тело попадания совпадает с первоисточником.

Вызов: офлайн-первый клиент

Реализуйте fetch_offline_fallback(Url): сначала CACHE_ONLY (попадание — используем); при 504 откат к обычному режиму с выходом в сеть и записью в кэш. Протестируйте весь процесс с выключенной сетью. Критерии приёмки: с кэшем офлайн работает; без кэша — явная офлайн-ошибка (различайте «сеть отключена» и «сервер 5xx»); запись в кэш соблюдает хранимость ответа (no-store не попадает в хранилище — проверяется последующим офлайн-поведением).

Итоги

ТемаКратко
СборкаStore в конфигурацию Client — всё автоматически; Client держит ссылку; без Store ноль накладных
Четыре режимаDEFAULT / Disabled / Reload (сильный источник всё же обновляет) / Only (без записи синтетический 504)
Главный ключметод + действующий URI + раздел + объявление Vary; раздел — средство изоляции вызывающего
Не хранитсяno-store/незранимые коды/Vary:*/запрещённая общая аутентифицированная приватность
ЭвристикаLast-Modified 10%·потолок сутки·можно выключить (консервативный дефолт)
Две ручки ёмкостиMaxBody = один захват (сверх — fail-open); конфигурация Store = общая ёмкость + вытеснение
Условная фиксация304/HEAD/Range через версионное условие Replace; при конфликте сохраняется конкурентный обновлитель
Сборка Rangeлокальный фрагмент + дозапрос недостающего; MaxRanges по умолчанию 16 ограничивает сложность
Инвалидацияуспех unsafe -> целевой URI + односайтовые Location/Content-Location
Семейство протоколовpolicy/control/time/validate/range/store/status независимо обрезаются и тестируются

📖 Справочники API в этой главе:xhttp-http_cache (10 записей)xhttp-http_client (101 записей)

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