Ориентация главы
Том 9 дал вам все детали HTTP (разбор, кадрирование, поля, декодирование); xhttp тома 10 собирает их в высокоуровневый клиент — эта глава начинает с самого тонкого слоя: слоя удобства easy. xrtHttpClientGetSync(客户端, URL, NULL) (клиент, URL, NULL) выполняет запрос-ответ одной строкой; ключ в том, что за этой строкой нет скрытого Engine, Client или второй реализации запроса — она лишь создаёт временный запрос и передаёт его xrtHttpClientDo() на заморозку; тайм-ауты, отмена, перенаправления, Cookie, кэш, прокси, TLS, распаковка, диагностика и цепочка ошибок полностью совпадают с низкоуровневым входом. Пройдя главу, вы получаете «кратчайший рабочий путь» и ещё одно суждение: когда оставаться на слое easy, а когда спускаться к построителю запросов главы 99.
98.1引入
Написать утилиту «взять URL и напечатать код состояния»: в эпоху curl это одна команда; в мире C-библиотек обычно тридцать строк сборки (Engine, Client, запрос, обратный вызов, ожидание, очистка). Слой easy сжимает это до трёх ключевых строк — но проектный вопрос лежит вне «сжатия»: у большинства библиотек слой удобства — другой путь кода, поведение тихо расходится с низкоуровневым входом (easy не поддерживает перенаправления, другие коды ошибок, нет диагностики). Позиция xhttp: слой удобства — только синтаксический сахар; http_client_easy на время вызова создаёт временный xhttprequest, передаёт Do на заморозку и уничтожает до возврата — любой вызов easy всегда можно механически переписать в форму построителя, поведение не изменится.
Существование этого слоя делает кривую обучения xhttp трёхступенчатой: эта глава (однострочный вызов) → глава 99 (построители и полная картина среды выполнения) → глава 104 (сервер).
98.2概念
Три группы входов удобства и три формы завершения
| Вход | Семантика |
|---|---|
xrtHttpClientGet(客户端, URL, 选项, 回调, 上下文) (клиент, URL, опции, обратный вызов, контекст) | GET без тела (пустое тело искусственно не создаётся) |
xrtHttpClientPost(客户端, URL, 字节, 类型, 选项, 回调, 上下文) (клиент, URL, байты, тип, опции, обратный вызов, контекст) | копирует фиксированное байтовое тело |
xrtHttpClientSendBytes(...) | PUT/PATCH/любой метод с фиксированным байтовым телом |
Базовая форма — обратный вызов; после включения http_client_easy_future одноимённые входы получают суффиксы Async (возвращает xfuture) и Sync (блокирующе возвращает xhttpresult*). Синхронному входу запрещено блокировать на сетевом Worker — он ждёт конечное состояние в вызывающем потоке; вызов на Worker даёт взаимоблокировку — та же дисциплина, что и синхронное ожидание Future в главе 81.
GET без тела и POST с пустым телом: семантическая граница
Слой удобства намеренно различает два «нет содержимого»: у GET нет тела (Content-Length/Content-Type не создаются); POST/SendBytes с пустым байтовым представлением означает явное тело нулевой длины (создаётся Content-Length: 0). Различие идёт от семантики протокола — тело GET и так не определено, а пустое тело POST легально и однозначно. Точно так же при пустом ContentType поле Content-Type не создаётся — никакого вранья.
Граница слоя удобства: что он намеренно не принимает
Header, потоковое тело, формы, аутентификация, Upload — входы удобства намеренно этого не принимают. Когда оно нужно — спускайтесь: xrtHttpRequestCreate() + xrtHttpRequestSetBody() + xrtHttpClientDo*() — низкоуровневые входы всегда открыты и стоят рядом со слоем удобства (глава 99). Эта «намеренность» — дизайн: таблица параметров слоя удобства помещается в одну строку; сложные потребности выражаются явным построителем, без промежуточного состояния вроде «скрытой конфигурационной структуры слоя удобства».
Объект результата: снимок только для чтения
xhttpresult — снимок только для чтения, передаваемый вызывающему после завершения: xrtHttpResultResponse(结果) (результат) одалживает xhttpresponse (версия/статус/фраза/Header/trailer/итоговый действующий URL — точка приземления после перенаправлений, глава 100), xrtHttpResponseBody(响应) (ответ) — непрерывное представление тела, xrtHttpResultDestroy освобождает. Тело растёт по надобности, фиксированный буфер не резервируется; при потоковом выполнении (глава 99) буфер тела вовсе не выделяется, считаются только байты. Результат после использования обязательно Destroy — это единственный владеющий объект, которым вам нужно управлять в этой главе.
Минимальная сборка
Три фиксированных шага перед вызовом easy: xrtNetEngineConfigInit/Create/Start (Engine) → xrtHttpClientConfigInit + xrtHttpClientCreate(引擎, 配置) (движок, конфигурация) (Client) → вызов удобства. Создание Client сразу валидирует статическую конфигурацию Dial/Stream/Exchange/частного Resolver — неверная политика падает на месте, не откладываясь до первого запроса (та же философия «валидации при старте», что и идентичность главы 83). Порядок очистки как в главе 66: сначала Client, потом Engine; Destroy движка может потребовать ожидания опустошения Worker (цикл повторных попыток — стандартная поза; финал примера этой главы и есть шаблон).
Место easy в слоях xhttp
98.3示例
Первая полная программа: однострочный GET
Программа ниже из examples/http/client_easy — полная утилита из трёх ключевых строк:
#include <stdio.h> #include <string.h> #include <xhttp.h> /* 用一行 GET 便利入口执行请求并输出响应状态与正文。 */ int main(int argc, char** argv) { xnetengineconfig EngineConfig; xhttpclientconfig ClientConfig; xnetengine* pEngine; xhttpclient* pClient; xhttpresult* pResult; const xhttpresponse* pResponse; int iExit = 1; if ( argc != 2 ) { fprintf(stderr, "usage: client_easy <http-url>\n"); return 0; } xrtNetEngineConfigInit(&EngineConfig); pEngine = xrtNetEngineCreate(&EngineConfig); if ( (pEngine == NULL) || !xrtNetEngineStart(pEngine) ) { goto cleanup_engine; } xrtHttpClientConfigInit(&ClientConfig); pClient = xrtHttpClientCreate(pEngine, &ClientConfig); if ( pClient == NULL ) { goto cleanup_engine; } pResult = xrtHttpClientGetSync( pClient, (xstrview){ argv[1], strlen(argv[1]) }, NULL ); if ( pResult == NULL ) { goto cleanup_client; } pResponse = xrtHttpResultResponse(pResult); if ( pResponse != NULL ) { xbytesview Body = xrtHttpResponseBody(pResponse); printf("status: %u\n", (unsigned)xrtHttpResponseStatus(pResponse)); if ( Body.Size != 0 ) { (void)fwrite(Body.Data, 1, Body.Size, stdout); (void)fputc('\n', stdout); } iExit = 0; } xrtHttpResultDestroy(pResult); cleanup_client: xrtHttpClientDestroy(pClient); cleanup_engine: if ( pEngine != NULL ) { while ( !xrtNetEngineDestroy(pEngine) ) { xrtClearError(); xrtThreadYield(); } } return iExit; }
Что только что произошло. (1) После трёхшаговой сборки xrtHttpClientGetSync(pClient, URL视图, NULL) (URL-представление) одной строкой отправляет и ждёт — третий параметр NULL берёт опции вызова по умолчанию (тайм-ауты/перенаправления/кэш и прочее наследуют конфигурацию Client). (2) Три элемента успешного пути: HttpResultResponse одалживает ответ → HttpResponseStatus код состояния + HttpResponseBody представление тела (прямой fwrite, ноль копий) → HttpResultDestroy освобождает. (3) При отказе GetSync возвращает NULL, подробности — в слоте ошибки потока (модель главы 4 дошла и досюда). (4) Финальный цикл while (!xrtNetEngineDestroy(...)) заслуживает внимания — Destroy движка может вернуть отказ из-за идущего завершения; сбрасывайте ошибку и повторяйте до успеха: это стандартная запись «уничтожай Engine последним» в мире xhttp.
Вторая полная программа: форма Future
Вторая программа из examples/http/client_future — асинхронная грань тех же вызовов:
#include <stdio.h> #include <string.h> #include <xhttp.h> /* 以通用 Future 执行一次 GET,并在宿主线程读取拥有型 HTTP 结果。 */ int main(int argc, char** argv) { xnetengineconfig EngineConfig; xhttpclientconfig ClientConfig; xnetengine* pEngine; xhttpclient* pClient; xhttprequest* pRequest; xfuture* pFuture; xfuture* pClosed; xhttpresult* pResult; const xhttpresponse* pResponse; int iExit = 1; if ( argc != 2 ) { fprintf(stderr, "usage: client_future <http-url>\n"); return 0; } xrtNetEngineConfigInit(&EngineConfig); pEngine = xrtNetEngineCreate(&EngineConfig); if ( (pEngine == NULL) || !xrtNetEngineStart(pEngine) ) { goto cleanup_engine; } xrtHttpClientConfigInit(&ClientConfig); pClient = xrtHttpClientCreate(pEngine, &ClientConfig); if ( pClient == NULL ) { goto cleanup_engine; } pRequest = xrtHttpRequestCreate( XRT_STR_LITERAL("GET"), (xstrview){ argv[1], strlen(argv[1]) } ); if ( pRequest == NULL ) { goto cleanup_client; } pFuture = xrtHttpClientDoAsync( pClient, pRequest, NULL ); xrtHttpRequestDestroy(pRequest); if ( (pFuture == NULL) || (xrtFutureWait(pFuture) != XWAIT_OK) || (xrtFutureState(pFuture) != XFUTURE_RESOLVED) ) { xrtFutureDestroy(pFuture); goto cleanup_client; } pResult = (xhttpresult*)xrtFutureValue(pFuture); pResponse = xrtHttpResultResponse(pResult); if ( pResponse != NULL ) { xbytesview Body = xrtHttpResponseBody(pResponse); printf("status: %u\n", (unsigned)xrtHttpResponseStatus(pResponse)); if ( Body.Size != 0 ) { (void)fwrite(Body.Data, 1, Body.Size, stdout); (void)fputc('\n', stdout); } iExit = 0; } xrtFutureDestroy(pFuture); cleanup_client: pClosed = xrtHttpClientWaitAsync(pClient); if ( iExit == 0 ) { (void)xrtHttpClientDrain(pClient); } else { (void)xrtHttpClientAbort(pClient); } xrtHttpClientDestroy(pClient); if ( pClosed != NULL ) { (void)xrtFutureWait(pClosed); xrtFutureDestroy(pClosed); } cleanup_engine: if ( pEngine != NULL ) { while ( !xrtNetEngineDestroy(pEngine) ) { xrtClearError(); xrtThreadYield(); } } return iExit; }
Что только что произошло. (1) Здесь отправка идёт путём построителя (HttpRequestCreate + HttpClientDo), ибо цель демонстрации включает явный жизненный цикл объекта запроса — сравните с easy: серия Get и есть свёртка этих трёх строк. (2) После завершения Future xrtFutureValue берёт результат, HttpResultResponse идёт тем же путём снимка только для чтения — Sync и Future разделяют один объект результата, слой удобства отличается лишь тем, «кто ждёт» (версия xhttp для двух форм главы 82). (3) Прочтите два примера рядом: одна сборка, один финал, одна модель ошибок — различие только в двух строках отправки и ожидания. Это проверяемая форма «нет второй реализации».
98.4契约
- Нет второй реализации: easy лишь создаёт временный запрос и передаёт
Doна заморозку; всё поведение (тайм-ауты/отмена/перенаправления/Cookie/кэш/прокси/TLS/распаковка/диагностика/цепочка ошибок) совпадает с низкоуровневым; механически переписывается в форму построителя. - Три входа, три формы: Get/Post/SendBytes × обратный вызов/Async/Sync; Sync запрещено блокировать на Worker.
- Семантика тела: GET без тела (пустое тело не изобретается); пустое представление = явная нулевая длина; пустой ContentType = поле не создаётся.
- Намеренно не принимает: Header/потоковое тело/формы/аутентификацию — спуск к построителю (глава 99), без промежуточного состояния.
- Объект результата: снимок только для чтения;
ResultResponseодалживает ответ,ResponseBodyодалживает тело; итоговый действующий URL содержит точку приземления после перенаправлений; после использованияResultDestroy. - Валидация при старте: создание Client сразу проверяет статическую конфигурацию; неверная политика не откладывается до первого запроса.
- Обрезка:
XHTTP_MODULE_HTTP_CLIENT_EASY(обратный вызов) и_EASY_FUTURE(Future/синхронный) независимы. - Дисциплина сборки: Engine стартует первым; очистка сначала Client, потом Engine; Destroy Engine — циклом повторных попыток.
Типичные ошибки
Симптом: взаимоблокировка — обратный вызов завершения исполняется на Worker, Sync ждёт завершения в том же потоке.
Причина: реализация Sync — «вызывающий поток ждёт конечного состояния»; Worker, ожидающий собственное завершение, — вечное взаимное ожидание. Внутри обратного вызова либо переправляйте в другой поток, либо используйте саму форму обратного вызова.
static void on_done(xhttpcall* pCall, ..., ptr pCtx) { /* на Worker: */ pResult = xrtHttpClientGetSync(pClient, OtherUrl, NULL); /* взаимоблокировка */ }
static void on_done(xhttpcall* pCall, ..., ptr pCtx) { /* внутри обратного вызова - следующая форма обратного вызова */ xrtHttpClientGet(pClient, OtherUrl, NULL, on_next, pCtx); } /* действительно нужен последовательный блокирующий wait: Sync/Future в не-Worker потоке */
Симптом: понадобился один Header — бросили easy и сами склеили Engine+DNS+TCP+TLS, переписав путь запроса; или наоборот — запихнули в параметры easy глобальную конфигурационную структуру.
Причина: не осознано, что граница намеренная: над easy нет скрытой конфигурации; нужна не «более умная easy», а спуск на слой вниз — одной строки построителя HttpRequestSetHeader достаточно, остальная сборка переиспользуется целиком.
/* ради одного Header выбросить весь xhttp-клиент */ hand_roll_engine_dns_tls_http(); /* 300 строк */
xhttprequest* pReq = xrtHttpRequestCreate(); xrtHttpRequestSetMethod(pReq, ...); xrtHttpRequestSetUrl(pReq, ...); xrtHttpRequestSetHeader(pReq, XRT_STR_LITERAL("Authorization"), Token); xrtHttpRequestSetBytes(pReq, Body, iSize, Type); pCall = xrtHttpClientDo(pClient, pReq, NULL, on_done, pCtx); xrtHttpRequestDestroy(pReq); /* после заморозки снимка в Do исходный запрос можно уничтожать сразу */
Симптом: каждый запрос даёт утечку одного объекта результата; или представление тела используется наполовину, когда ответ уже освобождён — падение.
Причина: xhttpresult — владеющий снимок (ответ/тело под его управлением); ResultResponse/ResponseBody — только заимствование. Правило жизни: представление живёт в результате, результат живёт до вашего Destroy.
const xhttpresponse* pR = xrtHttpResultResponse(pResult); xrtHttpResultDestroy(pResult); /* сначала снесли дом */ printf("%.*s\n", Body from pR); /* представление повисло */
const xhttpresponse* pR = xrtHttpResultResponse(pResult); xbytesview Body = xrtHttpResponseBody(pR); consume(Body); /* сначала потребить */ xrtHttpResultDestroy(pResult); /* потом освободить */
Практические задания
Против одного тестового сервиса (локально поднимите server из главы 104 или любой httpbin-подобный сервис) получите результаты Get/Post/SendBytes, печатая код состояния и длину тела. Критерии приёмки: сборочный код трёх входов полностью одинаков; у GET-ответа заголовок Content-Type не создаётся.
Один и тот же URL выполните GetSync и Get+Future; полями FirstByte/RequestWireBytes из xhttpcallresult.Info сравните диагностику двух вызовов. Критерии приёмки: семантика диагностических полей двух путей совпадает; вы можете объяснить разницу WireBytes и BodyBytes (байты в канале против отданного тела — после распаковки они различаются).
Вызов Post (с типом) перепишите построчно в форму HttpRequestCreate/SetMethodUrl/SetBytes + Do и сверьте вывод и диагностику обеих версий. Затем перепишите Get. Критерии приёмки: поведение сходится по пунктам (статус/тело/итоговый URL/класс ошибки); при переписи не нужна никакая «специальная документация easy» — её не существует.
Итоги
| Тема | Кратко |
|---|---|
| Проектная позиция | нет второй реализации: временный запрос уходит Do на заморозку; поведение идентично низкоуровневому |
| Три входа | Get (без тела) / Post (копия байтов) / SendBytes (любой метод + байты) |
| Три формы | база — обратный вызов; _EASY_FUTURE добавляет Async (xfuture) / Sync (блокирующий) |
| Семантика тела | GET без тела; пустое представление = явный ноль; пустой тип = поле не создаётся |
| Граница | намеренно не принимает Header/потоковое/формы/аутентификацию — спуск к построителю |
| Объект результата | снимок только для чтения; Response/Body заимствуются; итоговый URL содержит точку после перенаправлений; обязательный Destroy |
| Дисциплина Sync | запрещено блокировать на Worker — внутри обратного вызова используйте форму обратного вызова |
| Валидация при старте | создание Client сразу проверяет статическую конфигурацию |
| Сборка | старт Engine → создание Client → вызов; очистка в обратном порядке, Destroy Engine циклом |
| Обрезка | EASY и EASY_FUTURE независимые макросы |
📖 Справочники API в этой главе:xhttp-http_client (101 записей)xhttp-http_client_easy (9 записей)net (247 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки