Конфігурація TrueAsync Server
(PHP 8.6+, true_async_server 0.6+)
Уся конфігурація сервера задається через об'єкт TrueAsync\HttpServerConfig до виклику new HttpServer($config). Після того, як HttpServer створено, конфіг заморожується: будь-які сетери на ньому кидатимуть HttpServerRuntimeException.
use TrueAsync\HttpServer;
use TrueAsync\HttpServerConfig;
use TrueAsync\LogSeverity;
$config = (new HttpServerConfig())
->addListener('0.0.0.0', 8080)
->addListener('0.0.0.0', 8443, tls: true)
->addHttp3Listener('0.0.0.0', 8443)
->setCertificate('/etc/tls/server.crt')
->setPrivateKey('/etc/tls/server.key')
->setWorkers(4)
->setKeepAliveTimeout(60)
->setMaxBodySize(50 * 1024 * 1024)
->setCompressionEnabled(true)
->setLogSeverity(LogSeverity::INFO)
->setLogStream(STDERR);
$server = new HttpServer($config);Сетери повертають static, тому конфіг будується ланцюжком.
Listeners
Сервер може слухати довільну кількість TCP/Unix-сокетів і UDP-портів (для HTTP/3) одночасно.
| Метод | Що робить |
|---|---|
addListener($host, $port, $tls = false) | TCP, HTTP/1.1 + HTTP/2 (h2c за preface на plaintext, h2 через ALPN на TLS) |
addHttp1Listener($host, $port, $tls = false) | TCP, лише HTTP/1.1. Клієнт з HTTP/2 preface отримає 400 |
addHttp2Listener($host, $port, $tls = false) | TCP, лише HTTP/2. Без TLS це h2c з обов'язковим preface |
addHttp3Listener($host, $port) | UDP, HTTP/3 / QUIC. TLS 1.3 увімкнено автоматично, використовується сертифікат сервера |
addUnixListener($path) | Unix-сокет, HTTP/1.1 + HTTP/2 (стиль h2c) |
$config
->addListener('0.0.0.0', 80) // H1 + H2c
->addListener('0.0.0.0', 443, tls: true) // H1 + H2 over TLS
->addHttp3Listener('0.0.0.0', 443); // H3 / QUIC на тому самому портуДля phased rollout HTTP/3 можна тимчасово вимкнути анонс Alt-Svc:
$config->setHttp3AltSvcEnabled(false);TLS
$config
->setCertificate('/etc/tls/server.crt')
->setPrivateKey('/etc/tls/server.key');Сертифікат і ключ спільні для всіх TLS-listeners (включно з HTTP/3). TLS 1.2/1.3, ALPN, слабкі шифри вимкнено, stateless session tickets, safe renegotiation вимкнено.
Workers і bootloader
setWorkers(1) (значення за замовчуванням) вмикає single-threaded режим: start() крутить event-loop на потоці, що викликає.
setWorkers(N > 1) піднімає вбудований пул з N потоків через Async\ThreadPool. Кожен воркер re-bind'ить ті самі listeners, ядро (Linux/BSD) розподіляє accept через SO_REUSEPORT. Батьківський start() чекає завершення всіх воркерів.
$config
->setWorkers(4)
->setBootloader(function () {
// виконується один раз у кожному воркері перед таск-loop
require __DIR__ . '/vendor/autoload.php';
Database::warmupPool();
OpcacheWarm::compile();
})
->setRequestScope(true); // дефолт; false економить 2 алокації/запит, але занулює request_context()Hot reload
Замінюйте когорту воркерів, не роняючи жодного з'єднання (лише pool-режим). Обидва тригери викликають HttpServer::reload(), який заново запускає bootloader у свіжих воркерах на тих самих сокетах:
$config
->enableHotReload([__DIR__ . '/app'], ['php'], debounceMs: 300, maxHoldMs: 2000) // стежити за файлами (dev)
->enableReloadOnSignal(); // SIGHUP (prod)Подробиці: Multi-worker.
Таймаути
| Метод | За замовчуванням | Що таймаутить |
|---|---|---|
setReadTimeout($sec) | — | приймання запиту цілком |
setWriteTimeout($sec) | — | надсилання відповіді |
setKeepAliveTimeout($sec) | — | idle між запитами; 0 вимикає keep-alive |
setShutdownTimeout($sec) | — | graceful shutdown: скільки чекати активні запити |
Ліміти і backpressure
$config
->setBacklog(1024)
->setMaxConnections(50_000)
->setMaxInflightRequests(10_000)
->setMaxBodySize(10 * 1024 * 1024)
->setBackpressureTargetMs(10);setMaxConnections($n)— жорстка межа кількості TCP-з'єднань.0знімає обмеження.setMaxInflightRequests($n)— admission control: після цього числа активних обробників нові запити отримують швидку відмову. H1 → 503 +Retry-After: 1, H2 →RST_STREAM REFUSED_STREAM(retry-safe за RFC 7540 §8.1.4). На H2 жорстка межа на з'єднаннях не допомагає, бо нові streams приїздять по вже прийнятому з'єднанню.0бере значенняmax_connections × 10.setMaxBodySize($bytes)— максимум на тіло запиту. За замовчуванням 10 MiB, діапазон 1 KiB..16 GiB. H1 віддає 413 і закриває з'єднання; H2 шлеRST_STREAM(INTERNAL_ERROR).setBackpressureTargetMs($ms)— поріг CoDel sojourn для accept-side backpressure. Коли per-request queue-wait тримається вище порогу 100 ms поспіль, listen-сокет ставиться на паузу.0вимикає CoDel. За замовчуванням 5 ms; для типового web 10–20 ms; для повільних обробників (БД, IO) 50–100 ms.
Graceful drain (Step 8)
Керування міграцією навантаження за L4-балансувальником:
| Метод | Дефолт | Призначення |
|---|---|---|
setMaxConnectionAgeMs($ms) | 0 (off) | Після ±10% jitter ліміту з'єднання отримує Connection: close (H1) або GOAWAY (H2). Аналог gRPC MAX_CONNECTION_AGE. Production: 600_000 (10 хв). |
setMaxConnectionAgeGraceMs($ms) | 0 | Hard-close після Connection: close/GOAWAY. 0 вимикає force-close таймер. |
setDrainSpreadMs($ms) | 5000 | Вікно рівномірного спреду per-connection drain при CoDel trip / hard-cap (анти-thundering-herd). |
setDrainCooldownMs($ms) | 10_000 | Мінімальний gap між реактивними drain-тригерами. |
Ліміти HTTP/2 streaming
$config
->setStreamWriteBufferBytes(256 * 1024) // 256 KiB per stream, 4 KiB .. 64 MiB
->setH2StaticBudgetMax(0); // 0 = auto (memory_limit / 8)HttpResponse::send($chunk) блокує корутину обробника лише під backpressure: коли per-stream staging buffer заповнений. За замовчуванням 256 KiB (для порівняння: gRPC-Go 64 KiB, Envoy 1 MiB, Node.js 16 KiB).
HTTP/3 production knobs
$config
->setHttp3IdleTimeoutMs(30_000) // RFC 9000 §10.1
->setHttp3StreamWindowBytes(256 * 1024) // per-stream flow control
->setHttp3MaxConcurrentStreams(100) // initial_max_streams_bidi
->setHttp3PeerConnectionBudget(16) // per-source-IP cap, slow-loris захист
->setHttp3SocketBufferBytes(8 << 20) // UDP rcv/snd буфер, поглинає вхідні сплески
->setHttp3Pacing(false) // opt-in send pacing, для lossy/rate-limited шляхів
->setHttp3AltSvcEnabled(true); // RFC 7838 Alt-Svc анонсConnection-level initial_max_data виводиться як window × max_concurrent_streams (патерн nginx).
setHttp3SocketBufferBytes($bytes)— UDP receive/send буфер сокета. Поглинає вхідні сплески, щоб вони не переливалися вRcvbufErrors. За замовчуванням 8 MiB; ядро обрізає доnet.core.{r,w}mem_max, якщо немає привілеїв.0лишає значення ОС за замовчуванням.setHttp3Pacing($bool)— обмежувати кожен сплеск доsend_quantumcongestion controller'а і розкладати пакети на pacing-таймері ngtcp2. Вимкнено за замовчуванням: на безвтратному шляху pacing лише додає вартості, тож вмикайте його тільки для обмежених шляхів.
WebSocket
$config
->setWsMaxMessageSize(1024 * 1024) // 1 MiB, 128 .. 256 MiB
->setWsMaxFrameSize(1024 * 1024) // 1 MiB, той самий діапазон
->setWsPingIntervalMs(30_000) // keepalive PING на простої
->setWsPongTimeoutMs(60_000) // дедлайн на відповідний PONG
->setWsPermessageDeflate(false) // RFC 7692, вимкнено за замовчуванням
->setWsMaxSubscriptions(0) // ліміт топік-фільтрів на з'єднання; 0 = без ліміту
->setWsPublishRateLimit(0); // token bucket над publish(); 0 = offsetWsMaxMessageSize($bytes)— максимум на пересібране повідомлення. Перевищення дає1009 Message Too Bigі розрив з'єднання (RFC 6455 §7.4.1).setWsMaxFrameSize($bytes)— максимум на один фрейм. Захищає від fragment-flood, коли клієнт шле мільйони крихітних фрагментів.setWsPingIntervalMs($ms)— як часто сервер сам шле PING на з'єднаннях, що простоюють.0вимикає автоматичний ping.setWsPongTimeoutMs($ms)— скільки чекати PONG після PING, перш ніж вважати з'єднання мертвим і закрити його кодом1001 GoingAway.0вимикає таймаут.setWsPermessageDeflate($bool)— RFC 7692, стиснення на рівні повідомлень. Вимкнено за замовчуванням: це свідомий opt-in, бо стиснення коштує CPU і розширює поверхню атак decompression-bomb. Узгоджується лише коли клієнт сам пропонує це розширення; потребує збірки з zlib.setWsMaxSubscriptions($count)— скільки окремих топік-фільтрів може тримати одне з'єднання.0(за замовчуванням) — без ліміту, як постачається кожен self-hosted брокер. Задавайте його, коли клієнтський ввід доходить доsubscribe(); понад лімітsubscribe()кидаєWebSocketException.setWsPublishRateLimit($perSecond, $burst = 0)— per-connection token bucket надpublish(), єдиним WS-викликом, що спричиняє роботу на кожному воркері.0(за замовчуванням) — off. Понад лімітpublish()кидаєWebSocketBackpressureException.
Модель топіків pub/sub дивіться в керівництві по WebSocket, а сам API з'єднання — у довіднику.
Body streaming
Вмикає pull-based стрім тіла запиту (issue #26): парсери H1/H2 кладуть чанки в чергу, обробник читає їх через HttpRequest::readBody() без утримання всього тіла в RAM.
$config->setBodyStreamingEnabled(true);
$server->addHttpHandler(function ($req, $res) {
while (($chunk = $req->readBody()) !== null) {
// обробити чанк (наприклад, потоковий запис на диск, парсинг)
}
$res->setStatusCode(204);
});Без setBodyStreamingEnabled(true) обробник отримує вже повністю прочитане тіло через getBody(); readBody() у цьому режимі недоступний.
Порівняння для 50 паралельних 20-MiB POST'ів (h2load, WSL2): peak RSS падає 1170 MiB → 197 MiB (×6), пропускна спроможність 36 req/s → 100 req/s (×2.7), бо dispatch обробника більше не чекає повного тіла.
Див. також Стрімінг.
Auto-await body
$config->setAutoAwaitBody(true); // дефолт: trueКоли увімкнено, non-multipart запити чекають повного тіла перед викликом обробника (multipart завжди стрім). Корисно для класичної обробки тіла цілком.
JSON
$config->setJsonEncodeFlags(JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);Ці прапорці застосовуються до HttpResponse::json(), коли той, хто викликає, не передав $flags явно. JSON_THROW_ON_ERROR мовчки стрипається: помилка кодування дає 500 з JSON-тілом помилки, виняток не пробрасується в обробник.
Логування і статистика
Для одного консольного стріму цукру setLogSeverity() / setLogStream() достатньо:
use TrueAsync\LogSeverity;
$config
->setLogSeverity(LogSeverity::INFO)
->setLogStream(STDERR); // будь-який php_stream: файл, php://stderr, php://memory, user wrapperЛогер вимкнено за замовчуванням (LogSeverity::OFF). Рівні (OpenTelemetry SeverityNumber):
| Рівень | Що потрапляє |
|---|---|
OFF (0) | нічого |
DEBUG (5) | трасування H3-пакетів і т. п. |
INFO (9) | server lifecycle (start/stop), bind retries |
WARN (13) | TLS handshake fail, peer reset, absorbed exceptions |
ERROR (17) | listener bind failed, hard protocol errors |
FATAL навмисно відсутній: він потрапляє через zend_error_noreturn(E_ERROR), який і так перериває процес.
Під пулом воркерів не використовуйте
setLogStream(). Stream-ресурс, відкритий батьком, не може перетнути межу воркер-потоку. ВикористовуйтеsetLogSinks()із sink'омfile/stdout/stderr, який кожен воркер може відкрити сам.
Для кількох призначень, структурованого access log, syslog чи JSON-виводу використовуйте setLogSinks() — і ввімкніть cross-worker getStats() через setStatsEnabled(true):
use TrueAsync\LogSeverity;
$config
->setStatsEnabled(true)
->setLogSinks([
['type' => 'file', 'path' => '/var/log/app/access.log',
'format' => 'json', 'category' => 'access', 'level' => LogSeverity::INFO],
['type' => 'stderr', 'format' => 'pretty', 'level' => LogSeverity::WARN],
]);Обидва повністю розписані на сторінці Спостережуваність.
Телеметрія (W3C Trace Context)
$config->setTelemetryEnabled(true);Коли увімкнено, вхідні traceparent / tracestate парсяться і прикріплюються до запиту. В обробнику доступні:
$req->getTraceParent(); // raw header
$req->getTraceState();
$req->getTraceId(); // 32 lower-hex chars
$req->getSpanId(); // 16 lower-hex chars
$req->getTraceFlags(); // int (0x01 = sampled)Повний довідник
Див. TrueAsync\HttpServerConfig: усі 60+ методів з детальним описом і валідними діапазонами значень.