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

Том 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

1Слой удобства easy (эта глава)Get/Post/SendBytes одной строкой - временный запрос уходит Do на заморозку
2Среда выполнения (глава 99)построитель запросов + Client + опции вызова + пул соединений/тайм-ауты/отмена
3Слой автоматического поведения (главы 100/101)перенаправления/повторы/Cookie/распаковка/кэш - переключатели конфигурации
4Протокольный фундамент (том 9)разбор/фрейминг/поля/декодирование - детали

98.3示例

Первая полная программа: однострочный GET

Программа ниже из examples/http/client_easy — полная утилита из трёх ключевых строк:

extlibs/xhttp/examples/http/client_easy/main.c源码 ↗
#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;
}


$ 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_easy/main.c -lws2_32 -lbcrypt -ladvapi32 -liphlpapiusage: client_easy <http-url>

Что только что произошло. (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 — асинхронная грань тех же вызовов:

extlibs/xhttp/examples/http/client_future/main.c源码 ↗
#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;
}
$ 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_future/main.c -lws2_32 -lbcrypt -ladvapi32 -liphlpapiusage: client_future <http-url>

Что только что произошло. (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 — циклом повторных попыток.

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

Ловушка 1: вызов входа Sync в обратном вызове Worker:Ловушка 1: вызов входа Sync в обратном вызове Worker

Симптом: взаимоблокировка — обратный вызов завершения исполняется на 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 потоке */
Ловушка 2: принять easy за границу возможностей и обходить её самоделкой:Ловушка 2: принять easy за границу возможностей и обходить её самоделкой

Симптом: понадобился один 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 исходный запрос можно уничтожать сразу */
Ловушка 3: забыли Destroy у объекта результата (или уничтожили ответ раньше времени):Ловушка 3: забыли Destroy у объекта результата (или уничтожили ответ раньше времени)

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

Причина: 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 не создаётся.

Продвинутое: сверка Sync и Future

Один и тот же URL выполните GetSync и Get+Future; полями FirstByte/RequestWireBytes из xhttpcallresult.Info сравните диагностику двух вызовов. Критерии приёмки: семантика диагностических полей двух путей совпадает; вы можете объяснить разницу WireBytes и BodyBytes (байты в канале против отданного тела — после распаковки они различаются).

Вызов: механическая перепись easy→построитель

Вызов 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 записей)

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