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

Что свернул слой easy (глава 98)? Эта глава разворачивает и показывает.Построитель запросов (xhttprequest): явная сборка метод/URL/Header/тело/аутентификация; Do при отправке замораживает снимок — вызывающий может немедленно изменять или уничтожать исходный запрос;слой вызова: два рода дедлайнов в xhttpcalloptions (общий тайм-аут накрывает всю цепочку, тайм-аут простоя смотрит только на время без прогресса), токен отмены, потолок тела ответа;диагностика: машина состояний и метки времени xhttpcallresult.Info (монотонные часы, миллисекунды) — RequestWireBytes/ResponseWireBytes накапливают всю цепочку перенаправлений;пул соединений: фрагментация по origin (до 32 фрагментов), в каждом своя FIFO ожидания и LRU простоя, глобальная атомарная квота — переиспользование есть корень производительности. Четыре шестерни вместе и есть «среда выполнения» — каждая строка easy бегает по ним.

99.1引入

Три сценария выталкивают вас из easy вниз. Сценарий один: послать JSON POST с заголовком Authorization — три строки построителя, easy не умеет (намеренно). Сценарий два: пакет запросов к одному сервису — расходы нового TCP+TLS-рукопожатия на каждый запрос суть потолок пропускной способности; переиспользование пула снижает стоимость со второго раза до нуля рукопожатий. Сценарий три: медленный источник — общий тайм-аут ведает «сколько максимум ждать весь запрос», но источник, проталкивающий один байт в час, никогда не уложится в общий тайм-аут; тайм-аут простоя создан именно для такого «соединение живо, но прогресса нет».

Третий сценарий заслуживает лишнего шага мысли: зачем два тайм-аута? Общий тайм-аут — верхний предел ресурса (этот запрос занимает у меня максимум 30 секунд); тайм-аут простоя — детектор прогресса (передача движется — ждём; замерло на 10 секунд — бросаем). Приём байтов запроса и потребление байтов ответа оба обновляют счётчик простоя — «медленно, но движется» и «быстро, но замерло» тем самым различимы. xhttp сделал их ортогональной конфигурацией: Timeout и IdleTimeout независимы, нулевое значение наследует Client, XHTTP_CLIENT_TIMEOUT_NONE явно отключает.

99.2概念

Построитель запросов и семантика заморозки

xrtHttpRequestCreate(方法, URL) (метод, URL) создаёт изменяемый запрос; SetHeader/SetBytes (фиксированное байтовое тело, копируется один раз)/SetBody (ссылочное тело — файл, поток-производитель)/SetAuth (аутентификация) собирают по элементам. Семантика заморозки: до успешного возврата xrtHttpClientDo копируются запрос и конфигурация с семантикой значений, сохраняется ссылка на тело — после отправки исходный запрос можно немедленно изменять или уничтожать (независимость асинхронного исполнения гарантирует снимок). У Clone метод/URL/Header/Trailer полностью независимы, тело лишь добавляет ссылку — шаблон «изменил копию и отправил» стоит O(дельты).

Слой вызова: дедлайны, отмена и потолки

1ОтправкаDo замораживает снимок -> очередь -> исполнение на Worker -> DNS/набор номера/TLS/посылка/приём
2Общий тайм-аутот отправки накрывает всю цепочку (очередь/DNS/TCP/прокси/TLS/передачу)
3Тайм-аут простояот начала исполнения мерит «без прогресса» - передача/приём байтов обновляют
4ОтменаxrtHttpCallCancel из любого потока; отменённое до отправки сразу входит в конечное состояние отмены
5ПотолкиResponseBodyLimit ограничивает представление тела (до декодирования); Decompress.MaxBody - открытый текст

Уровни двух потолков надо различать: ResponseBodyLimit действует на представление тела после снятия HTTP/1-кадрирования и до декодирования Content-Encoding — сетевой ответ, попадание кэша и локально собранный Range-ответ делят один бюджет; открытый текст после декодирования ограничивается отдельно Decompress.MaxBody — содержимому с высокой степенью сжатия не обойти бюджет памяти (клиентская версия «обороны от бомб» главы 97). Долгоживущие соединения вроде SSE выбирают представление без потолка и опираются на структурные пределы Parser — «что ограничивать» решает структура содержимого, а не подрезка всех под одну гребёнку.

Диагностический снимок: все поля Info

xhttpcallresult.Info (прилагается к обратному вызову завершения; снимок исполнения можно скопировать и из любого потока): State (очередь/исполнение/неизменяемое конечное состояние — после публикации финала все поля заморожены, опоздавшая отмена не перепишет опубликованный результат), Phase (сохраняется фактическая стадия завершения), миллисекундные метки (TransportReady/RequestSent/FirstByte/Headers — монотонные часы, не достигнутые равны нулю), байтовая статистика (RequestWireBytes/ResponseWireBytes накапливают всю цепочку перенаправлений; ResponseBodyBytes — окончательно отданное тело, при автоматической распаковке пишется открытый текст), ReusedConnection (переиспользовано ли хоть на одном прыжке), Secure (шифрование текущего/финального прыжка). Эти поля и есть карта осмотра «где тормозит» — глава 137 по анализу производительности вернётся к ним.

Пул соединений: фрагментация по origin и честное ожидание

После включения http_client_pool соединениями управляют по origin (scheme+нечувствительный к регистру host+port+личность прокси):

  • Фрагментация: до 32 фиксированных origin-фрагментов по числу Worker, в каждом свой индекс/FIFO ожидания/LRU простоя/Timer очистки — Timer создаётся не на каждое соединение; между фрагментами координируется только атомарная квота, без горячей блокировки уровня Client.
  • Честность: ожидание внутри фрагмента — FIFO «раньше готов — раньше исполняется»; при возврате соединения ждущий того же origin принимает его напрямую; при исчерпании глобальной квоты выдача между фрагментами идёт по кругу — «фрагмент, спящий вечно», не существует.
  • Условия возврата в пул: граница сообщения цела, нет Connection: close, нет обновления, транспорт здоров — все четыре выполнены, только тогда переиспользование; HTTP/1 pipelining не используется (одно соединение — одна транзакция; инженерное исполнение обороны от smuggling главы 90).
  • Значения по умолчанию: активные не ограничены, глобально простаивающих 128, на origin простаивающих 8, очистка 90 секунд; нулевой потолок простоя = отключение переиспользования.
  • Эксплуатационный интерфейс: xrtHttpClientCloseIdle (снимает только простаивающие, не прерывая активные), xrtHttpClientStats (количества и монотонные счётчики, читаемые конкурентно).

Отправка — сразу валидация, финал — сразу заморозка

Создание Client валидирует статическую конфигурацию; отправка Do валидирует диапазоны конфигурации (перекрытие адреса отклоняется синхронно, обратный вызов не публикуется); конечное состояние однажды опубликовано и не переписывается — опоздавшие отмена/тайм-аут/сбой транспорта не перекрывают опубликованный результат. Эти три правила пригвождают и «ошибку конфигурации», и «сбой исполнения» к определённым моментам времени — при отладке ошибки больше не дрейфуют.

99.3示例

Первая полная программа: сборка построителем и интроспекция URL

Программа ниже из examples/http/client_request — явная сборка JSON POST:

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



/* 构建一条可交给 HTTP 客户端执行的 JSON 请求。 */
int main(void)
{
	xhttprequest* pRequest = xrtHttpRequestCreate(
		XRT_STR_LITERAL("POST"),
		XRT_STR_LITERAL("https://api.example.test/items?dry_run=1")
	);
	const xurl* pUrl;
	uint16 iPort;

	if ( (pRequest == NULL) ||
		!xrtHttpRequestSetHeader(
			pRequest,
			XRT_STR_LITERAL("Accept"),
			XRT_STR_LITERAL("application/json")
		) ||
		!xrtHttpRequestSetBytes(
			pRequest,
			(xbytesview){
				(cbytes)"{\"name\":\"xrt\"}",
				14
			},
			XRT_STR_LITERAL("application/json; charset=utf-8")
		) ) {
		xrtHttpRequestDestroy(pRequest);
		return 1;
	}
	pUrl = xrtHttpRequestUrl(pRequest);
	if ( !xrtUrlPort(pUrl, &iPort) ) {
		xrtHttpRequestDestroy(pRequest);
		return 1;
	}
	printf(
		"%.*s %.*s:%u\n",
		(int)xrtHttpRequestMethod(pRequest).Size,
		xrtHttpRequestMethod(pRequest).Data,
		(int)pUrl->Host.Size,
		pUrl->Host.Data,
		(unsigned)iPort
	);
	xrtHttpRequestDestroy(pRequest);
	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_request/main.c -lws2_32 -lbcrypt -ladvapi32 -liphlpapiPOST api.example.test:443

Что только что произошло. (1) HttpRequestCreate(方法, URL) (метод, URL) создаёт запрос одним шагом — URL в полной форме (scheme+host+query), внутри разбирается xurl главы 102. (2) SetHeader добавляет Accept, SetBytes копирует 14 байтов JSON-тела и объявляет тип — два обычных Set построителя. (3) xrtHttpRequestUrl одалживает разобранную структуру URL, xrtUrlPort берёт численный порт (443 — значение HTTPS по умолчанию восполняется здесь, семантика XURL_PORT_VALUE главы 102); печать доказывает способность построителя к интроспекции: метод, хост и порт — читаемые факты. (4) Финал Destroy — запрос и без отправки годится как «структурный держатель URL/метода». Реальная отправка: после xrtHttpClientDo(pClient, pRequest, NULL, on_done, pCtx) — немедленно Destroy исходного запроса; семантика заморозки избавляет от слежения «асинхронность ещё пользуется?».

Вторая полная программа: статистика пула и очистка простоя

Вторая программа из examples/http/client_pool — эксплуатационная грань среды выполнения:

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



/* 建立一个有界连接池,并读取初始并发统计。 */
int main(void)
{
	xnetengineconfig EngineConfig;
	xhttpclientconfig ClientConfig;
	xhttpclientstats Stats;
	xnetengine* pEngine;
	xhttpclient* pClient;

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

	xrtHttpClientConfigInit(&ClientConfig);
	ClientConfig.Pool.MaxConnections = 256;
	ClientConfig.Pool.MaxConnectionsPerOrigin = 32;
	ClientConfig.Pool.MaxWaiting = 1024;
	ClientConfig.Pool.MaxWaitingPerOrigin = 128;
	ClientConfig.Pool.MaxIdle = 64;
	ClientConfig.Pool.MaxIdlePerOrigin = 8;
	ClientConfig.Pool.IdleTimeout = INT64_C(30000);
	pClient = xrtHttpClientCreate(pEngine, &ClientConfig);
	if ( pClient == NULL ) {
		xrtNetEngineDestroy(pEngine);
		return 1;
	}
	if ( !xrtHttpClientStats(pClient, &Stats) ) {
		xrtHttpClientDestroy(pClient);
		xrtNetEngineDestroy(pEngine);
		return 1;
	}

	printf(
		"active=%zu idle=%zu waiting=%zu opened=%llu\n",
		Stats.ActiveConnections,
		Stats.IdleConnections,
		Stats.WaitingCalls,
		(unsigned long long)Stats.ConnectionsOpened
	);
	(void)xrtHttpClientCloseIdle(pClient);
	xrtHttpClientDestroy(pClient);
	if ( !xrtNetEngineDestroy(pEngine) ) {
		return 1;
	}
	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_pool/main.c -lws2_32 -lbcrypt -ladvapi32 -liphlpapi(示例校验连接池统计与空闲清理路径后正常退出)

Что только что произошло. (1) HttpClientStats(pClient, &Stats) читает текущие величины пула и монотонные счётчики жизненного цикла — поля не обещают один глобальный момент (слабая согласованность конкурентного чтения); ActiveConnections включает соединения в наборе номера/использовании/зарезервированной квоте. (2) HttpClientCloseIdle снимает только простаивающие — не прерывая активные вызовы; финал жизненного цикла ещё ждёт асинхронного Close и отменяющих обратных вызовов Timer фрагмента («снят с LRU» ≠ «полностью закрыто» — различение двух моментов и есть честность этого API). (3) Конфигурационная грань: потолки простоя/ожидания/активности xhttpclientpoolconfig — параметры развёртывания; 8 простаивающих на origin хватает большинству API-клиентов; сервисам с ограниченным числом восходящих соединений снизьте глобальный потолок. Статистика → подстройка → снова статистика — это цикл ёмкости.

99.4契约

  • Семантика заморозки: копирование завершается до успешного возврата Do; после отправки исходный запрос можно менять и уничтожать; Clone — дельта-копия (тело добавляет ссылку).
  • Два рода дедлайнов: Timeout — вся цепочка от отправки; IdleTimeout — время без прогресса от начала исполнения (приём/передача обновляют); ноль наследует Client, TIMEOUT_NONE явно выключает; одновременное наступление устойчиво сообщает TOTAL.
  • Отмена: CallCancel из любого потока; отменённое до отправки — без Timer, без DNS, сразу финал; после публикации финала не переписывается.
  • Двойной потолок: ResponseBodyLimit (представление тела, до декодирования, кэш в том же бюджете) + Decompress.MaxBody (открытый текст) — высокой степени сжатия не обойти бюджет; SSE заменяет потолок загрузки структурными пределами.
  • Заморозка Info: поля финала заморожены; время — монотонные часы в миллисекундах, не достигнутое — ноль; поля Wire накапливают цепочку перенаправлений; Body пишет окончательный открытый текст.
  • Фрагментация пула: ≤32 фиксированных фрагментов; внутри фрагмента FIFO/LRU/Timer; между фрагментами атомарная квота; четыре условия возврата; без pipelining; по умолчанию 128/8/90s.
  • Эксплуатация пула: CloseIdle не прерывает активные; Stats слабо согласован и читается конкурентно; Drain/Abort идут одним путём и ждут асинхронного завершения.
  • Валидация при отправке: перекрытие диапазонов конфигурации отклоняется синхронно без публикации обратного вызова; при создании проверяются статические конфигурации Dial/Stream/Exchange.
  • Обрезка: HTTP_CLIENT_POOL/_FUTURE/_PREPARE/_STREAM независимы — обрезайте по нужным граням.

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

Ловушка 1: продолжать менять исходный запрос после отправки, надеясь влиять на идущий вызов:Ловушка 1: продолжать менять исходный запрос после отправки, надеясь влиять на идущий вызов

Симптом: после Do ещё раз SetHeader — идущий запрос «не получил» новый заголовок; кажется, в библиотеке баг.

Причина: семантика заморозки — в момент успешного возврата Do снимок уже определён; последующие изменения принадлежат «следующей отправке». Это возможность, а не дефект: иначе каждая отправка ждала бы, пока вызывающий объявит «я закончил менять».

错误写法
pCall = xrtHttpClientDo(pClient, pReq, NULL, on_done, pCtx);
xrtHttpRequestSetHeader(pReq, Extra, Value);  /* идущий не изменить */
正确写法
pCall = xrtHttpClientDo(pClient, pReq, NULL, on_done, pCtx);
xrtHttpRequestDestroy(pReq);   /* миссия исходного запроса окончена */
/* новая потребность? новый запрос и новая отправка - семантика снимка изолирует конкурентность */
Ловушка 2: только общий тайм-аут — медленно тянущее соединение выжимает лимит:Ловушка 2: только общий тайм-аут — медленно тянущее соединение выжимает лимит

Симптом: общий тайм-аут 30 секунд, источник проталкивает байт каждые 20 секунд — каждый запрос «законно» выжимает 30 секунд; пропускную способность душат такие источники.

Причина: общий тайм-аут смотрит на длительность, не на прогресс. Детектор прогресса — именно тайм-аут простоя: «соединение живо, но не движется» 10 секунд — пора бросать.

错误写法
xrtHttpClientConfigInit(&Config);
Config.Timeout = 30000000ull;   /* только общий: источник «байт/20s» тянет весь срок */
正确写法
xrtHttpClientConfigInit(&Config);
Config.Timeout = 30000000ull;        /* общий бюджет 30s */
Config.IdleTimeout = 10000000ull;    /* 10s без прогресса - бросаем */
/* приём/передача оба обновляют простой - «медленно, но движется» не страдает, «замерло» быстро отсекается */
Ловушка 3: принимать Stats за точный снимок для решений о ёмкости:Ловушка 3: принимать Stats за точный снимок для решений о ёмкости

Симптом: деля ActiveConnections + IdleConnections на мгновенный QPS, получают плавающие выводы.

Причина: поля Stats не обещают один момент времени — при конкурентном чтении поля из разных мгновений. Решения о ёмкости делайте по монотонным счётчикам жизненного цикла (накопленные создания/разрушения/переиспользования) — по трендам, а не по мгновенным отношениям.

错误写法
double load = (double)Stats.ActiveConnections /
	(Stats.ActiveConnections + Stats.IdleConnections);  /* мгновенная смесь: дрейф */
正确写法
/* счётчики жизненного цикла монотонны: две выборки дают скорость/долю переиспользования - стабильно сравнимо */
double reuse_rate = (double)Stats.ReusedConnections /
	(double)(Stats.CreatedConnections + 1);

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

Базовое: полная сборка построителем

Построителем отправьте POST с заголовком Authorization и JSON-телом (на локальный или httpbin-сервис); в обратном вызове завершения напечатайте статус и Info.Phase. Критерии приёмки: немедленное уничтожение исходного запроса после отправки не влияет на результат; тело ответа и Header полностью читаемы.

Продвинутое: эксперимент с двумя тайм-аутами

Поднимите медленный сервис (байт в секунду) и зависший сервис (принимает соединение, не шлёт данных); тремя конфигурациями — только общий тайм-аут, только простой, оба — измерьте время отказа. Критерии приёмки: времена отказа трёх конфигураций сходятся с выводом контракта; вы можете указать различие стадий завершения в Info.Phase.

Вызов: эталон переиспользования соединений

На один origin отправьте 100 запросов (параллелизм 8); по счётчикам жизненного цикла Stats посчитайте долю переиспользования и число рукопожатий; затем отключите пул через ClientConfig (нулевой потолок простоя) и сравните. Критерии приёмки: три набора данных — доля переиспользования, общее время, частота попаданий Info.ReusedConnection — самосогласованы; вы можете объяснить, откуда разница времени после отключения пула (TCP+TLS-рукопожатия × число).

Итоги

ТемаКратко
ПостроительCreate(метод,URL) + SetHeader/SetBytes/SetBody/SetAuth; Do замораживает снимок
Семантика заморозкипосле отправки исходный запрос можно менять и уничтожать; Clone — дельта (тело добавляет ссылку)
Два дедлайнаTimeout — вся цепочка (от отправки); IdleTimeout — без прогресса (приём/передача обновляют); ноль наследует/NONE выключает
Отменаиз любого потока; отмена до отправки — сразу финал; финал заморожен и не переписывается
Двойной потолокпредставление тела (до декодирования, кэш в том же бюджете) + открытый текст (после распаковки) — два шлюза против бомб
Infoфинал заморожен; миллисекунды монотонных часов; Wire накапливает цепочку перенаправлений; Body — байты открытого текста
Фрагментация пула≤32 фиксированных фрагментов; внутри фрагмента FIFO/LRU/Timer; между фрагментами атомарная квота без горячей блокировки
Условия возвратаграница цела + нет close + нет обновления + здоров; без pipelining; по умолчанию 128/8/90s
ЭксплуатацияCloseIdle не прерывает активные; Stats слабо согласован — ёмкость по монотонным счётчикам
ИнтроспекцияRequestUrl/Method читаемы; UrlPort восполняет порт по умолчанию — запрос тоже структурный держатель

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

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