websocket.hphp

A single-file RFC 6455 WebSocket server, written in HolyPHP itself. Real SHA-1 handshake, text/binary frames, fragmentation, ping/pong, close handshake, and non-blocking I/O so one slow client never blocks the others.

stdlib — embedded RFC 6455 browser-compatible non-blocking sockets ~450 lines of HolyPHP

Minimal server

import "websocket.hphp";

$server = new WebSocketServer("tcp://0.0.0.0:9001");

$server->onMessage = function(int $client, string $msg): void {
    $server->broadcast($msg);        // chat-style echo
};

$server->run();                      // blocking event loop

Connect from any browser:

const ws = new WebSocket("ws://localhost:9001");
ws.onmessage = (e) => console.log(e.data);
ws.send("hello");

Construction

new WebSocketServer(string $addr = "tcp://127.0.0.1:9001")
PropertyTypeMeaning
$addrstringbind address, tcp://host:port
$verboseboollog lifecycle + frames to stdout
$onOpenclosurefn(int $client)
$onMessageclosurefn(int $client, string $message) — text frames
$onBinaryclosurefn(int $client, string $bytes) — binary frames
$onCloseclosurefn(int $client, int $code)

Client ids are small integers starting at 1, stable for the connection's lifetime.

Methods

bool listen() // bind + listen; false on failure void run() // blocking loop until stop() void stop() // leave the loop from a callback bool isRunning() bool tick(int $timeoutMs = 50) // one pass: accept + read bool send(int $client, string $message) // text frame bool sendBinary(int $client, string $bytes) // binary frame bool ping(int $client) int broadcast(string $message, int $except = 0) // to all (minus one) void close(int $client, int $code = 1000) // clean close handshake void drop(int $client, int $code = 1000) // force disconnect int clientCount() bool isOpen(int $client) string peerOf(int $client) // "ip:port" array clients() // ids of open clients

Event-driven chat server (full example)

import "websocket.hphp";

$server = new WebSocketServer("tcp://127.0.0.1:9500");
$server->verbose = true;
if (!$server->listen()) { exit(1); }

$server->onOpen = function(int $client) use ($server): void {
    $server->broadcast("user " . $client . " joined the chat");
};

$server->onMessage = function(int $client, string $msg) use ($server): void {
    $server->broadcast("user " . $client . ": " . $msg);
};

$server->onClose = function(int $client, int $code) use ($server): void {
    $server->broadcast("user " . $client . " left the chat");
};

while ($server->tick()) {
    usleep(10000);    // 10 ms loop; the library multiplexes I/O
}

Integrate into your own loop

Don't want the blocking run()? Call tick() from your own loop — it accepts pending connections and drains readable sockets, then returns.

while ($server->tick(10)) {          // your game/render loop here
    do_frame();
}

Protocol details handled for you

FeatureStatus
HTTP upgrade handshake (Sec-WebSocket-Accept, real SHA-1 + base64)
Masked client frames (unmasked in server direction)
Fragmented messages (continuation frames, opcode 0)
Text (1) / binary (2) opcodes
Ping/pong keepalive (opcodes 9/10)
Close handshake with status codes (opcode 8)
64-bit extended payload lengths
One slow client never blocks others (poll-based)

Lower-level pieces (exported)

The library also exposes its frame codec if you want to build something exotic:

string ws_frame(string $payload, int $opcode = 1) array? ws_decode(string $buf) // null while frame incomplete string ws_accept_key(string $key) // Sec-WebSocket-Accept string ws_close_payload(int $code)

ws_decode returns ["fin" => bool, "opcode" => int, "payload" => string, "consumed" => int].

Browser-side starter

A complete HTML client ships at examples/ws_chat.html — dark themed chat UI, auto-reconnect, Enter to send. Open it directly from disk; no web server needed.

hphp build examples/ws_chat.hphp -o wschat.exe
./wschat.exe
# then open examples/ws_chat.html in Chrome