Анатомия минимального приложения: два файла — один сервис
Приложение XServer, которое запускается, отвечает на запросы и откатывается к статике, в минимуме — это xs.json и main.c. В этой главе они разобраны построчно — это фундамент всего вашего будущего кода.
Обзор
4.1 Введение
«Hello world» большинства фреймворков проходит сквозь пять слоёв абстракции: регистрация входа → сопоставление маршрутов → цепочка middleware → объект контекста → сериализация ответа. Философия XServer противоположна: сначала выдать минимальные факты протокола — разобранный заголовок запроса, указатели транспорта, автомат кадрирования — и отойти в сторону. Поэтому минимальное приложение действительно минимально:
Ниже — построчный разбор этих двух фактов. Они взяты из реальных файлов demo-single, из них лишь вырезана бизнес-часть.
4.2 xs.json построчно
{
"services": [
{
"enabled": true, /* выключатель сервиса */
"class": "http", /* класс протокола: http/ws/tcp/udp/custom */
"name": "demo", /* глобально уникальное имя, ключ xsServerFind */
"ip": "127.0.0.1",
"port": 9080,
"host_default": { /* хост по умолчанию: откат, когда vhost не совпал */
"enabled": true,
"name": "app",
"path": "wwwroot", /* корень статики */
"devlang": "c", /* c = скрипт TCC; static = чистая статика */
"devfile": "main.c" /* вход скрипта (относительно каталога конфига) */
}
}
]
}
Что только что произошло: конфигурация объявляет цепочку фактов — слушать HTTP на 127.0.0.1:9080; запросы, не попавшие в другие vhost, уходят хосту по умолчанию app; корень статики app — wwwroot; бизнес-логика в main.c. Любое неверное поле (опечатка в class, порт вне диапазона) ломает старт до строки config loaded и называет конкретное поле — полная таблица полей в руководстве · конфигурация, том 2 разбирает каждое.
4.3 main.c построчно
#include <xsbase.h> /* контрактный заголовок: подтягивает xrt_decl.h и libtcc.h (всё в VFS) */
/* вызывается один раз после загрузки поколения (управляющий поток сборки / reload) */
static void ServiceInit(XS_HostInfo* pHost)
{
printf("[%s] service init\n", pHost->Name);
}
/* по разу на запрос (worker-потоки движка, возможна конкурентность) */
static XS_RequestResult RequestProc(XS_HttpReq* pReq)
{
/* необработанные запросы → откат в статический слой (wwwroot), остальное 404 */
return XS_FALLBACK;
}
/* вызывается при утилизации поколения (GC-поток, не более одного раза за жизнь) */
static void ServiceUnit(XS_HostInfo* pHost)
{
printf("[%s] service unit\n", pHost->Name);
}
Что только что произошло:
#include <xsbase.h>не требует установки — заголовок вместе с полными объявлениями xrt лежит в VFS внутри exe, TCC читает его напрямую;- хост ищет
ServiceInit / RequestProc / ServiceUnitпо имени символа (static не прячет — в памяти-режиме TCC символы видны), все они необязательны; - три функции ровно соответствуют трём моментам поколения из главы 3: рождение / обслуживание / утилизация.
Запустим (вывод с реальной машины):
$ ./xs [xs] config loaded: .../xs.json (1 servers) [xs] script loaded: .../main.c (0.4 KB gen 1) [app] service init [xs] server 'demo' http bound on 127.0.0.1:9080 [xs] running, press Ctrl+C to stop $ curl -s http://127.0.0.1:9080/ <!DOCTYPE html> ... /* wwwroot/index.html — / забрал статический слой */
4.4 Трёхзначный возврат
Возврат RequestProc — это не «успех / провал», а кто отвечает за остаток жизни запроса:
| Возврат | Смысл | Что драйвер делает дальше |
|---|---|---|
XS_OK | Я написал полный ответ | Вычитывает остаток body и продолжает цикл keep-alive |
XS_FALLBACK | Я не обработал | GET/HEAD падают в статический слой; остальные методы 404 |
XS_TAKEOVER | Я забираю соединение целиком | Драйвер отходит; приложение само принимает/отправляет и в конце Close (глава 25) |
4.5 Откат в статический слой
После XS_FALLBACK в дело вступает статический слой хоста: он ищет файлы в path хоста (здесь wwwroot) — GET отдаёт содержимое, HEAD только заголовки, с percent-decode, индексами каталогов (по умолчанию index.html) и отказом для файлов с точкой. Так / выше и получил index.html. Четыре ручки статического слоя (свои заголовки, страницы ошибок, индексы, deny_dotfiles) открываются в главе 10 тома 2.
Связка «сначала скрипт, статика как подстраховка» избавляет от кода для статики — и от правил рерайта для динамических маршрутов.
4.6 Контракт
- Символы: контрактные символы ищутся по имени, все необязательны — хост без экспортированного RequestProc просто чисто статический сайт.
- Владение:
XS_HttpReqи указатели внутри действительны только в стеке колбэка; в нашем примере голый return безрисков, но как только начнёте пользоваться представлениями — работают правила главы 3. - Ответственность: вернуть XS_OK — значит «полный ответ написан»: заголовок + тело (кроме HEAD) в зоне вашей ответственности.
Частые ошибки
- Симптом
- Часть запросов «случайно» ломается: пустые ответы, зависания или обрывы соединений.
- Причина
- Функция не-void, упавшая с конца, — неопределённое поведение C: возврат — случайный мусор со стека, трёхзначность интерпретируется как попало.
static XS_RequestResult
RequestProc(XS_HttpReq* pReq)
{
if ( path is mine ) ...;
/* нет return: мусор случайно попадает в состояние */
}static XS_RequestResult
RequestProc(XS_HttpReq* pReq)
{
if ( path is mine ) return XS_OK;
return XS_FALLBACK; /* явная концовка */
}- Симптом
- Клиент не может разобрать ответ; на keep-alive ломаются последующие запросы.
- Причина
- Строка пишется прямо в
pReq->tcpкак ответ — Content-Length, семантика HEAD и автомат соединения не поддерживаются.
xrtNetStreamSend(pReq->tcp,
"HTTP/1.1 200 OK\n\nhello", 22);
return XS_OK; /* сообщение неполное, без длины *//* заголовок строит xrtHttp1ResponseWrite
* (глава 15 сделает полную обёртку) */- Симптом
- В тело ответа подмешивается содержимое файла; браузер сообщает об ошибке декодирования.
- Причина
- XS_FALLBACK велит статическому слою продолжить писать в то же соединение; ваши полбайта стали префиксом сообщения.
ConnSend(pReq, head, n);
if ( !hasBody ) return XS_FALLBACK;/* решение принимается до записи:
* либо полный ответ + XS_OK,
* либо ничего не пишем + FALLBACK */Упражнения
Не подглядывая в главу, создайте каталог и напишите xs.json + main.c; запустите и проверьте curl'ом, что / отдаёт статическую страницу, а /nope — 404.
service init и gen 1; оба запроса curl ведут себя ожидаемо.Подсказки: возьмите time() и strftime() из стандартной библиотеки C (доступны в окружении TCC); для ответа пока хватит предельно простого заголовка (важен корректный Content-Length), глава 15 всё формализует.
curl -s http://127.0.0.1:9080/time возвращает текущее время, и curl разбирает сообщение без мусора в хвосте.Верните тело POST-запроса на /echo как есть. Подсказки: читайте pReq->body (API чтения тела фиксированной длины — xrt: xrtHttp1BodyRead); если целиком тяжело — начните с маленьких тел.
curl -d "hello xs" .../echo возвращает hello xs дословно; проверьте на 50 и на 5000 байтах.Итоги
| Тезис | Вывод |
|---|---|
| Минимальное приложение | xs.json (факт сборки) + main.c (факт бизнеса), и больше ничего |
| Контрактные символы | Поиск по имени, все необязательны; Init/Request/Unit — три момента поколения |
| Трёхзначный возврат | XS_OK отвечено / FALLBACK в статику / TAKEOVER забрать соединение |
| Статический слой | Забирает GET/HEAD после FALLBACK; четыре ручки — том 2 |
| Заголовки | xsbase.h и все объявления в VFS внутри exe, ноль установки |
- справочник xrt · HTTP/1.1 — полные сигнатуры ResponseWrite / BodyRead
- учебник xrt · HTTP-сервер — как библиотека собирает HTTP (сравните с тем, что делает за вас хост XServer)