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 займається справою. І ось тепер, коли в сервісу є обличчя, час його оживити. Пам'ятаєте смужку прогресу з найпершого розділу першої серії, ту, що малювалася в терміналі? Вона зараз переїде в браузер. І малювати її буде сам сервер.