Ориентация главы
После трёх автоматических поведений главы 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概念
Сборка и режимы вызова
Четыре режима на уровне вызова: 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 и потолки:
#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; }
Что только что произошло. (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 — протокольный слой разбора и суждения политики:
#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; }
Что только что произошло. (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 независимы).
Типичные ошибки
Симптом: установили 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; /* потолок одного захвата */
Симптом: приватный ресурс, запрошенный пользователем 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); /* приватная изоляция - активное решение вызывающего; инструмент за вас не угадывает */
Симптом: в офлайн-режиме (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 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки