WebSocket

App::ws($path, $onMessage, $onOpen, $onClose) — register a WebSocket endpoint. ZealPHP uses OpenSwoole\WebSocket\Server which is backward-compatible with HTTP routes on the same port.

WebSocket registration
$app->ws(
    '/ws/chat',
    onMessage: function($server, $frame, $g) {
        // $frame->data   — message text
        // $frame->fd     — connection id
        // $frame->opcode — 1=TEXT, 2=BINARY (PING/PONG filtered automatically)
        $server->push($frame->fd, 'echo: ' . $frame->data);
    },
    onOpen: function($server, $request, $g) {
        // $request->fd     — connection id
        // $request->cookie — cookies from upgrade request
        // $request->get    — query params from ws://host/path?key=val
        $server->push($request->fd, json_encode(['event' => 'connected']));
    },
    onClose: function($server, $fd, $g) {
        // clean up per-connection state
    }
);

Live demo — 6 endpoints

Echo
Broadcast
Ticker
Rooms
Auth
Binary

WS /ws/echo — mirrors every message back verbatim.

$app->ws('/ws/echo', onMessage: fn($server,$frame) => $server->push($frame->fd, 'echo: '.$frame->data));

WS /ws/broadcast — every message goes to ALL connected clients.

$broadcastClients = [];
$app->ws('/ws/broadcast',
    onMessage: function($server, $frame, $g) use (&$broadcastClients) {
        foreach (array_keys($broadcastClients) as $fd) {
            if ($server->isEstablished($fd))
                $server->push($fd, json_encode(['from'=>$frame->fd,'msg'=>$frame->data]));
        }
    },
    onOpen:  fn($s,$req) => $broadcastClients[$req->fd] = true,
    onClose: fn($s,$fd)  => unset($broadcastClients[$fd])
);

WS /ws/ticker — server pushes every 1s using a spawned coroutine.

$app->ws('/ws/ticker',
    onMessage: fn($s,$f) => trim($f->data)==='stop' ? $s->close($f->fd) : null,
    onOpen: function($server, $request, $g) {
        $fd = $request->fd;
        go(function() use ($server, $fd) {
            $i = 0;
            while ($server->isEstablished($fd)) {
                co::sleep(1);
                $server->push($fd, json_encode(['tick' => ++$i, 'time' => date('H:i:s')]));
            }
        });
    }
);

WS /ws/rooms?room=general — cross-worker rooms via Store (OpenSwoole\Table). Every worker shares the same client registry.

// Shared across all workers — created before run()
Store::make('ws_rooms', 4096, [
    'room' => [\OpenSwoole\Table::TYPE_STRING, 64],
    'uid'  => [\OpenSwoole\Table::TYPE_STRING, 128],
]);

$app->ws('/ws/rooms',
    onOpen: fn($server, $request, $g) => Store::set('ws_rooms', (string)$request->fd, [
        'room' => $request->get['room'] ?? 'general',
        'uid'  => $request->get['uid']  ?? 'guest_'.$request->fd,
    ]),
    onMessage: function($server, $frame, $g) {
        $me = Store::get('ws_rooms', (string)$frame->fd);
        foreach (Store::table('ws_rooms') as $fd => $info)
            if ($info['room'] === $me['room'] && $server->isEstablished((int)$fd))
                $server->push((int)$fd, json_encode(['from'=>$me['uid'],'msg'=>$frame->data]));
    },
    onClose: fn($server, $fd, $g) => Store::del('ws_rooms', (string)$fd)
);

WS /ws/auth?token=secret — validates token in onOpen, disconnects with code 4001 if invalid.

$app->ws('/ws/auth',
    onOpen: function($server, $request, $g) {
        $token = $request->get['token'] ?? null;
        if ($token !== 'secret') {
            $server->push($request->fd, json_encode(['error' => 'Unauthorized']));
            $server->disconnect($request->fd, 4001, 'Unauthorized');
            return;
        }
        $server->push($request->fd, json_encode(['event' => 'authenticated']));
    },
    onMessage: fn($server, $frame) => $server->push($frame->fd, 'secure: '.$frame->data)
);

WS /ws/binary — checks $frame->opcode, echoes binary as binary. PING/PONG filtered automatically by ZealPHP.

$app->ws('/ws/binary',
    onMessage: function($server, $frame, $g) {
        if ($frame->opcode === \OpenSwoole\WebSocket\Server::WEBSOCKET_OPCODE_BINARY) {
            // Echo raw bytes back as a binary frame
            $server->push($frame->fd, $frame->data, \OpenSwoole\WebSocket\Server::WEBSOCKET_OPCODE_BINARY);
        } else {
            $server->push($frame->fd, json_encode(['bytes' => strlen($frame->data)]));
        }
    }
);

Browser JavaScript

HTTPS-aware browser client
const endpoint = '/ws/echo';
const scheme = location.protocol === 'https:' ? 'wss://' : 'ws://';
const socket = new WebSocket(scheme + location.host + endpoint);

socket.addEventListener('open', () => {
    socket.send('Hello from the browser');
});

socket.addEventListener('message', event => {
    console.log('received:', event.data);
});

function sendWhenReady(message) {
    if (socket.readyState === WebSocket.OPEN) {
        socket.send(message);
        return;
    }

    socket.addEventListener('open', () => socket.send(message), { once: true });
}

Live browser client

Disconnected
/ws/echo 0 sent / 0 received
Choose an endpoint and connect. Send will auto-connect if needed.