TrueAsync ServerСтатика

Статика

У ProfileService появился фронтенд. HTML, CSS, скрипты, аватарки — обычное дело. Раньше их раздавал nginx, но nginx мы торжественно проводили в первой главе. Кто теперь?

Первый порыв понятен: написать обработчик, который открывает файлы по URL. Остановитесь. Прикиньте, что это значит: тысячи запросов к логотипу в день, и на каждый — корутина, вход в PHP, fopen, выход. PHP здесь не делает ничего, что требовало бы PHP.

Сервер решает это радикально. Статический маршрут обслуживается целиком в C. Запрос к нему в PHP не заходит вообще:

php
use TrueAsync\StaticHandler;

$server->addStaticHandler(
    new StaticHandler('/assets/', '/var/www/profile/public')
);

Префикс URL, каталог на диске, готово. GET /assets/css/app.css превращается в асинхронное чтение файла прямо в сокет, через libuv, мимо PHP. Обработчики из прошлых глав продолжают получать всё остальное. Маунтов может быть несколько, совпадения ищутся в порядке регистрации. Весь HTTP-этикет из sendFile здесь тоже есть: Content-Type, ETag с 304, докачка по Range.

Настройка маунта

StaticHandler настраивается цепочкой, до подключения к серверу:

php
$static = (new StaticHandler('/assets/', '/var/www/profile/public'))
    ->setCacheControl('public, max-age=86400')
    ->enablePrecompressed('br', 'zstd', 'gzip')
    ->hide('*.map', 'drafts/**')
    ->setOnMissing(StaticOnMissing::NEXT);

$server->addStaticHandler($static);

Пройдёмся по строчкам.

setCacheControl — заголовок кэширования на каждый ответ. В паре со включённым по умолчанию ETag браузер скачивает файл заново только тогда, когда файл действительно изменился.

enablePrecompressed — мой любимый пункт. Если рядом с app.css лежит app.css.br, клиенту с подходящим Accept-Encoding уйдёт готовый сжатый файл. Вдумайтесь в экономику: сжали один раз на этапе сборки фронтенда, самым дорогим и качественным уровнем, а раздаёте миллион раз, не тратя на компрессию ни такта.

hide — глобы, которые получают 404 независимо от того, существует файл или нет. Карты сорцов и черновики наружу не пойдут.

setOnMissing(NEXT) — судьба запросов мимо файлов. По умолчанию промах отвечает 404 прямо из C. NEXT вместо этого передаёт запрос дальше, в обычный PHP-обработчик. Зачем? SPA. Файл /assets/app.js отдаётся с диска, а несуществующий /assets/whatever проваливается в приложение, которое ответит своим index.html.

После addStaticHandler объект запирается: сервер уже построил по нему структуры для горячего пути. Попытка дёрнуть сеттер после этого — исключение.

Безопасность по умолчанию

Небольшое лирическое отступление. Отдача файлов по URL — исторически одна из самых урожайных дыр веб-серверов. ../../etc/passwd в адресной строке — приём старше многих читателей этой главы.

Поэтому политика из коробки параноидальная. Запросы с .. — 404. Пути через файлы с точкой в начале — 404: ни .env, ни .git не утекут, даже если случайно окажутся в каталоге. Символические ссылки не разыменовываются вовсе: файл должен физически лежать внутри корня маунта, и никакой симлинк наружу его не вытащит.

Всё это ослабляется осознанно (setDotfilePolicy, setSymlinkPolicy), но умолчания подобраны так, чтобы вариант «подключил и забыл» был безопасным.

Когда файлов очень много

Для горячих маунтов есть ещё один рычаг:

php
$static->setOpenFileCache(maxEntries: 1024, ttlSeconds: 60);

Кэш запоминает разрешённый путь, метаданные и заголовки последних файлов и срезает syscall-прогулку на повторных запросах. На большом каталоге или сетевой файловой системе — ощутимо. На маленьком локальном сайте — нет, потому и выключен по умолчанию.

Вот и вся глава, я обещал короткую. Статика летит мимо PHP, PHP занимается делом. И вот теперь, когда у сервиса есть лицо, пора его оживить. Помните полоску прогресса из самой первой главы первой серии, ту, что рисовалась в терминале? Она сейчас переедет в браузер. И рисовать её будет сам сервер.