TrueAsync\WebSocket
(PHP 8.6+, true_async_server 0.9+)
Класи full-duplex з'єднання за RFC 6455. Керівництво з прикладами: WebSocket.
TrueAsync\WebSocket
Одне WebSocket-з'єднання. Створюється сервером одразу після коміту upgrade-handshake і передається першим аргументом в обробник, зареєстрований через HttpServer::addWebSocketHandler().
namespace TrueAsync;
final class WebSocket implements \Iterator
{
public function recv(): ?WebSocketMessage;
public function send(string $text): void;
public function sendBinary(string $data): void;
public function trySend(string $text): bool;
public function trySendBinary(string $data): bool;
public function ping(string $payload = ''): void;
public function close(WebSocketCloseCode|int $code = WebSocketCloseCode::NORMAL, string $reason = ''): void;
public function isClosed(): bool;
public function getSubprotocol(): ?string;
public function getRemoteAddress(): string;
// Iterator
public function current(): ?WebSocketMessage;
public function key(): int;
public function next(): void;
public function rewind(): void;
public function valid(): bool;
}Екземпляри конструюються лише сервером, new WebSocket недоступний користувацькому коду.
Життєвий цикл
З'єднання прив'язане до корутини обробника. Коли обробник повертає керування з будь-якої причини, зокрема через return із recv()-циклу на null, сервер закриває з'єднання кодом 1000 Normal. Явний close() до return потрібен лише тоді, коли треба інший код або текст причини закриття.
Модель конкурентності
send(),sendBinary()іping()можна безпечно викликати з кількох корутин одночасно: сервер сам стежить за черговістю відправки, тому дані різних викликів не змішуються на проводі.- У
recv()може бути лише один викликач одночасно. Якщо викликатиrecv()з двох корутин паралельно, другий виклик одразу отримає винятокWebSocketConcurrentReadException. Читати повідомлення потрібно строго з одного місця коду, в одному циклі. close()можна викликати багаторазово і з будь-якої корутини: повторні виклики нічого не роблять.
recv
public WebSocket::recv(): ?WebSocketMessageПриймає наступне текстове або бінарне повідомлення. Призупиняє викликаючу корутину до повного повідомлення або закриття з'єднання.
Повертає WebSocketMessage або null, коли клієнт закрив з'єднання штатно: нормальний код CLOSE (1000/1001/1005) або розрив без CLOSE-фрейму. Типовий цикл: while (($m = $ws->recv()) !== null) { ... }.
Метод кидає винятки:
WebSocketClosedException— при протокольній помилці або явному error-коду закриття;$closeCode/$closeReasonнесуть код і текст причини за RFC 6455.WebSocketConcurrentReadException— якщо інша корутина вже викликалаrecv()на тому самому з'єднанні і ще не отримала відповідь.
send
public WebSocket::send(string $text): voidНадсилає текстовий фрейм. $text повинен бути валідним UTF-8: невалідні дані відхиляються одразу, щоб отримувач ніколи не побачив кадр, що порушує RFC 6455 §5.6.
У звичайній ситуації функція повертає керування одразу. Якщо буфер на відправку заповнений (клієнт повільно читає), корутина призупиняється і продовжить роботу, коли буфер звільниться. Якщо очікування затягується довше write_timeout_ms, метод кидає виняток WebSocketBackpressureException, і обробник вирішує, що робити: відкинути повідомлення, закрити з'єднання або повторити спробу пізніше.
Метод також кидає WebSocketClosedException, якщо з'єднання вже закрите.
sendBinary
public WebSocket::sendBinary(string $data): voidНадсилає бінарний фрейм. Бінарні дані не мають обмеження на UTF-8. Поведінка при переповненні буфера відправки така сама, як у send().
trySend
public WebSocket::trySend(string $text): boolНеблокувальна відправка. Якщо буфер на відправку не заповнений, ставить текстовий фрейм у чергу і повертає true. Якщо буфер уже заповнений, нічого не надсилає і повертає false: викликаючий код сам вирішує, відкинути повідомлення, сповільнитися чи закрити з'єднання. На відміну від send(), trySend() ніколи не призупиняє корутину, тому добре підходить для розсилки одного повідомлення багатьом клієнтам, коли один повільний клієнт не повинен затримувати решту.
Наскільки великим може бути буфер на відправку, задає HttpServerConfig::setStreamWriteBufferBytes() (значення 0 знімає обмеження: trySend() тоді завжди ставить повідомлення в чергу і повертає true).
Функція повертає true, якщо повідомлення прийнято в чергу, і false, якщо буфер відправки переповнений і клієнт не встигає читати. Кидає WebSocketClosedException, якщо з'єднання вже закрите.
trySendBinary
public WebSocket::trySendBinary(string $data): boolНеблокувальна бінарна відправка. Поводиться так само, як trySend().
ping
public WebSocket::ping(string $payload = ''): voidНадсилає PING-фрейм. За RFC 6455 §5.5.2 клієнт зобов'язаний відповісти PONG. Прикладному коду рідко потрібно кликати це вручну: таймер keepalive сервера (HttpServerConfig::setWsPingIntervalMs()) надсилає PING'и автоматично, якщо він налаштований.
$payload приймає до 125 байт (RFC 6455 §5.5).
close
public WebSocket::close(WebSocketCloseCode|int $code = WebSocketCloseCode::NORMAL, string $reason = ''): voidІніціює закриття з'єднання. Можна викликати багаторазово: повторні виклики нічого не роблять.
$code— значенняWebSocketCloseCodeабо ціле число4000..4999(діапазон для власних, не стандартних кодів, за RFC 6455 §7.4.2).$reason— текст причини в UTF-8, до 123 байт.
isClosed
public WebSocket::isClosed(): booltrue після виклику close() або після того, як клієнт надіслав CLOSE-фрейм.
getSubprotocol
public WebSocket::getSubprotocol(): ?stringSubprotocol, узгоджений під час upgrade, або null, якщо жоден не вибрано.
getRemoteAddress
public WebSocket::getRemoteAddress(): stringАдреса клієнта у формі host:port (IPv4) або [host]:port (IPv6) для TCP-з'єднань. Порожній рядок для з'єднань через Unix-сокет.
Iterator
public WebSocket::current(): ?WebSocketMessage
public WebSocket::key(): int
public WebSocket::next(): void
public WebSocket::rewind(): void
public WebSocket::valid(): boolДає змогу писати foreach ($ws as $msg) замість ручного recv()-циклу. На кожному кроці цикл забирає наступне повідомлення; штатне закриття з'єднання просто завершує foreach, а закриття з помилкою кидає WebSocketClosedException прямо з циклу.
TrueAsync\WebSocketMessage
namespace TrueAsync;
final class WebSocketMessage
{
public readonly string $data;
public readonly bool $binary;
}Одне повністю зібране повідомлення, яке віддає WebSocket::recv(). Текстові повідомлення вже перевірені на валідність UTF-8, тому можна використовувати $data як є, без повторної перевірки.
$data— вміст повідомлення. Для текстових повідомлень це валідний UTF-8 рядок.$binary—true, якщо повідомлення було надіслане як бінарне,falseдля текстового.
Екземпляри конструюються лише сервером. Отримати їх можна тільки через WebSocket::recv(), створити new WebSocketMessage вручну не можна.
TrueAsync\WebSocketUpgrade
namespace TrueAsync;
final class WebSocketUpgrade
{
public function reject(int $status, string $reason = ''): void;
public function setSubprotocol(string $name): void;
public function getOfferedSubprotocols(): array;
public function getOfferedExtensions(): array;
}Об'єкт, що представляє upgrade у процесі узгодження. Існує з моменту виклику обробника до виклику reject() або до успішного return (тоді сервер надсилає 101 з subprotocol'ом, вибраним через setSubprotocol()).
Доступний лише обробникам, зареєстрованим із трьома параметрами:
$server->addWebSocketHandler(function (WebSocket $ws, HttpRequest $req, WebSocketUpgrade $u) {
// ...
});Сервер сам визначає, скільки параметрів вказано в обробнику, і передає WebSocketUpgrade лише якщо їх три. Обробник із двома параметрами цей об'єкт не отримує, і upgrade приймається з налаштуваннями за замовчуванням.
Після того як handshake завершено, будь-який виклик на цьому об'єкті кидає виняток: Sec-WebSocket-Protocol уже надіслано клієнту, subprotocol більше не можна змінити.
reject
public WebSocketUpgrade::reject(int $status, string $reason = ''): voidВідхиляє upgrade із заданим HTTP-статусом. 101 не надсилається; клієнт отримує вибраний статус, і з'єднання закривається. Після reject() обробник повинен одразу завершити роботу: подальший обмін даними не допускається.
$status— HTTP-код (має бути 4xx або 5xx).$reason— необов'язкове тіло відповіді.
setSubprotocol
public WebSocketUpgrade::setSubprotocol(string $name): voidВибирає subprotocol зі списку, запропонованого клієнтом. Вибране значення потрапить у заголовок Sec-WebSocket-Protocol відповіді. Викликати потрібно до return з обробника і до reject(). Сервер не перевіряє, що вибране значення справді входить у список із getOfferedSubprotocols(), за це відповідає сам обробник.
getOfferedSubprotocols
public WebSocketUpgrade::getOfferedSubprotocols(): arrayПовертає список subprotocol'ів (string[]), які клієнт надіслав у заголовку Sec-WebSocket-Protocol, у порядку його переваги. Порожній масив, якщо клієнт нічого не запропонував.
getOfferedExtensions
public WebSocketUpgrade::getOfferedExtensions(): arrayПовертає список розширень (string[]) із заголовка Sec-WebSocket-Extensions, у порядку переваги клієнта. permessage-deflate (RFC 7692, стиснення повідомлень) сервер узгоджує сам через HttpServerConfig::setWsPermessageDeflate(), решта значень зі списку суто інформаційні. Порожній масив, якщо клієнт нічого не запропонував.
TrueAsync\WebSocketCloseCode
namespace TrueAsync;
enum WebSocketCloseCode: int
{
case NORMAL = 1000;
case GOING_AWAY = 1001;
case PROTOCOL_ERROR = 1002;
case UNSUPPORTED_DATA = 1003;
case NO_STATUS = 1005; // RESERVED
case ABNORMAL_CLOSURE = 1006; // RESERVED
case INVALID_FRAME_PAYLOAD = 1007;
case POLICY_VIOLATION = 1008;
case MESSAGE_TOO_BIG = 1009;
case MANDATORY_EXTENSION = 1010;
case INTERNAL_SERVER_ERROR = 1011;
case TLS_HANDSHAKE = 1015; // RESERVED
}Стандартні коди закриття з'єднання за RFC 6455 §7.4.1. Власні, не стандартні коди теж дозволені: WebSocket::close() приймає як значення цього enum, так і ціле число в діапазоні 4000..4999 (RFC 6455 §7.4.2).
Винятки
\Exception
└── TrueAsync\HttpServerException
└── TrueAsync\WebSocketException
├── WebSocketClosedException // final
├── WebSocketBackpressureException // final
└── WebSocketConcurrentReadException // finalTrueAsync\WebSocketException
class WebSocketException extends HttpServerException {}Базовий виняток для всіх помилок WebSocket. Розширює спільний для сервера HttpServerException, тож наявні catch-all обробники продовжують працювати.
TrueAsync\WebSocketClosedException
final class WebSocketClosedException extends WebSocketException
{
public readonly int $closeCode;
public readonly string $closeReason;
}З'єднання закрите не штатним чином, а з іншої причини: протокольна помилка або явний error-код від клієнта. $closeCode містить код закриття за RFC 6455 (або 1006 Abnormal Closure, якщо CLOSE-фрейм узагалі не прийшов, наприклад при обриві мережі). $closeReason містить текст причини з CLOSE-фрейму клієнта в UTF-8, або порожній рядок, якщо тексту не було.
Штатне закриття клієнтом (код 1000) винятку не кидає: WebSocket::recv() у цьому разі просто повертає null.
TrueAsync\WebSocketBackpressureException
final class WebSocketBackpressureException extends WebSocketException {}Кидається з send()/sendBinary(), коли буфер на відправку залишається заповненим довше, ніж write_timeout_ms. Це сигнал застосунку, що клієнт читає занадто повільно: можна закрити з'єднання або відкинути повідомлення і продовжити роботу.
TrueAsync\WebSocketConcurrentReadException
final class WebSocketConcurrentReadException extends WebSocketException {}Помилка коду: друга корутина викликала recv(), поки перша вже чекала на відповідь від recv() на тому самому WebSocket. Читати повідомлення з одного з'єднання можна лише в одному місці коду; якщо повідомлення потрібно роздати кільком обробникам, будуйте один recv()-цикл і розподіляйте повідомлення з нього самі.