ez-php / websocket
PHP 8.5 Fiber-based WebSocket server for ez-php — RFC 6455 handshake, frame codec, ChannelManager pub/sub, and a non-blocking event loop.
Requires
- php: ^8.5
Requires (Dev)
- ez-php/docker: ^2.0
- friendsofphp/php-cs-fixer: ^3.94
- phpstan/phpstan: ^2.1
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-strict-rules: ^2.0
- phpunit/phpunit: ^13.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-main
- 2.5.6
- 2.5.5
- 2.5.4
- 2.5.3
- 2.5.2
- 2.5.1
- 2.5.0
- 2.4.11
- 2.4.10
- 2.4.9
- 2.4.8
- 2.4.7
- 2.4.6
- 2.4.5
- 2.4.4
- 2.4.3
- 2.4.2
- 2.4.1
- 2.4.0
- 2.3.9
- 2.3.8
- 2.3.7
- 2.3.6
- 2.3.5
- 2.3.4
- 2.3.3
- 2.3.2
- 2.3.1
- 2.3.0
- 2.2.1
- 2.2.0
- 2.1.1
- 2.1.0
- 2.0.1
- 2.0.0
- 1.14.0
- 1.13.1
- 1.13.0
- 1.12.2
- 1.12.1
- 1.12.0
- 1.11.2
- 1.11.1
- 1.11.0
- 1.10.0
- 1.9.2
- 1.9.1
- 1.9.0
- 1.8.0
- 1.7.1
- 1.7.0
- 1.6.1
- 1.6.0
- 1.5.1
- 1.5.0
- 1.4.2
- 1.4.1
- 1.4.0
- 1.3.0
- 1.2.0
This package is auto-updated.
Last update: 2026-09-30 19:50:28 UTC
README
PHP 8.5 Fiber-based WebSocket server for the ez-php ecosystem.
Implements RFC 6455 from the ground up — no third-party WebSocket library needed. Each connection runs in its own Fiber, allowing hundreds of concurrent clients on a single PHP process without threads or async extensions.
Intentionally separate from ez-php/broadcast (SSE/event-bus); this module
provides bidirectional, low-latency real-time communication.
Installation
composer require ez-php/websocket
No framework integration is needed — the server runs as a standalone long-lived process.
Quick start
<?php use EzPhp\WebSocket\ChannelManager; use EzPhp\WebSocket\ConnectionInterface; use EzPhp\WebSocket\Frame; use EzPhp\WebSocket\HandlerInterface; use EzPhp\WebSocket\Server; class ChatHandler implements HandlerInterface { public function __construct(private readonly ChannelManager $channels) {} public function onOpen(ConnectionInterface $conn): void { $this->channels->subscribe('general', $conn); $this->channels->broadcast('general', "{$conn->id()} joined."); } public function onMessage(ConnectionInterface $conn, Frame $frame): void { $this->channels->broadcast('general', "[{$conn->id()}] {$frame->payload}"); } public function onClose(ConnectionInterface $conn): void { $this->channels->unsubscribeAll($conn); $this->channels->broadcast('general', "{$conn->id()} left."); } public function onError(ConnectionInterface $conn, \Throwable $e): void { error_log("WebSocket error [{$conn->id()}]: {$e->getMessage()}"); } } $server = new Server('0.0.0.0', 8080); $server->run(new ChatHandler(new ChannelManager()));
Start the server:
php chat-server.php
Connect from a browser:
const ws = new WebSocket('ws://localhost:8080'); ws.onmessage = e => console.log(e.data); ws.onopen = () => ws.send('Hello!');
Core classes
Server
$server = new Server(host: '0.0.0.0', port: 8080); // optional third argument: maxMessageBytes (default 16 MiB) $server->run($handler); // blocks; handles SIGTERM externally
- Opens a TCP server socket via
stream_socket_server() - Spawns one
Fiberper accepted connection - Non-blocking
stream_select()event loop resumes Fibers when sockets are readable - Handles PING→PONG and CLOSE frames automatically
ConnectionInterface / Connection
$conn->id(); // unique string ID assigned by Server $conn->isConnected(); // false after close() or peer disconnect $conn->send('text'); // UTF-8 TEXT frame $conn->sendBinary($b); // BINARY frame $conn->close('bye'); // clean close with status 1000
Frame
Represents one RFC 6455 frame. Available in HandlerInterface::onMessage():
$frame->opcode; // Opcode::TEXT | Opcode::BINARY $frame->payload; // decoded (unmasked) payload string $frame->fin; // always true here — fragmented messages arrive reassembled (see below)
ChannelManager
Named pub/sub channels over connected clients:
$mgr->subscribe('room', $conn); $mgr->unsubscribe('room', $conn); $mgr->unsubscribeAll($conn); // called in onClose() $mgr->broadcast('room', 'message'); // sends TEXT frame to all connected subscribers $mgr->connections('room'); // list<ConnectionInterface> $mgr->channels(); // list<string> $mgr->count('room'); // int
broadcast() silently prunes disconnected connections.
Architecture notes
- Fragmented messages are reassembled: a
TEXT/BINARYframe with FIN=0 and itsCONTINUATIONframes reachonMessage()once, as one complete frame.PINGs between fragments are answered. A stray continuation, a new data frame inside an open message, or a fragmented control frame closes with1002. A message larger thanmaxMessageBytes(third constructor argument, default 16 MiB), whether one frame or the sum of its fragments, closes with1009. No permessage-deflate. - Unmasked client frames are refused: per RFC 6455 §5.1 the server closes with status
1002. - No TLS (WSS): terminate TLS at a reverse proxy (nginx, Caddy) and use plain
ws://internally. - No authentication: verify cookies or tokens in
onOpen()and call$conn->close()on failure.
License
MIT