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

Всё вне бизнес-маршрутизации — журналирование, хронометраж, авторизация, CORS, сжатие — должно пройти по каждому запросу. xrtHttpServerUse регистрирует их как middleware: вход в порядке регистрации, выход в обратном (луковичная модель); xrtHttpServerNext синхронно входит в следующий слой, после возврата исполняется пост-логика (хронометраж/завершение журнала);не вызвать Next — значит замкнуть цепь — middleware может ответить напрямую или удержать Connection для асинхронной обработки. Ключевая деталь контракта: Next действительна только внутри текущего синхронного стека обратных вызовов (нельзя сохранять в поток/Future/корутину), вызывается максимум один раз (повтор = путь ошибки) — асинхронное middleware должно замыкать цепь и удерживать Connection само, а не откладывать Next. Вторая половина главы — слой статических файлов: ответ одним файлом, отображение каталогов с песочницей (http_server_static — серверное приземление идеи dir-песочницы главы 47).

105.1引入

Ценность луковичной модели — в паре «до + после»: middleware журналирования до вызывает помечает старт, зовёт Next, после возврата помечает финал и статус — один слой кода накрывает весь жизненный цикл запроса. Но асинхронность ломает наивную луковицу: если middleware хочет «спросить кэш и решить, пускать ли» (асинхронный запрос), когда звать Next? Ответ контракта — не звать — замкнуть цепь и удержать Connection; по завершении асинхронности ответить самому или вручную диспетчеризировать на финальный маршрут. Дисциплина идёт от технического ограничения: Next — дескриптор кадра синхронного стека — сохранить его в другом месте и позвать позже значит войти заново в машину состояний (тот же запрет, что у обратных вызовов Select главы 86).

Статические файлы — другой род сквозной задачи: почти каждому сервису надо «отправить файл» — favicon, артефакты фронтенд-сборки, архивные загрузки. Ручное «прочитать файл + угадать тип + ответить Range» — традиционный номер каждого C-официанта; http_server_static стандартизирует это: один файл, отображение префикса каталога, согласование Range (Range главы 91 в серверной форме) — всё в одном месте.

105.2概念

Регистрация и луковица

1РегистрацияUse(middleware A) -> Use(B) -> Get(маршрут) (сперва middleware, потом маршруты) -> RouterFreeze
2Запрос«до» A -> «до» B -> обработчик маршрута -> «после» B -> «после» A (выход в обратном порядке)
3Nextсинхронно войти в следующий слой; возврат = диспетчеризация следующего слоя вернулась (не завершение I/O)
4Замыкание цепибез вызова Next - middleware отвечает напрямую или удерживает Connection для асинхронности

Два способа регистрации: Use (данными управляет вызывающий) и UseOwned (после успешной регистрации ответственность Release(Data) переходит к Router — разрушение последней ссылки освобождает в обратном порядке регистрации).Возврат обратного вызова: true = этот слой принимает запрос; false = неустранимая прикладная ошибка — среда выполнения посильно фиксирует фиксированные 500, при уже зафиксированном ответе — аварийное закрытие. Количество middleware ограничено потолком MaxRoutes Router; рост регистрации атомарен (провал не меняет видимого числа и не передаёт ответственность за очистку).

Точная семантика Next

Три легко неправильно понятых пункта. (1) Возврат Next ≠ ответ дописан: возврат означает лишь возврат диспетчеризации следующего слоя — синхронный прямой ответ обычно уже вошёл в состояние XHTTP_CONN_RESPONSE, а асинхронный маршрут может ещё ждать; хронометраж полного ответа ведите по пути завершения ответа Connection. (2) Действительна внутри стека: xhttpservernext действительна только в текущем синхронном стеке обратного вызова — сохранить её в другой поток/Future/корутину и позвать — неопределённое поведение; повторный вызов выставляет XHTTP_SERVER_MIDDLEWARE_ERROR_NEXT и входит в путь ошибки. (3) Проброс параметров: на совпавшем маршруте middleware получает те же Params/Count, что видит финальный маршрут (при несовпадении — пусто) — middleware может видеть параметры маршрута (id в вашем журнале).

Сотрудничество с 404/405/OPTIONS

Unified-журналу и обработчику ошибок надо видеть 404/405 и автоматические OPTIONS — при включённом хотя бы одном middleware тело несовпавшего запроса сперва отбрасывается, затем запрос входит в полную цепочку (слой журнала видит полную форму запроса); без middleware несовпавший запрос отвечает напрямую на стадии Headers, тело отвергается (удобный быстрый путь). Включение меняет «глубину обработки несовпавших» — ровно это и есть исполнение «middleware принимает сквозные задачи на себя».

Слой статических файлов

http_server_static даёт отображение файлов на маршруты:

  • Один файл: семейство xrtHttpConnFile — один путь на один файл (favicon, robots и компания); Range-вариант xrtHttpConnFileRange на том же уровне.
  • Отображение каталога: префикс URL ↔ каталог на диске — /static/* отображается на каталог артефактов сборки; защита от выхода за пределы (атаки ../ перехватываются в слое отображения — идея dir-песочницы главы 47).
  • Согласование: Content-Type по расширению (база MIME), ETag/Last-Modified с условными запросами (путь 304), запросы Range (зеркало http_cache_range на сервере — докачка).
  • Путь ответа: файловые ответы идут через файловый источник тела ReplyBody (путь прямого ответа главы 104) — отправка без копирования (семейство SendFile с передачей напрямую в ядро из главы 68 исполняется на транспортном слое).

Единство пути диспетчеризации

Фиксированный Router и Host Mux (мультиплексор, ветвящийся по заголовку Host, — один сервис на много доменных имён) используют один и тот же путь диспетчеризации — таблица middleware и таблица маршрутов замораживаются вместе в совместное чтение. Диспетчеризация горячего пути без блокировок (та же философия производительности, что и фрагментация пула главы 99). Синтаксис захвата сегментов {name} в образцах маршрутов (http_route.h) родствен захвату {name} ограниченного родича pattern главы 30 — заморозка и есть точка, где «скомпилировать раз, на горячем пути только смотреть» приземляется в слой маршрутизации.

105.3示例

Первая полная программа: полная сборка middleware + маршруты

Программа ниже из examples/http/server_middleware — middleware журналирования оборачивает health-маршрут:

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



/* 请求日志层在 Next 前后包裹路由,短路时也可以直接提交响应。 */
static bool exampleHttpMiddlewareLog(
	xhttpserver* pServer,
	xhttpconn* pConnection,
	const xhttpserverrequest* pRequest,
	const xhttprouteparam* pParams,
	size_t iParamCount,
	xhttpservernext* pNext,
	ptr pData
)
{
	xstrview Method = xrtHttpServerRequestMethod(pRequest);
	xstrview Target = xrtHttpServerRequestTarget(pRequest);
	bool bResult;

	(void)pServer;
	(void)pConnection;
	(void)pParams;
	(void)iParamCount;
	(void)pData;
	printf(
		"> %.*s %.*s\n",
		(int)Method.Size,
		Method.Data,
		(int)Target.Size,
		Target.Data
	);
	bResult = xrtHttpServerNext(pNext);
	printf("< state=%d\n", (int)xrtHttpConnState(pConnection));
	return bResult;
}



/* 常用业务路由直接提交固定 JSON,不要求构造响应对象。 */
static void exampleHttpMiddlewareRoute(
	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\"}")
	);
}



/* 展示中间件先注册、路由后注册、最后一次冻结的完整构造顺序。 */
int main(void)
{
	xhttpserverrouter* pRouter = xrtHttpServerRouterCreate(NULL);

	if ( (pRouter == NULL) ||
		!xrtHttpServerUse(
			pRouter,
			exampleHttpMiddlewareLog,
			NULL
		) || !xrtHttpServerGet(
			pRouter,
			XRT_STR_LITERAL("/health"),
			exampleHttpMiddlewareRoute,
			NULL
		) || !xrtHttpServerRouterFreeze(pRouter) ) {
		xrtHttpServerRouterDestroy(pRouter);
		return 1;
	}
	printf(
		"middleware: %u, routes: %u\n",
		(unsigned)xrtHttpServerMiddlewareCount(pRouter),
		(unsigned)xrtHttpServerRouterCount(pRouter)
	);
	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_middleware/main.c -lws2_32 -lbcrypt -ladvapi32 -liphlpapimiddleware: 1, routes: 1

Что только что произошло. (1) Порядок сборки и есть луковичный порядок: Use(日志中间件) (middleware журналирования) регистрируется первым, Get("/health", 路由) (маршрут) вторым, RouterFreeze замораживает — middleware всегда снаружи маршрутов (порядок регистрации и есть порядок оборачивания). (2) Шаблон формы middleware журналирования: до печатает > 方法 目标 (метод цель) -> xrtHttpServerNext(pNext) входит в следующий слой (health-маршрут отвечает JSON) → после возврата печатает < state — xrtHttpConnState показывает ответ уже зафиксирован (синхронный прямой путь). (3) Вывод middleware: 1, routes: 1 эхом повторяет факты сборки — MiddlewareCount/RouterCount есть интроспекция конфигурации (та же поза «конфигурация проверяема», что и Stats главы 99). (4) В реальном сервисе этот Router вешается в конфигурацию диспетчеризации xhttpserverconfig — образец этой главы сфокусирован на слое сборки, потому сеть не поднимается.

Вторая полная программа: сервис статических файлов

Вторая программа из examples/http/static_file — стандартная сборка файловых ответов:

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

#include <stdio.h>



/* 展示从受限文件根准备静态表示元数据和完整文件正文。 */
int main(void)
{
	static const char sPath[] =
		"xrt-http-static-file-example.tmp";
	xtaskpoolconfig Config = { 1, 8, 0 };
	const xhttprepresentation* pCurrent;
	xtaskpool* pPool;
	xhttpstaticfile* pFile;
	xhttpbody* pBody;
	xroot Root;
	xfile File;
	int iResult = 1;

	(void)xrtFileDelete(sPath);
	xrtClearError();
	File = xrtOpen(
		sPath,
		XFILE_WRITE |
		XFILE_CREATE |
		XFILE_EXCLUSIVE
	);
	if ( (File == NULL) ||
		!xrtWriteFull(
			File,
			"static resource",
			15,
			NULL
		) ||
		!xrtClose(File) ) {
		return 1;
	}
	Root = xrtRootOpen(".");
	pPool = xrtTaskPoolCreate(&Config);
	pFile = (Root != NULL) && (pPool != NULL) ?
		xrtHttpStaticFileOpen(
			pPool,
			Root,
			sPath
		) : NULL;
	pCurrent = pFile != NULL ?
		xrtHttpStaticFileRepresentation(
			pFile
		) : NULL;
	pBody = pFile != NULL ?
		xrtHttpStaticFileTakeBodyAll(
			pFile
		) : NULL;
	if ( (pCurrent != NULL) &&
		pCurrent->HasETag &&
		(pBody != NULL) ) {
		printf(
			"size=%llu weak-etag=%.*s\n",
			(unsigned long long)
				xrtHttpBodyLength(pBody),
			(int)pCurrent->ETag.Opaque.Size,
			pCurrent->ETag.Opaque.Data
		);
		iResult = 0;
	}
	xrtHttpBodyDestroy(pBody);
	xrtHttpStaticFileDestroy(pFile);
	if ( pPool != NULL ) {
		(void)xrtTaskPoolDestroy(pPool);
	}
	if ( Root != NULL ) {
		(void)xrtRootClose(Root);
	}
	(void)xrtFileDelete(sPath);
	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/static_file/main.c -lws2_32 -lbcrypt -ladvapi32 -liphlpapi(输出静态服务装配结果并正常退出)

Что только что произошло. (1) Статический слой ставит отображение «путь → файл» в систему маршрутов — регистрация одного файла (класс favicon) и отображение префикса каталога (/static/*) имеют свои входы; суждение расширение→MIME встроено. (2) Файловые ответы идут через файловый источник ReplyBody главы 104 — без чтения в память, ноль копий на транспортном слое (форма памяти сервиса больших файлов не зависит от размера файла). (3) Условные запросы (If-None-Match/If-Modified-Since) и Range согласуются внутри слоя — 304 и 206 автоматические пути; examples/http/static_path (отображение каталога и защита от выхода), static_multipart_body (локально собранные multipart-ответы), server_file (форма работающего сервиса) покрывают остальные грани статического семейства.

105.4契约

  • Луковичный порядок: вход в порядке регистрации, выход в обратном; сперва middleware, потом маршруты; после заморозки — совместное чтение (фиксированный Router и Host Mux на одном пути диспетчеризации).
  • Семантика Next: действительна в синхронном стеке, максимум один раз; возврат = возврат диспетчеризации ≠ завершение I/O; повтор = путь ошибки.
  • Замыкание цепи: не вызвать Next — замкнуть — ответить напрямую или удержать Connection для асинхронности; асинхронное middleware обязано замыкать, а не откладывать Next.
  • Возврат обратного вызова: true = принято; false = неустранимая ошибка → посильно 500 (при зафиксированном ответе — аварийное закрытие).
  • Проброс параметров: middleware видят те же Params, что и финальный маршрут; дескриптор и Next только заимствуют текущий стек.
  • Глубина несовпавших: с middleware тело несовпавших сперва отбрасывается, затем входит в цепь (журнал видит 404/405/OPTIONS); без — прямой ответ на стадии Headers.
  • Потолки и атомарность: число middleware ограничено MaxRoutes; рост регистрации атомарен; UseOwned освобождает в обратном порядке регистрации.
  • Статический слой: один файл/отображение каталога/защита от выхода/MIME/условные запросы/Range встроены; файловые ответы идут путём без копий.
  • Обрезка: HTTP_SERVER_MIDDLEWARE/HTTP_SERVER_STATIC/HTTP_SERVER_FILE независимые макросы.

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

Ловушка 1: сохранить Next и позвать её асинхронно:Ловушка 1: сохранить Next и позвать её асинхронно

Симптом: перемежающиеся сбои машины состояний или MIDDLEWARE_ERROR_NEXT — обратный вызов асинхронного завершения позвал Next, сохранённый давно.

Причина: xhttpservernext — дескриптор кадра синхронного стека. Правильная асинхронная форма — замкнуть цепь + управлять соединением самому, а не носить синхронный дескриптор в другое место.

错误写法
static bool onMiddleware(..., xhttpservernext* pNext, ...) {
	/* хотим продолжить после асинхронного lookup-а кэша */
	g_SavedNext = pNext;               /* сохранили дескриптор: кадр стека давно мёртв */
	async_lookup(later_call_next);     /* вызов позже: неопределённое поведение */
}
正确写法
static bool onMiddleware(xhttpserver* pS, xhttpconn* pC,
		const xhttpserverrequest* pReq, const xhttprouteparam* pP,
		size_t iP, xhttpservernext* pNext, ptr pD) {
	/* замыкание: без вызова Next; удерживаем соединение для асинхронного решения */
	async_cache_lookup(pC, pReq);
	return true;   /* этот слой принял - ответ фиксируется асинхронным путём */
}
Ловушка 2: хронометраж ответа по возврату Next:Ловушка 2: хронометраж ответа по возврату Next

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

Причина: возврат Next = возврат диспетчеризации следующего слоя; когда байты ответа дописаны — другой вопрос. Полный хронометраж — по пути завершения ответа Connection (события Close/осушения ответа).

错误写法
uint64_t t0 = now();
Next(pNext);
log_duration(now() - t0);   /* меряет время стека, а не время запроса */
正确写法
/* до: пометить старт (хранить по идентификатору запроса); парный хронометраж в обратном вызове завершения ответа */
on_request_start(pConn);    /* записать */
Next(pNext);
/* момент завершения сверяется на пути завершения ответа (например, финальное состояние перед Close) */
Ловушка 3: забыть границу защиты в статическом отображении каталога:Ловушка 3: забыть границу защиты в статическом отображении каталога

Симптом: запросы вроде /static/../../etc/passwd действительно читают системные файлы — самописное отображение склеивает пути строками без нормализации.

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

错误写法
snprintf(Path, "%s/%s", RootDir, UrlPath);   /* склейка без проверки */
send_file(Path);                              /* ../ прорывается насквозь */
正确写法
/* брать отображение каталога статического слоя: защита от выхода/нормализация встроены */
/* или при самостоятельном управлении: сперва нормализация (UrlPathPathNormalize, глава 102), потом проверка префикса */
if ( !path_inside_root(RootDir, Normalized) ) {
	reply_404();
}

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

Базовое: проверка трёхслойной луковицы

Зарегистрируйте middleware журналирования, хронометража и CORS, оборачивающие один маршрут, — печатайте метки до и после каждого слоя и наблюдайте порядок входа/выхода. Критерии приёмки: последовательность вывода строго «вход в порядке регистрации, выход в обратном»; после замыкания любого слоя внутренние слои больше не исполняются.

Продвинутое: middleware авторизации

Реализуйте middleware auth_required: проверяйте заголовок Authorization; отсутствует/недействителен — прямо 401 (замыкание); действителен — разбор токена в контекст запроса для маршрута. Сверьте с семейством схем аутентификации главы 106. Критерии приёмки: защищённый маршрут без токена 401, с токеном достижим; при замыкании middleware маршрут не исполняется.

Вызов: статический сервис с кэшем

Отображение каталога + согласование в middleware: ETag-middleware (генерация на первом ответе, сравнение на условных запросах — 304) + Range статического слоя. curl-ом проверьте три пути (первый 200, повторный 304, докачка 206). Критерии приёмки: статусы и байты трёх классов ответов верны; 304 без тела; фрагменты Range совпадают со смещениями файла.

Итоги

ТемаКратко
Луковичная модельвход в порядке регистрации, выход в обратном; сперва middleware, потом маршруты; Freeze — совместное чтение
Семантика Nextдействительна в синхронном стеке, максимум один раз; возврат = возврат диспетчеризации ≠ завершение I/O
Замыкание цепибез вызова Next — ответить напрямую или удержать соединение для асинхронности; асинхронное middleware обязано замыкать
UseOwnedответственность Release переходит к Router; разрушение последней ссылки освобождает в обратном порядке регистрации
Возврат обратного вызоваtrue принимает / false неустранимо → посильно 500
Проброс параметровmiddleware и маршруты видят одни и те же Params; дескриптор заимствует только текущий стек
Глубина несовпавшихс middleware: тело сперва отбрасывается, потом в цепь; без: прямой ответ на стадии Headers
Статический слойодин файл/отображение каталога/защита от выхода/MIME/условные запросы/Range; ответы без копий
Единство диспетчеризациификсированный Router и Host Mux на одном пути; горячий путь без блокировок
ОбрезкаMIDDLEWARE/STATIC/FILE независимые макросы

📖 Справочники API в этой главе:xhttp-http_server_middleware (4 записей)xhttp-http_server_static (6 записей)xhttp-http_server_router (17 записей)

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