Ориентация главы
Что свернул слой 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(дельты).
Слой вызова: дедлайны, отмена и потолки
Уровни двух потолков надо различать: 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:
#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; }
Что только что произошло. (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 — эксплуатационная грань среды выполнения:
#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; }
Что только что произошло. (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независимы — обрезайте по нужным граням.
Типичные ошибки
Симптом: после 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); /* миссия исходного запроса окончена */ /* новая потребность? новый запрос и новая отправка - семантика снимка изолирует конкурентность */
Симптом: общий тайм-аут 30 секунд, источник проталкивает байт каждые 20 секунд — каждый запрос «законно» выжимает 30 секунд; пропускную способность душат такие источники.
Причина: общий тайм-аут смотрит на длительность, не на прогресс. Детектор прогресса — именно тайм-аут простоя: «соединение живо, но не движется» 10 секунд — пора бросать.
xrtHttpClientConfigInit(&Config); Config.Timeout = 30000000ull; /* только общий: источник «байт/20s» тянет весь срок */
xrtHttpClientConfigInit(&Config); Config.Timeout = 30000000ull; /* общий бюджет 30s */ Config.IdleTimeout = 10000000ull; /* 10s без прогресса - бросаем */ /* приём/передача оба обновляют простой - «медленно, но движется» не страдает, «замерло» быстро отсекается */
Симптом: деля 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 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки