Ориентация главы
После шести клиентских глав (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 отправки — фиксированный буфер отправки на каждое соединение не резервируется); приёмная память управляется предоставляемым по надобностиxnetbufTCP и жёсткими потолками. - Network: полностью открыты несколько конечных точек TCP Server, общие динамические порты, accept-очередь, reuse-port —
xrtHttpServerLocalвозвращает реальные конечные точки (стандартная поза «порт прослушивания 0», как в главе 66);xrtHttpServerNetworkодалживает ссылку на нижележащийxnetserver(низкоуровневая статистика для особых сценариев; после использованияxrtNetServerDestroy).
Цепочка событий и стратегия тела
Три ключевые семантики. (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 — минимальный полный сервис:
#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; }
Что только что произошло. (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 — комбинация маршрутизации и прямого ответа:
#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; }
Что только что произошло. (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 — независимые комбинируемые слои; в ядре нет второй машины состояний.
Типичные ошибки
Симптом: асинхронный потребитель читает перемешанные данные фрагментов или падает — то же висячее представление, что в главе 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) */
Симптом: клиент, оборвавший сеть посреди загрузки (без RST), занимает соединение и буферы часами — лимит соединений съедают такие зомби.
Причина: дефолт WriteTimeout может не покрывать вашу форму развёртывания (ноль = защита выключена). У «нет прогресса» стадии ответа обязан быть срок.
xrtHttpServerConfigInit(&Config); /* WriteTimeout остался дефолтным/нулём - зомби-соединения занимают место вечно */
xrtHttpServerConfigInit(&Config); Config.WriteTimeout = UINT64_C(30000000); /* 30s без прогресса ответа - бросаем */ Config.IdleTimeout = UINT64_C(75000000); /* keep-alive 75s */ /* все пять явно - защита это контракт развёртывания, не везение */
Симптом: 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 | буферизация/поток/отброс/отказ/прямой ответ; лимит уровня маршрута |
| Противодавление Body | Pause только внутри обратного вызова, фрагмент сразу недействителен; 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 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки