Глава 4 / 51
Том 1 · Старт и ментальная модель

Анатомия минимального приложения: два файла — один сервис

Приложение XServer, которое запускается, отвечает на запросы и откатывается к статике, в минимуме — это xs.json и main.c. В этой главе они разобраны построчно — это фундамент всего вашего будущего кода.

Обзор

Позиционирование: сжимаем «работающее демо» главы 2 до минимальной понятной единицы. После прочтения вы сможете написать пустое приложение XServer не подглядывая. Кто уже писал маршруты демо — смотрите в первую очередь 4.4 (трёхзначный возврат) и ошибку ①: это самая частая яма новичков.

4.1 Введение

«Hello world» большинства фреймворков проходит сквозь пять слоёв абстракции: регистрация входа → сопоставление маршрутов → цепочка middleware → объект контекста → сериализация ответа. Философия XServer противоположна: сначала выдать минимальные факты протокола — разобранный заголовок запроса, указатели транспорта, автомат кадрирования — и отойти в сторону. Поэтому минимальное приложение действительно минимально:

xs.json объявляет «где слушать, где скрипт» (факт сборки) main.c объявляет «что делать с запросом» (факт бизнеса) └──── два факта — один сервис ────┘

Ниже — построчный разбор этих двух фактов. Они взяты из реальных файлов demo-single, из них лишь вырезана бизнес-часть.

4.2 xs.json построчно

xs.json проверено на реальной машине источник: release/demo-single/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 построчно

main.c проверено на реальной машине минимизировано из demo-single/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)
Три состояния взаимоисключающие и обязательные: перед XS_OK ответ действительно должен быть дописан; если в соединение уже записаны байты — возвращать XS_FALLBACK нельзя (статический слой допишет вторую копию и сломает сообщение). Драйвер сверяет заявленные скриптом потреблённые байты с фактически ушедшими в сеть; расхождение рвёт соединение.

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) в зоне вашей ответственности.

Частые ошибки

Забыт return (неопределённое поведение)
Симптом
Часть запросов «случайно» ломается: пустые ответы, зависания или обрывы соединений.
Причина
Функция не-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 сделает полную обёртку) */
Полответа, а потом FALLBACK
Симптом
В тело ответа подмешивается содержимое файла; браузер сообщает об ошибке декодирования.
Причина
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

Подсказки: возьмите time() и strftime() из стандартной библиотеки C (доступны в окружении TCC); для ответа пока хватит предельно простого заголовка (важен корректный Content-Length), глава 15 всё формализует.

Приёмка: curl -s http://127.0.0.1:9080/time возвращает текущее время, и curl разбирает сообщение без мусора в хвосте.
ВызовЗародыш /echo

Верните тело 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