WebSocket
(PHP 8.6+, true_async_server 0.9+)
HttpServer::addWebSocketHandler() реєструє обробник full-duplex з'єднань за RFC 6455.
З'єднання починається як звичайний HTTP-запит, а потім клієнт просить сервер перемкнути його на інший протокол прямо на цьому самому TCP-з'єднанні: це і називається Upgrade. Сервер відповідає статусом 101 Switching Protocols, і далі по тому самому з'єднанню йде вже не HTTP, а WebSocket. Підтримується:
- Upgrade з HTTP/1.1 (класичний заголовок
Connection: Upgrade). - Upgrade з HTTP/2 (RFC 8441 Extended CONNECT).
wss://(WebSocket поверх TLS).- permessage-deflate (RFC 7692) — стиснення повідомлень на рівні протоколу.
Реалізація перевірена набором тестів Autobahn|Testsuite і проходить усі 246 тестів категорії
behavior.
use TrueAsync\HttpServer;
use TrueAsync\HttpServerConfig;
use TrueAsync\WebSocket;
$server = new HttpServer(
(new HttpServerConfig())
->addListener('0.0.0.0', 8080)
);
$server->addWebSocketHandler(function (WebSocket $ws) {
foreach ($ws as $msg) {
$ws->send('echo: ' . $msg->data);
}
});
$server->start();Кожне з'єднання обслуговується власною корутиною: та сама модель, що і у звичайних HTTP-запитів.
Життєвий цикл
З'єднання живе, поки не повернеться корутина-обробник. Якщо обробник просто завершився (наприклад, recv()/foreach повернув null наприкінці циклу), сервер сам закриє з'єднання кодом 1000 Normal. Явний close() до return потрібен лише тоді, коли треба інший код або свій текст причини.
Приймання повідомлень: recv() і foreach
public WebSocket::recv(): ?WebSocketMessageПризупиняє корутину доти, доки не прийде наступне повідомлення або з'єднання не закриється. Повертає WebSocketMessage або null, якщо клієнт закрив з'єднання штатно (звичайний код закриття або розрив без явного CLOSE):
while (($msg = $ws->recv()) !== null) {
handle($msg->data, $msg->binary);
}WebSocket реалізує \Iterator, тому той самий цикл можна записати коротше: foreach ($ws as $msg) { ... }. Штатне закриття просто завершує foreach, а закриття з помилкою кидає WebSocketClosedException прямо з циклу.
Читати повідомлення потрібно з одного місця: якщо викликати recv() з двох корутин паралельно на тому самому з'єднанні, другий виклик отримає виняток WebSocketConcurrentReadException. Якщо повідомлення потрібно роздати кільком обробникам, тримайте один recv()-цикл і розподіляйте повідомлення з нього самі.
Надсилання повідомлень: send(), trySend()
send() і sendBinary() можна безпечно викликати з будь-якої корутини, зокрема одночасно з кількох: сервер сам стежить, щоб дані різних викликів не змішувалися на проводі.
$ws->send('text frame'); // текст ПОВИНЕН бути валідним UTF-8
$ws->sendBinary($binaryData); // бінарні дані, без обмеження на кодуванняЗазвичай ці функції повертають керування одразу. Якщо клієнт читає повільно і буфер на відправку заповнюється, корутина призупиняється і продовжить роботу, коли клієнт трохи розвантажить буфер. Якщо очікування затягується довше write_timeout_ms, летить виняток WebSocketBackpressureException, і обробник вирішує, що робити: відкинути повідомлення, закрити з'єднання або повторити спробу.
Для розсилки одного повідомлення багатьом клієнтам, де один повільний клієнт не повинен затримувати решту, є неблокувальні варіанти:
if (!$ws->trySend($text)) {
// буфер цього клієнта заповнений, повідомлення НЕ надіслано, клієнт відстає
}trySend()/trySendBinary() ніколи не призупиняють корутину: вони одразу повертають true, якщо повідомлення прийнято, і false, якщо буфер заповнений (тоді повідомлення просто не надсилається). Розмір цього буфера задає HttpServerConfig::setStreamWriteBufferBytes() (0 знімає обмеження: trySend() тоді завжди надсилає і повертає true).
Закриття з'єднання: close(), isClosed()
$ws->close(WebSocketCloseCode::NORMAL, 'bye');Ініціює закриття з'єднання. Можна викликати багаторазово: повторні виклики нічого не роблять. Код закриття задається значенням WebSocketCloseCode або цілим числом 4000..4999 (діапазон для власних, не стандартних кодів). $reason приймає текст причини в UTF-8, до 123 байт.
isClosed() повертає true після close() або після того, як клієнт сам надіслав сигнал про закриття.
Ping і keepalive
$ws->ping('optional payload'); // до 125 байт, RFC 6455 §5.5Застосунку рідко потрібно кликати ping() вручну: сервер сам надсилає PING на з'єднаннях, де давно не було активності, за таймером HttpServerConfig::setWsPingIntervalMs(). Якщо клієнт не відповість вчасно (setWsPongTimeoutMs()), сервер сам закриє з'єднання. Подробиці конфігурації дивіться в Конфігурації.
Subprotocol і відмова у підключенні: WebSocketUpgrade
За замовчуванням обробник отримує тільки WebSocket $ws. Щоб самому вирішувати, чи приймати підключення і який subprotocol вибрати, зареєструйте обробник із трьома параметрами: сервер сам визначає їхню кількість і в цьому разі додає третій об'єкт WebSocketUpgrade:
use TrueAsync\WebSocket;
use TrueAsync\HttpRequest;
use TrueAsync\WebSocketUpgrade;
$server->addWebSocketHandler(function (WebSocket $ws, HttpRequest $req, WebSocketUpgrade $u) {
$offered = $u->getOfferedSubprotocols(); // із заголовка Sec-WebSocket-Protocol
if (!in_array('chat.v2', $offered, true)) {
$u->reject(400, 'unsupported subprotocol');
return;
}
$u->setSubprotocol('chat.v2'); // потрібно викликати до return або до reject()
foreach ($ws as $msg) {
// ...
}
});WebSocketUpgrade живе з моменту виклику обробника до reject() або до успішного return (тоді сервер завершує підключення з вибраним subprotocol'ом). Після цього моменту будь-який виклик на цьому об'єкті кидає виняток: змінити subprotocol уже не можна, відповідь клієнту вже надіслана.
getOfferedExtensions() повертає список розширень, які запропонував клієнт. permessage-deflate (RFC 7692, стиснення повідомлень) сервер узгоджує сам через HttpServerConfig::setWsPermessageDeflate(), решта значень зі списку суто інформаційні.
Коди закриття і винятки
Перелік (enum) WebSocketCloseCode містить стандартні коди закриття за RFC 6455 (NORMAL, GOING_AWAY, PROTOCOL_ERROR, MESSAGE_TOO_BIG та інші). Ієрархія винятків:
\Exception
└── TrueAsync\HttpServerException
└── TrueAsync\WebSocketException
├── WebSocketClosedException // closeCode / closeReason
├── WebSocketBackpressureException // клієнт довго не читає
└── WebSocketConcurrentReadException // другий recv() паралельноШтатне закриття клієнтом виражається через null з recv(), а не через виняток. Виняток летить лише при протокольній помилці або закритті з явним кодом помилки; $closeCode/$closeReason несуть причину. Подробиці дивіться в довіднику.
Конфігурація
| Метод | За замовчуванням | Призначення |
|---|---|---|
setWsMaxMessageSize($bytes) | 1 MiB | максимум на пересібране повідомлення, інакше 1009 |
setWsMaxFrameSize($bytes) | 1 MiB | максимум на один фрейм, захист від потоку крихітних фрагментів |
setWsPingIntervalMs($ms) | 30000 | як часто сервер надсилає PING на простої, 0 вимикає |
setWsPongTimeoutMs($ms) | 60000 | скільки чекати PONG перед розривом (1001) |
setWsPermessageDeflate($bool) | false | RFC 7692, опційно через навантаження на CPU |
Детальніше дивіться в Конфігурації.