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

После шести клиентских глав (97–102) перспектива переворачивается: теперь вы сервер. xrtHttpServerStart одним шагом собирает Engine, агрегированный TCP Server, Stream и протокольные машины состояний без ввода-вывода в открытый текстовый HTTP/1-сервис — зависимости не прячутся за таблицей функций (честная обрезка). В главе три темы: конфигурационная грань — пять тайм-аутов (Header/Body/Request/Idle/Write — каждый независимо защищает свою стадию, ноль отключает); цепочка событий — Open → Headers → Body → Request → Error/Close, где Headers принимает стратегическое решение до чтения тела (буферизовать/стримить/отбросить/отвергнуть/ответить напрямую), Body принимает только потоковые фрагменты, возврат false из обратного вызова — 500; три пути ответа — Reply (прямой ответ фиксированными байтами, ноль временных контейнеров), ReplyBody (версия с источником тела), Respond (полный построитель динамических полей и trailer). Маршрутизация и middleware — глава 105; SSE и потоковость — главы 107/108. Вход разбора заголовков запроса у протокольного автомата в основе — ровно сетевая привязка парсера главы 90 (http1_net: цепочка буферов идёт в парсер напрямую, без ручной сшивки).

104.1引入

Крупнейшая парадигменная разница сервера и клиента — инверсия управления: клиент «инициирует и ждёт», сервер «управляется событиями» — когда придёт запрос, сколько их, с телом или без, и не вредоносно-ли-медленный, всё решает противоположная сторона. Дизайн среды выполнения разворачивается вокруг этой инверсии: пять тайм-аутов, каждый против своего вида «медленной атаки» (медленные заголовки — HeaderTimeout, медленное тело — BodyTimeout, зависшее приложение — RequestTimeout, висит без дела — IdleTimeout, ответ не движется — WriteTimeout); стратегия тела решается на стадии Headers — прежде чем дойдёт маршрутизация, вы обязаны сказать, «что делать с телом этого запроса»; прикладное противодавление (PauseRequestBody) позволяет не читать тело противоположной стороны, пока асинхронный потребитель не догонит, — TCP-окно естественно сжимается, память не растёт.

Путь прямого ответа Reply заслуживает отдельной фразы: фиксированные JSON/текстовые ответы не создают временных контейнеров Reply или Header, не требуют строить словари или JSON-объекты — один вызов прямо в замороженное ядро. Health-проверки, короткие ошибки, фиксированные ответы между микросервисами — эти «есть у каждого сервиса» пути получают форму с нулевой стоимостью.

104.2概念

Запуск и конфигурация

xrtHttpServerStart(Engine, 配置, 事件表) (движок, конфигурация, таблица событий) создаёт и немедленно стартует; провал запуска не оставляет частично работающего Server — все логические конечные точки либо связались все, либо общий провал (конфигурация нескольких конечных точек — в разделе Network). Ключевые пункты конфигурации:

  • Пять тайм-аутов (миллисекунды): HeaderTimeout (стадия чтения заголовков), BodyTimeout (чтение тела), RequestTimeout (длительность прикладной обработки), IdleTimeout (простой keep-alive), WriteTimeout (нет прогресса ответа) — ноль отключает соответствующую защиту. У каждого отрезка медленной атаки свой шлюз.
  • Соединения и потолки: MaxConnections (ноль = без прикладного лимита), MaxInformations (число информационных ответов в очереди на запрос), WriteSize (лизинг одной zero-copy отправки — фиксированный буфер отправки на каждое соединение не резервируется); приёмная память управляется предоставляемым по надобности xnetbuf TCP и жёсткими потолками.
  • Network: полностью открыты несколько конечных точек TCP Server, общие динамические порты, accept-очередь, reuse-port — xrtHttpServerLocal возвращает реальные конечные точки (стандартная поза «порт прослушивания 0», как в главе 66); xrtHttpServerNetwork одалживает ссылку на нижележащий xnetserver (низкоуровневая статистика для особых сценариев; после использования xrtNetServerDestroy).

Цепочка событий и стратегия тела

1Openадрес, Worker, факты нижележащего TCP-соединения
2Headersточка решения до чтения тела - буферизовать / стримить / отбросить / отвергнуть / ответить直接
3Bodyсрабатывает только в потоковом режиме - одалживает фрагменты тела; возврат false = ошибка обратного вызова + 500
4Requestполный запрос прибыл - синхронный ответ или удержание Connection для поздней доставки
5Error -> Closeна соединение максимум один стабильный финал-ошибка и ровно один финал; Shutdown = всё освобождено и завершено

Три ключевые семантики. (1) Право стратегии у Headers: по маршруту вызывается xrtHttpConnSetRequestBodyLimit (единственный жёсткий потолок текущего Exchange) — лимит загрузки есть решение уровня маршрута, а не глобальная константа. (2) Противодавление Body: PauseRequestBody можно вызывать только внутри обратного вызова Body — по возврату обратного вызова текущего фрагмента оно теряет силу (нужно оставить — копируйте сами); после завершения асинхронного потребителя из любого потока ResumeRequestBody (встроенная в Connection команда восстановления, без выделения узлов задач); во время паузы BodyTimeout продолжает отсчёт — потерянный потребитель не может бесконечно занимать соединение. (3) Единственность ответа: на запрос — ровно одна финальная фиксация (Respond/Reply/ReplyBody, одно из трёх); xrtHttpConnInform может заранее отправить информационный ответ 1xx (кроме 101).

Три пути ответа и замороженное ядро

  • xrtHttpConnReply(连接, 状态, 类型, 字节) (соединение, статус, тип, байты) — прямой ответ фиксированными байтами: копируется в компактное Body и сразу в замороженное ядро HTTP/1, ноль временных контейнеров. Стандартная форма health-проверки.
  • xrtHttpConnReplyBody(连接, ..., 可选 xhttpbody 引用) (соединение, ..., опциональная ссылка xhttpbody) — версия того же прямого пути с источником тела: Borrow/Take/Reference/файл/производитель избавляют от копирования; неизвестная длина — chunked (1.1) или закрытие (1.0). После возврата вызова вызывающий может уничтожать свою ссылку body.
  • xrtHttpConnRespond — полный путь построителя для динамических полей и trailer (зеркало построителя запросов главы 99).

Жизненный цикл: Drain и Shutdown

xrtHttpServerDrain — грациозное осушение: новые соединения не принимаются, идущие запросы завершаются, соединения закрываются одно за другим; событие Shutdown публикуется после выхода всех протокольных объектов, передачи прав транспорта, завершения среды выполнения и возврата принятых обратных вызовов Upgrade-передачи — «ничего не бегает» гарантировано событием, а не догадкой. Шаблон финала «цикл ожидания XHTTP_SERVER_CLOSED» тот же, что у TCP главы 67.

104.3示例

Первая полная программа: сервис health-проверки

Программа ниже из examples/http/server — минимальный полный сервис:

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

#include <math.h>
static inline double exampleTimerLimit(int64 Timeout)
{
    return Timeout == XRT_WAIT_FOREVER ? INFINITY : xrtTimer() + (double)Timeout / 1000.0;
}
static inline bool exampleTimerExpired(double Limit)
{
    return xrtTimer() >= Limit;
}
static inline int64 exampleTimerRemaining(double Limit)
{
    double Ms;
    if (Limit == INFINITY) return XRT_WAIT_FOREVER;
    Ms = ceil((Limit - xrtTimer()) * 1000.0);
    return Ms <= 0 ? 0 : Ms >= 0x1p63 ? INT64_MAX : (int64)Ms;
}



/* 比较借用视图与零结尾文本。 */
static bool exampleHttpEqual(xstrview Value, cstr sText)
{
	size_t iSize = strlen(sText);

	return (Value.Size == iSize) &&
		(memcmp(Value.Data, sText, iSize) == 0);
}



/* 直接提交固定响应,不要求应用创建 Reply 或 JSON 对象。 */
static void exampleHttpRequest(
	xhttpserver* pServer,
	xhttpconn* pConnection,
	const xhttpserverrequest* pRequest,
	ptr pData
)
{
	xstrview Target = xrtHttpServerRequestTarget(pRequest);

	(void)pServer;
	(void)pData;
	if ( exampleHttpEqual(Target, "/health") ) {
		(void)xrtHttpConnReply(
			pConnection,
			XHTTP_STATUS_OK,
			XRT_STR_LITERAL(
				"application/json; charset=utf-8"
			),
			XRT_BYTES_LITERAL(
				"{\"code\":200,\"msg\":\"OK\"}"
			)
		);
		return;
	}
	(void)xrtHttpConnReply(
		pConnection,
		XHTTP_STATUS_NOT_FOUND,
		XRT_STR_LITERAL("text/plain; charset=utf-8"),
		XRT_BYTES_LITERAL("Not Found")
	);
}



/* 输出稳定错误域、操作和消息,便于服务端日志关联。 */
static void exampleHttpError(
	xhttpserver* pServer,
	xhttpconn* pConnection,
	const xerror* pError,
	ptr pData
)
{
	(void)pServer;
	(void)pConnection;
	(void)pData;
	fprintf(
		stderr,
		"HTTP error [%s/%s]: %s\n",
		xrtErrorDomain(pError),
		xrtErrorOperation(pError),
		xrtErrorMessage(pError)
	);
}



/* 启动动态端口 HTTP 服务,按回车后优雅排空。 */
int main(void)
{
	xnetengineconfig EngineConfig;
	xhttpserverconfig ServerConfig;
	xhttpserverevents Events;
	xnetengine* pEngine = NULL;
	xhttpserver* pServer = NULL;
	xnetaddr Address;
	double Deadline;
	str sEndpoint = NULL;
	int iResult = 1;

	xrtNetEngineConfigInit(&EngineConfig);
	pEngine = xrtNetEngineCreate(&EngineConfig);
	if ( (pEngine == NULL) || !xrtNetEngineStart(pEngine) ) {
		goto Cleanup;
	}
	xrtHttpServerConfigInit(&ServerConfig);
	if ( !xrtNetAddrLoopback(
		&ServerConfig.Network.Listen.Address,
		XNET_FAMILY_IPV4,
		0
	) ) {
		goto Cleanup;
	}
	xrtHttpServerEventsInit(&Events);
	Events.Request = exampleHttpRequest;
	Events.Error = exampleHttpError;
	pServer = xrtHttpServerStart(
		pEngine,
		&ServerConfig,
		&Events
	);
	if ( (pServer == NULL) ||
		!xrtHttpServerLocal(pServer, 0, &Address) ) {
		goto Cleanup;
	}
	sEndpoint = xrtNetAddrEndpointString(&Address);
	if ( sEndpoint == NULL ) {
		goto Cleanup;
	}
	printf(
		"listening on http://%s/health\n"
		"press Enter to drain\n",
		sEndpoint
	);
	xrtFree(sEndpoint);
	sEndpoint = NULL;
	(void)getchar();
	if ( !xrtHttpServerDrain(pServer) ) {
		goto Cleanup;
	}
	Deadline = exampleTimerLimit(INT64_C(5000));
	while ( xrtHttpServerState(pServer) !=
		XHTTP_SERVER_CLOSED ) {
		if ( exampleTimerExpired(Deadline) ) {
			(void)xrtHttpServerAbort(pServer);
			break;
		}
		xrtThreadYield();
	}
	iResult = 0;

Cleanup:
	xrtFree(sEndpoint);
	if ( (pServer != NULL) &&
		(xrtHttpServerState(pServer) !=
		 XHTTP_SERVER_CLOSED) ) {
		(void)xrtHttpServerAbort(pServer);
		Deadline = exampleTimerLimit(INT64_C(5000));
		while ( xrtHttpServerState(pServer) !=
			XHTTP_SERVER_CLOSED ) {
			if ( exampleTimerExpired(Deadline) ) {
				iResult = 2;
				break;
			}
			xrtThreadYield();
		}
	}
	xrtHttpServerDestroy(pServer);
	if ( (pEngine != NULL) &&
		!xrtNetEngineDestroy(pEngine) &&
		(iResult == 0) ) {
		iResult = 2;
	}
	return iResult;
}
$ 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/server/main.c -lws2_32 -lbcrypt -ladvapi32 -liphlpapilistening on http://127.0.0.1:52173/healthpress Enter to drain

Что только что произошло. (1) Три сборочные вещи: старт Engine, HttpServerConfigInit (пять тайм-аутов по умолчанию — консервативный дефолт с полностью включённой защитой), HttpServerEventsInit + два обратных вызова (Request/Error). (2) Адрес прослушивания NetAddrLoopback + порт 0 (назначает система) — xrtHttpServerLocal забирает реальные конечные точки, склеивает URL и печатает — та же поза, что у Listener главы 67, в исполнении xhttp. (3) Обратный вызов Request и есть весь бизнес: Target равен /health — HttpConnReply прямо отвечает JSON (один вызов, ноль контейнеров); иначе 404 напрямую — оба пути прямые. (4) Enter запускает Drain: грациозное осушение → цикл ожидания XHTTP_SERVER_CLOSED → уничтожение — полный жизненный цикл сервиса в одном main; такова поза учебного образца.

Вторая полная программа: сборка роутера

Вторая программа из examples/http/server_router — комбинация маршрутизации и прямого ответа:

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



/* 常用路由可以直接提交固定正文,不要求应用创建 Reply。 */
static void exampleHttpServerRoute(
	xhttpserver* pServer,
	xhttpconn* pConnection,
	const xhttpserverrequest* pRequest,
	const xhttprouteparam* pParams,
	size_t iParamCount,
	ptr pData
)
{
	(void)pServer;
	(void)pRequest;
	(void)pParams;
	(void)iParamCount;
	(void)pData;
	(void)xrtHttpConnReply(
		pConnection,
		XHTTP_STATUS_OK,
		XRT_STR_LITERAL("application/json; charset=utf-8"),
		XRT_BYTES_LITERAL("{\"code\":200,\"msg\":\"OK\"}")
	);
}



/* 注册常用方法并冻结为可无锁并发匹配的 Server Router。 */
int main(void)
{
	xhttpserverrouter* pRouter = xrtHttpServerRouterCreate(NULL);

	if ( (pRouter == NULL) ||
		!xrtHttpServerGet(
			pRouter,
			XRT_STR_LITERAL("/users/{id}"),
			exampleHttpServerRoute,
			NULL
		) || !xrtHttpServerPost(
			pRouter,
			XRT_STR_LITERAL("/users"),
			exampleHttpServerRoute,
			NULL
		) || !xrtHttpServerRouterFreeze(pRouter) ) {
		xrtHttpServerRouterDestroy(pRouter);
		return 1;
	}
	printf(
		"routes: %u, frozen: %s\n",
		(unsigned)xrtHttpServerRouterCount(pRouter),
		xrtHttpServerRouterFrozen(pRouter) ? "yes" : "no"
	);
	xrtHttpServerRouterDestroy(pRouter);
	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/server_router/main.c -lws2_32 -lbcrypt -ladvapi32 -liphlpapi(输出路由装配计数并正常退出)

Что только что произошло. (1) xrtHttpServerRouterCreate строит роутер; xrtHttpServerGet(路由器, 路径, 处理器, 数据) (роутер, путь, обработчик, данные) регистрирует обработчик метод+путь — PUT/POST/wildcard того же семейства; RouterFreeze замораживает в таблицу совместного чтения (та же мысль «отправил — заморозил» главы 99: диспетчеризация горячего пути не трогает изменяемые структуры). (2) Замороженная таблица маршрутов диспетчеризуется цепочкой событий Server: запрос прибыл → совпадение → обработчик получает массив параметров xhttprouteparam (параметры пути вроде id у /items/{id}); несовпадение — автоматический 404. (3) Обратный вызов уровня маршрута той же формы, что и обратный вызов Request этой главы, — снимок xhttpserverrequest + ответ HttpConnReply/Respond; middleware (xrtHttpServerUse) ложится поверх маршрутов — разворачивается в главе 104.

104.4契约

  • Атомарность запуска: все логические конечные точки связываются одновременно, провал не оставляет частичного Server; диапазоны адресов валидируются до открытия; потолки валидируются статически без выделения.
  • Пять тайм-аутов: Header/Body/Request/Idle/Write каждый защищает свой отрезок, миллисекунды, ноль отключает; у каждого отрезка медленной атаки свой шлюз.
  • Последовательность событий: все прикладные события исполняются последовательно на Worker-владельце соединения; Error максимум один (стабильная ошибка + цепочка причин), Close ровно один; сбой транспорта — фиксированный порядок Error→Close.
  • Стратегия Headers: выбор из пяти буферизация/поток/отброс/отказ/прямой ответ; лимит тела уровня маршрута (SetRequestBodyLimit меняет единственный жёсткий потолок текущего Exchange).
  • Противодавление Body: Pause только внутри обратного вызова Body; по возврату обратного вызова фрагмент недействителен; Resume из любого потока (встроенная команда, ноль выделений); во время паузы BodyTimeout отсчитывается.
  • Единственность ответа: одна финальная фиксация; Inform шлёт 1xx (кроме 101); информационные и финальный ответ на одной линии противодавления.
  • Путь прямого ответа: Reply/ReplyBody входят в замороженное ядро без временных контейнеров; ссылку body после фиксации можно уничтожать; неизвестная длина — chunked/закрытие.
  • Жизненный цикл: Drain — грациозное осушение; Shutdown публикуется после полного освобождения ресурсов (включая Upgrade-передачу); финал Closed ждётся циклом.
  • Обрезка: XHTTP_FEATURE_HTTP_SERVER зависит от Exchange/Response/TCP Server; потоковый исходящий, файлы, TLS, Upgrade — независимые комбинируемые слои; в ядре нет второй машины состояний.

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

Ловушка 1: сохранить представление фрагмента из обратного вызова Body для асинхронной обработки:Ловушка 1: сохранить представление фрагмента из обратного вызова Body для асинхронной обработки

Симптом: асинхронный потребитель читает перемешанные данные фрагментов или падает — то же висячее представление, что в главе 95.

Причина: фрагменты Body — заимствование — по возврату обратного вызова недействительны. Асинхронное потребление — либо копия, либо Pause/Resume, чтобы среда выполнения ждала вас.

错误写法
static bool onBody(..., xbytesview Chunk, ...) {
	queue_push(g_Queue, Chunk);      /* сохранили представление: по возврату повисло */
}
正确写法
static bool onBody(xhttpconn* pConn, ..., xbytesview Chunk, ...) {
	if ( !consumer_ready() ) {
		copy_and_queue(Chunk);                 /* оставить - копируем */
		xrtHttpConnPauseRequestBody(pConn);    /* приостановить дальнейшую доставку */
		return true;
	}
	consume_now(Chunk);                          /* синхронное потребление, ноль копий */
	return true;
}
/* после завершения асинхронного потребителя из любого потока: xrtHttpConnResumeRequestBody(pConn) */
Ловушка 2: медленному клиенту не выставили WriteTimeout:Ловушка 2: медленному клиенту не выставили WriteTimeout

Симптом: клиент, оборвавший сеть посреди загрузки (без RST), занимает соединение и буферы часами — лимит соединений съедают такие зомби.

Причина: дефолт WriteTimeout может не покрывать вашу форму развёртывания (ноль = защита выключена). У «нет прогресса» стадии ответа обязан быть срок.

错误写法
xrtHttpServerConfigInit(&Config);
/* WriteTimeout остался дефолтным/нулём - зомби-соединения занимают место вечно */
正确写法
xrtHttpServerConfigInit(&Config);
Config.WriteTimeout = UINT64_C(30000000);   /* 30s без прогресса ответа - бросаем */
Config.IdleTimeout = UINT64_C(75000000);    /* keep-alive 75s */
/* все пять явно - защита это контракт развёртывания, не везение */
Ловушка 3: длительная блокирующая операция в обратном вызове Request:Ловушка 3: длительная блокирующая операция в обратном вызове Request

Симптом: Worker заблокирован — остальные соединения того же Worker все встали (серверная цена экономики Worker главы 64).

Причина: события исполняются на Worker последовательно; блокировка в обратном вызове (синхронный файловый IO, внешний вызов) закладывает в залог целый Worker. RequestTimeout лишь ограничивает урон, не лечит.

错误写法
static void onRequest(..., const xhttpserverrequest* pReq, ...) {
	char* p = blocking_external_call(...);   /* блок 2s: Worker весь встал */
	reply(...);
}
正确写法
static void onRequest(xhttpserver* pS, xhttpconn* pC,
		const xhttpserverrequest* pReq, ptr pD) {
	submit_to_pool(pC, pReq);   /* перепоручить пулу исполнения (глава 59), удержав Connection */
	/* по завершении в пуле - доставка обратно на Worker-владелец и Respond; RequestTimeout страхует */
}

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

Базовое: эксперимент с пятью тайм-аутами

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

Продвинутое: маршрутизация и параметры

Router-ом реализуйте двухуровневые параметры пути /items/{id} и /items/{id}/parts/{part}; Request печатает значения параметров; добавьте обработчик PUT. Критерии приёмки: параметры пути извлекаются по порядку; несовпадение 404; несовпадение метода 405.

Вызов: шлюз с лимитом загрузки

Обратный вызов Headers ставит лимит тела по маршруту (/avatar 2 MB, /video 200 MB, прочее 1 MB); Body принимает потоково и считает байты; запрос сверх лимита отвергается на стадии тела (413), соединение закрывается чисто. Критерии приёмки: три лимита действуют каждый; соединение сверх лимита не занимает память (лимит перехватывает до расхода); нормальная загрузка принимается целиком.

Итоги

ТемаКратко
ЗапускStart одним шагом строит сервис открытого текста; провал не оставляет частичного Server; Local возвращает конечные точки динамического порта
Пять тайм-аутовHeader/Body/Request/Idle/Write каждый защищает свой отрезок; миллисекунды; ноль выключает
Цепочка событийOpen→Headers (точка стратегии)→Body (потоковые фрагменты)→Request→Error/Close; Worker последовательно
Пять стратегий Headersбуферизация/поток/отброс/отказ/прямой ответ; лимит уровня маршрута
Противодавление BodyPause только внутри обратного вызова, фрагмент сразу недействителен; Resume из любого потока без выделений; в паузу тайм-аут идёт
Единственность ответаодна финальная фиксация; Inform шлёт 1xx; три пути: Reply/ReplyBody/Respond
Путь прямого ответаReply без контейнеров прямо в замороженное ядро; ссылку body после фиксации можно уничтожать
Жизненный циклDrain осушает; Shutdown публикуется после полного освобождения ресурсов; финал Closed ждётся циклом
Networkнесколько конечных точек/динамический порт/reuse-port полностью открыты; Network одалживает нижележащую ссылку
ОбрезкаServer = Exchange+Response+TCP Server; потоковость/файлы/TLS/Upgrade — независимые слои

📖 Справочники API в этой главе:xhttp-http_server (75 записей)xhttp-http_server_router (17 записей)net (247 записей)

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