phasync
WebSockets and SSE / Examples

WebSockets and SSE

Examples

Every example of this topic, with its output.

ServerSentEvents#

Example 1An event stream
use Swerve\ClientRequest;
use Swerve\RequestHandler;
use Swerve\ServerSentEvents;

$app = new RequestHandler(function (ClientRequest $request) {
    $events = new ServerSentEvents($request);   // the head goes out at once
    foreach (['build started', 'tests passed', 'deployed'] as $n => $status) {
        phasync::sleep(0.05);
        $events->send($status, id: (string) ($n + 1));
    }
});                                            // the stream ends when the handler returns

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

// Run directly: serve the application with swerve, and send it requests
require __DIR__ . '/../serve.php';
serve(__FILE__, ["GET /deploys HTTP/1.1\r\nHost: example\r\nAccept: text/event-stream\r\n\r\n"]);
Output
HTTP/1.1 200 OK
content-type: text/event-stream
cache-control: no-cache
x-accel-buffering: no
Server: Swerve
Transfer-Encoding: chunked
Connection: close

1b
id: 1
data: build started


1a
id: 2
data: tests passed


16
id: 3
data: deployed


0

ServerSentEvents#

Example 1
return new Swerve\RequestHandler(function (Swerve\ClientRequest $request) {
    $sse = new Swerve\ServerSentEvents($request);
    foreach (Swerve::subscribe('news', $sse->lastEventId()) as $message) {
        $sse->send($message);
    }
});

ServerSentEvents::lastEventId#

Example 1Resume after the last event seen
use Swerve\ClientRequest;
use Swerve\RequestHandler;
use Swerve\ServerSentEvents;

$app = new RequestHandler(function (ClientRequest $request) {
    $news   = [1 => 'first', 2 => 'second', 3 => 'third'];
    $events = new ServerSentEvents($request);
    $after  = (int) ($events->lastEventId() ?? 0);   // a reconnecting browser says what it saw last
    foreach ($news as $id => $item) {
        if ($id > $after) {
            $events->send($item, id: (string) $id);
        }
    }
});

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

// Run directly: serve the application with swerve, and send it requests
require __DIR__ . '/../serve.php';
serve(__FILE__, ["GET /news HTTP/1.1\r\nHost: example\r\nLast-Event-ID: 1\r\n\r\n"]);
Output
HTTP/1.1 200 OK
content-type: text/event-stream
cache-control: no-cache
x-accel-buffering: no
Server: Swerve
Transfer-Encoding: chunked
Connection: close

14
id: 2
data: second


13
id: 3
data: third


0

ServerSentEvents::send#

Example 1Event names, ids and retry
use Swerve\ClientRequest;
use Swerve\RequestHandler;
use Swerve\ServerSentEvents;

$app = new RequestHandler(function (ClientRequest $request) {
    $events = new ServerSentEvents($request);
    $events->send('plain message');                                  // EventSource's onmessage
    $events->send("two\nlines", event: 'note');                      // addEventListener('note', ...)
    $events->send('{"price":101.5}', event: 'tick', id: '42', retry: 5000);
});

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

// Run directly: serve the application with swerve, and send it requests
require __DIR__ . '/../serve.php';
serve(__FILE__, ["GET /events HTTP/1.1\r\nHost: example\r\n\r\n"]);
Output
HTTP/1.1 200 OK
content-type: text/event-stream
cache-control: no-cache
x-accel-buffering: no
Server: Swerve
Transfer-Encoding: chunked
Connection: close

15
data: plain message


23
event: note
data: two
data: lines


36
event: tick
id: 42
retry: 5000
data: {"price":101.5}


0

ServerSentEvents::send#

Example 1
$sse->send(json_encode($order), event: 'order', id: (string) $order->id);

ServerSentEvents::comment#

Example 1Keep-alive comments
use Swerve\ClientRequest;
use Swerve\RequestHandler;
use Swerve\ServerSentEvents;

$app = new RequestHandler(function (ClientRequest $request) {
    $events = new ServerSentEvents($request);
    for ($i = 0; $i < 2; ++$i) {
        phasync::sleep(0.05);       // in production: some seconds without news
        $events->comment('keep-alive');   // the browser ignores it; a client that left makes it throw
    }
    $events->send('news at last');
});

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

// Run directly: serve the application with swerve, and send it requests
require __DIR__ . '/../serve.php';
serve(__FILE__, ["GET /news HTTP/1.1\r\nHost: example\r\n\r\n"]);
Output
HTTP/1.1 200 OK
content-type: text/event-stream
cache-control: no-cache
x-accel-buffering: no
Server: Swerve
Transfer-Encoding: chunked
Connection: close

e
: keep-alive


e
: keep-alive


14
data: news at last


0

WebSocket#

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

WebSocket#

Example 1
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);
        }
    });
});

WebSocket#

Example 1
foreach ($ws as $message) {               // ends when the connection closes
    $ws->send("echo: $message");
}

WebSocket::from#

Example 1Listen to messages, push from a subscription
use Swerve\ClientRequest;
use Swerve\RequestHandler;
use Swerve\Swerve;
use Swerve\WebSocket;

$app = new RequestHandler(function (ClientRequest $request) {
    WebSocket::from($request, function (WebSocket $ws) {
        $ws->onMessage->listen(function (string $data) {     // inbound: events
            Swerve::publish('chat', $data);                  // to every worker's subscribers
        });
        foreach (Swerve::subscribe('chat') as $out) {        // outbound: plain sequential code
            if ('/quit' === $out) {
                return;
            }
            $ws->send("someone said: $out");
        }
    });
});

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__, '/chat', ['hi all', 'anyone here?', '/quit']);
Output
HTTP/1.1 101 Switching Protocols
upgrade: websocket
connection: Upgrade
sec-websocket-accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Server: Swerve

> hi all
> anyone here?
> /quit
< someone said: hi all
< someone said: anyone here?
< close 1000

WebSocket::from#

Example 1
WebSocket::from($request, function (WebSocket $ws) {
    foreach ($ws as $message) {
        $ws->send(strtoupper($message));
    }
}, origins: ['https://example.com']);

WebSocket::accept#

Example 1Read with receive() and close yourself
use Swerve\ClientRequest;
use Swerve\RequestHandler;
use Swerve\WebSocket;

$app = new RequestHandler(function (ClientRequest $request) {
    if (null === $ws = WebSocket::accept($request)) {
        return;                                  // refused: swerve answered 426, 400 or 403
    }
    try {
        $total = 0;
        while (null !== ($message = $ws->receive())) {
            $total += (int) $message;
            $ws->send("total $total");
            if ($total >= 10) {
                break;
            }
        }
    } finally {
        $ws->end(1000, 'enough');                // with accept(), the handler closes
    }
});

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__, '/sum', ['3', '4', '5']);
Output
HTTP/1.1 101 Switching Protocols
upgrade: websocket
connection: Upgrade
sec-websocket-accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Server: Swerve

> 3
> 4
> 5
< total 3
< total 7
< total 12
< close 1000 enough

WebSocket::accept#

Example 1
if (null === $ws = WebSocket::accept($request)) {
    return;
}
try {
    foreach ($ws as $message) {
        $ws->send($message);
    }
} finally {
    $ws->end();
}

WebSocket::handshake#

Example 1Decisions for four requests
use Swerve\WebSocket;

$websocket = [
    'host'                  => ['example.com'],
    'upgrade'               => ['websocket'],
    'connection'            => ['Upgrade'],
    'sec-websocket-key'     => ['dGhlIHNhbXBsZSBub25jZQ=='],
    'sec-websocket-version' => ['13'],
    'sec-websocket-protocol' => ['chat.v2, chat.v1'],
];
$requests = [
    'a WebSocket handshake' => $websocket,
    'a plain GET'           => ['host' => ['example.com']],
    'a wrong version'       => ['sec-websocket-version' => ['8']] + $websocket,
    'a foreign origin'      => ['origin' => ['https://evil.example']] + $websocket,
];
foreach ($requests as $what => $headers) {
    $decision = WebSocket::handshake('GET', '1.1', $headers, false, subprotocols: ['chat.v1'], origins: ['https://example.com']);
    echo str_pad($what, 24), $decision->status, ' ', $decision->subprotocol ?? $decision->body, "\n";
}
Output
a WebSocket handshake   101 chat.v1
a plain GET             426 This address speaks WebSocket
a wrong version         400 Not a WebSocket handshake
a foreign origin        403 This origin may not connect

WebSocket::upgrade#

Example 1An adapter's upgrade
use Swerve\ClientRequest;
use Swerve\RequestHandler;
use Swerve\WebSocket;

// What a framework adapter does: decide with handshake(), let middleware add headers, then upgrade()
$app = new RequestHandler(function (ClientRequest $request) {
    $decision = WebSocket::handshake($request->getMethod(), $request->getProtocolVersion(), $request->getRequestHeaders(), false);
    if (!$decision->accepted()) {
        $request->sendResponseHeaders($decision->status, $decision->headers);
        $request->write($decision->body);

        return;
    }
    $headers = $decision->headers + ['x-served-by' => 'my-framework'];   // middleware decorates
    WebSocket::upgrade($request, $headers, function (WebSocket $ws) {
        $ws->send('upgraded by the adapter');
    });
});

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__, '/socket', []);
Output
HTTP/1.1 101 Switching Protocols
upgrade: websocket
connection: Upgrade
sec-websocket-accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
x-served-by: my-framework
Server: Swerve

< upgraded by the adapter
< close 1000

WebSocket::run#

Example 1After a 101 of your own
use Swerve\ClientRequest;
use Swerve\RequestHandler;
use Swerve\WebSocket;

// For code that sends the 101 itself: run() takes over the connection after it
$app = new RequestHandler(function (ClientRequest $request) {
    $decision = WebSocket::handshake($request->getMethod(), $request->getProtocolVersion(), $request->getRequestHeaders(), false);
    $request->sendResponseHeaders(101, $decision->headers);
    WebSocket::run($request, function (WebSocket $ws) {
        foreach ($ws as $message) {
            $ws->send(strtoupper($message));
            return;
        }
    });
});

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__, '/socket', ['shout']);
Output
HTTP/1.1 101 Switching Protocols
upgrade: websocket
connection: Upgrade
sec-websocket-accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Server: Swerve

> shout
< SHOUT
< close 1000

WebSocket::receive#

Example 1
while (null !== ($message = $ws->receive())) {
    $ws->send(strrev($message));
}

WebSocket::send#

Example 1
$ws->send(json_encode(['type' => 'welcome']));

WebSocket::sendBinary#

Example 1A binary message
use Swerve\ClientRequest;
use Swerve\RequestHandler;
use Swerve\WebSocket;

$app = new RequestHandler(function (ClientRequest $request) {
    WebSocket::from($request, function (WebSocket $ws) {
        $ws->sendBinary(pack('nN', 1, 42));     // bytes, not text: any value
        $ws->send('text must be UTF-8');
    });
});

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__, '/binary', []);
Output
HTTP/1.1 101 Switching Protocols
upgrade: websocket
connection: Upgrade
sec-websocket-accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Server: Swerve

< (binary) 00010000002a
< text must be UTF-8
< close 1000

WebSocket::sendBinary#

Example 1
$ws->sendBinary(pack('N', 42));

WebSocket::end#

Example 1A close code of your own
use Swerve\ClientRequest;
use Swerve\RequestHandler;
use Swerve\WebSocket;

$app = new RequestHandler(function (ClientRequest $request) {
    WebSocket::from($request, function (WebSocket $ws) {
        $ws->send('guess a number');
        foreach ($ws as $guess) {
            if ('7' === $guess) {
                $ws->end(4000, 'you win');       // 4000-4999: codes of your own application
            } else {
                $ws->send('no');
            }
        }
    });
});

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__, '/game', ['3', '7']);
Output
HTTP/1.1 101 Switching Protocols
upgrade: websocket
connection: Upgrade
sec-websocket-accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Server: Swerve

> 3
> 7
< guess a number
< no
< close 4000 you win

WebSocket::end#

Example 1
$ws->onMessage->listen(function (string $data) use ($ws) {
    if ('bye' === $data) {
        $ws->end(1000, 'bye');
    }
});

WebSocketHandshake#

Example 1Accepted and refused
use Swerve\WebSocket;

$headers = [
    'upgrade'               => ['websocket'],
    'connection'            => ['Upgrade'],
    'sec-websocket-key'     => ['dGhlIHNhbXBsZSBub25jZQ=='],
    'sec-websocket-version' => ['13'],
];

$accepted = WebSocket::handshake('GET', '1.1', $headers, false);
var_dump($accepted->accepted());
print_r($accepted->headers);            // the 101 response's headers

$refused = WebSocket::handshake('POST', '1.1', $headers, true);
var_dump($refused->accepted());
echo $refused->status, ' ', $refused->body, "\n";   // the complete final response
print_r($refused->headers);
Output
bool(true)
Array
(
    [upgrade] => websocket
    [connection] => Upgrade
    [sec-websocket-accept] => s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
)
bool(false)
400 Not a WebSocket handshake
Array
(
    [sec-websocket-version] => 13
    [content-type] => text/plain
    [content-length] => 25
)