phasync
WebSockets and SSE / WebSocket

Class

WebSocket

A WebSocket connection (RFC 6455), server side, on a ClientRequest or any Duplex.

Introduction#

WebSocket::from checks the handshake (and answers 426, 400 or 403 itself when it is not one), then runs the callback in a coroutine of its own. The connection closes when the callback returns (1000) or throws (1011, and swerve logs the exception); WebSocket::end closes it early. The connection is read all the time, also when the callback only sends: pings are answered, and when the client leaves the callback is cancelled, so the loop above ends with its client. The server pings every connection every WebSocket::$pingInterval seconds, from one coroutine for all of them, so proxies don't close a quiet one.

A message goes to the $onMessage listeners, one at a time, in the order it arrived: a slow listener holds back the reading. The pull style is the alternative, and the two don't mix: receive() throws while $onMessage has listeners.

send() and end() may be called from any coroutine. A client that reads slower than the callback sends makes send() wait; one that sends faster than the callback receives is not read from, so TCP holds it back. Neither makes the worker buffer.

WebSocket::accept is the lower level: the handler itself reads with receive(), as $onMessage is not called then, and calls end() when it is done.

The contract, with the close codes, the limits and what drains do, is in docs/websocket.md.

Class synopsis#

final class WebSocket implements \IteratorAggregate, \Traversable {
    /* Methods */
    public static WebSocket::from(
        ClientRequest $request,
        callable $callback,
        array $subprotocols = [],
        ?array $origins = null,
        int $maxMessage = self::MAX_MESSAGE
    ): void
    public static WebSocket::accept(
        ClientRequest $request,
        array $subprotocols = [],
        ?array $origins = null,
        int $maxMessage = self::MAX_MESSAGE
    ): ?WebSocket
    public static WebSocket::handshake(
        string $method,
        string $version,
        array $headers,
        bool $hasBody,
        array $subprotocols = [],
        ?array $origins = null
    ): WebSocketHandshake
    public static WebSocket::upgrade(
        ClientRequest $request,
        array $headers,
        callable $callback,
        int $maxMessage = self::MAX_MESSAGE
    ): void
    public static WebSocket::run(
        Duplex $connection,
        callable $callback,
        ?string $subprotocol = null,
        int $maxMessage = self::MAX_MESSAGE
    ): void
    public WebSocket::receive(): ?string
    public WebSocket::getIterator(): Generator
    public WebSocket::isBinary(): bool
    public WebSocket::send(string $text): void
    public WebSocket::sendBinary(string $data): void
    public WebSocket::end(int $code = 1000, string $reason = ''): void
    public WebSocket::isClosed(): bool
}

Constants#

NameValueDescription
MAX_MESSAGE1048576The largest message received by default; a larger one closes the connection with 1009.
INBOX64Messages received and not yet taken by receive() before the reading waits, in from().
WRITE_TIMEOUT30.0Seconds a send() waits for a client that doesn't read; then the client is given up and the connection closed.
CLOSE_TIMEOUT5.0Seconds the close frame waits for a client that doesn't read.

Properties#

NameTypeDescription
$pingIntervalstatic floatSeconds between the pings the server sends to every open connection (a process-wide setting).
$onMessagereadonly EventCalled with (string $data, bool $binary) for each text or binary message received, in from().
$onClosereadonly EventCalled once with (int $code, string $reason) when the connection has ended, whichever side ended it.
$subprotocolreadonly ?stringThe subprotocol chosen from the client's offer and the server's list, or null when none was.

Methods#

MethodDescription
fromAnswer the handshake and run $callback on the open connection; returns when the connection has closed.
acceptAnswer the handshake and return the open connection, or null after refusing the request.
handshakeFor framework adapters: decide a handshake from the parts of any request, answering nothing.
upgradeFor framework adapters: send the 101 with the headers an adapter decided, and run $callback on the open connection.
runFor adapters whose own code already sent the 101 (such as swerve-psr15's bridge, after its middleware decided the headers): run $callback on $connection as WebSocket::upgrade does, with the same close codes and cancellation. $connection need be no more than a Duplex: read(), write(), end() and close() are all this uses.
receiveThe next text or binary message, or null once the connection is closed.
getIteratorThe messages received, until the connection closes: foreach ($ws as $message).
isBinaryWhether the message receive() returned last was binary, rather than text.
sendSend a text message; nothing happens once the connection is closed.
sendBinarySend a binary message; nothing happens once the connection is closed.
endClose the connection: send a close frame with $code and $reason and end the sending side, once.
isClosedWhether the connection is closed: end() was called, the client said goodbye, or the connection ended.

Examples#

Example 1An echo server
use Swerve\ClientRequest;
use Swerve\RequestHandler;
use Swerve\WebSocket;

$app = new RequestHandler(function (ClientRequest $request) {
    WebSocket::from($request, function (WebSocket $ws) {
        foreach ($ws as $message) {             // ends when the connection closes
            if ('bye' === $message) {
                return;                          // returning closes the connection (1000)
            }
            $ws->send("echo: $message");
        }
    });
});

if (debug_backtrace()) {
    return $app;   // swerve loads this file: it is the application's swerve.php
}

// Run directly: serve the application with swerve, and talk to it as a browser would
require __DIR__ . '/../serve.php';
websocket(__FILE__, '/echo', ['hello', 'world', 'bye']);
Output
HTTP/1.1 101 Switching Protocols
upgrade: websocket
connection: Upgrade
sec-websocket-accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Server: Swerve

> hello
> world
> bye
< echo: hello
< echo: world
< close 1000
Example 2
return new Swerve\RequestHandler(function (Swerve\ClientRequest $request) {
    Swerve\WebSocket::from($request, function (Swerve\WebSocket $ws) {
        $ws->onMessage->listen(function (string $data) {      // inbound: events
            Swerve::publish('chat', $data);
        });
        foreach (Swerve::subscribe('chat') as $out) {         // outbound: plain sequential code
            if ('end' === $out) {
                return;                                       // returning closes the socket (1000)
            }
            $ws->send($out);
        }
    });
});
Example 3
foreach ($ws as $message) {               // ends when the connection closes
    $ws->send("echo: $message");
}

See also#