WebSockets and SSE
Examples
Every example of this topic, with its output.
ServerSentEvents#
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#
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#
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#
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}
0ServerSentEvents::send#
$sse->send(json_encode($order), event: 'order', id: (string) $order->id);ServerSentEvents::comment#
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#
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#
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#
foreach ($ws as $message) { // ends when the connection closes
$ws->send("echo: $message");
}WebSocket::from#
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#
WebSocket::from($request, function (WebSocket $ws) {
foreach ($ws as $message) {
$ws->send(strtoupper($message));
}
}, origins: ['https://example.com']);WebSocket::accept#
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#
if (null === $ws = WebSocket::accept($request)) {
return;
}
try {
foreach ($ws as $message) {
$ws->send($message);
}
} finally {
$ws->end();
}WebSocket::handshake#
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#
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#
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#
while (null !== ($message = $ws->receive())) {
$ws->send(strrev($message));
}WebSocket::send#
$ws->send(json_encode(['type' => 'welcome']));WebSocket::sendBinary#
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#
$ws->sendBinary(pack('N', 42));WebSocket::end#
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#
$ws->onMessage->listen(function (string $data) use ($ws) {
if ('bye' === $data) {
$ws->end(1000, 'bye');
}
});WebSocketHandshake#
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
)