Маршрутизация: статическая таблица + динамические pattern + слоты методов
Хост не поставляет роутер — это дизайн, а не недостача. Глава отдаёт вам каркас маршрутизации, проверенный в demo-single и обкатанный routebench на десяти тысячах маршрутов: две таблицы, одна компиляция, три возврата.
Обзор
17.1 Введение
«Забирать ли роутер в хост?» — вопрос настоящий: забрать — и гибкие потребности путей (регулярки, префиксы, wildcards) начинают давить на конфигурационную систему; оставить — каждое приложение пишет своё. Выбор XServer — давать детали, а не изделие: xrtMap (хэш-таблица) для точных совпадений, xrtPattern (пакетно прекампилируемый сопоставитель) для параметризованных путей — двадцать строк сборки дают продакшн-роутер, а особые потребности (регулярки, версионные префиксы) прорастают в любой момент.
17.2 Паттерн двух таблиц
static void RouteHTTP_Init(void)
{
G_StaticRouteTableHTTP = xrtMapCreate(sizeof(RouteInfoHTTP));
/* статическая таблица: точные URI (хэш xrtMap) */
AddStaticRouteHTTP("/api/v1/items", XHTTP_METHOD_GET, Handle_API_List);
AddStaticRouteHTTP("/api/v1/items", XHTTP_METHOD_POST, Handle_API_Add);
/* динамическая таблица: захват сегмента {id} (компилируется после регистрации) */
AddDynamicRouteHTTP("/api/v1/items/{id}",
XHTTP_METHOD_GET, Handle_API_Item);
AddDynamicRouteHTTP("/api/v1/items/{id}",
XHTTP_METHOD_PUT | XHTTP_METHOD_PATCH, Handle_API_Item_Update);
AddDynamicRouteHTTP("/api/v1/items/{id}",
XHTTP_METHOD_DELETE, Handle_API_Item_Delete);
/* захват внутри сегмента тоже работает: /api/v1/file/{name}.txt */
AddDynamicRouteHTTP("/api/v1/file/{name}.txt",
XHTTP_METHOD_GET, Handle_API_File);
/* динамические маршруты обязаны компилироваться один раз после всей регистрации */
(void)RouteHTTP_Compile();
}
Что только что произошло: статическая таблица попадает за O(1); динамическая внутри RouteHTTP_Compile через xrtPatternCompileMany пакетно компилируется в неизменяемый сопоставитель — дальше стоимость сопоставления зависит только от длины пути и никогда от числа маршрутов. Сигнатура обработчика — конвенция демо: (XS_HttpReq*, const RouteParamHTTP* arrParam, uint32 iParamCount), arrParam несёт представления захваченного {id}.
17.3 Слоты методов и 405
- Каждая пара (URI, метод) — слот; один URI с несколькими методами регистрируется раздельно; точное повторение URI и метода перекрывает старый колбэк с предупреждением
- Маски методов комбинируются свободно:
PUT | PATCHделят слот;XHTTP_METHOD_CRUD(пять методов) иXHTTP_METHOD_ANY(включая расширения) — агрегатные макросы (справочник перечислений) - URI попал, метод нет → 405 + заголовок Allow: Allow собирается из всех методов, зарегистрированных на этом URI, — источник
HEAD /json → 405, Allow: GETиз главы 2
GET | HEAD — ваш ReplyRaw уже единообразно не шлёт тело для HEAD (глава 15), поэтому большинство обработчиков поддерживают HEAD без единой правки.17.4 Порядок сопоставления и производительность
Цифры производительности (routebench, 3 раунда × 10 000 запросов, среднее):
| Число маршрутов | Задержка XServer | Сравнение |
|---|---|---|
| 10 | 42,0 мкс | Go 42,2 |
| 10 000 | 59,1 мкс (+41%) | PHP +492% / FastAPI +935% |
Секрет плоскости: статическая таблица — хэш O(1); динамическая после пакетной компиляции сопоставляет за O(длины пути) — десять тысяч маршрутов дороже десяти лишь на 41%, тогда как сравниваемые стеки на десяти тысячах деградируют на порядок.
17.5 Контракт
- Компиляция: после регистрации динамических маршрутов обязателен один Compile; дальше таблица неизменяема (пересборка при смене поколения — работа горячей перезагрузки).
- Порядок: статические → динамические → FALLBACK; при одном URI статическое приоритетнее динамического.
- Параметры: arrParam — представление уровня запроса, в колбэке используется и выбрасывается.
Частые ошибки
- Симптом
- Все динамические маршруты 404/падают в статику; статические работают.
- Причина
- Динамическая таблица становится сопоставимым неизменяемым pattern только после Compile — регистрация лишь собирает. Демо ставит Compile в конец RouteHTTP_Init с комментарием «обязательно».
- Симптом
- Маршруты-всё-видишь вроде "/api/{a}/{b}/{c}" перехватывают трафик друг друга и попадают не в свои обработчики.
- Причина
- pattern сопоставляется точными сегментами (не префиксы и не регулярки); но сваливание многих ресурсов в один паттерн размывает семантику. Разные ресурсы — разные pattern.
Упражнения
Реализуйте полный REST для /api/v1/users и /api/v1/users/{id} (GET список / POST создание / GET элемент / PUT+PATCH обновление / DELETE удаление + HEAD списка).
Через -v пронаблюдайте несколько запросов «URI попал, метод нет» и сверьте заголовок Allow со своими регистрациями.
Скриптом сгенерируйте 1000 статических + 1000 динамических маршрутов (см. генератор tools/routebench); замерьте время компиляции при старте и задержку запросов.
Итоги
| Тезис | Вывод |
|---|---|
| Две таблицы | статическая xrtMap O(1) + динамическая xrtPattern, плоско после компиляции |
| Слоты методов | слот на (URI × метод); маски комбинируются; агрегаты CRUD/ANY |
| 405 | URI попал, метод нет → 405 + Allow (объединение зарегистрированных методов) |
| Порядок | статика → динамика → FALLBACK → статический слой / 404 |
| Замеры | 10k маршрутов 59,1 мкс (+41%); сравниваемые стеки деградируют на порядок |
- справочник xrt · pattern — полные сигнатуры CompileMany/Lookup и представления захвата
- справочник xrt · regex — пакетная прекампиляция RegexSet для регулярных маршрутов