A WebSocket Chat Server

A WebSocket (RFC 6455) opens as an HTTP request carrying a Sec-WebSocket-Key. The server answers 101 Switching Protocols with the base64 SHA-1 of that key plus a fixed GUID. Then both sides send frames: an opcode byte (0x1 text, 0x8 close), a length and, from the client, an XOR mask:

ws-server.php: handshake, framing and broadcastPHP
<?php
function unframe(string $d): ?string {          // null means a close frame or a hang-up
  if ($d === '' || (ord($d[0]) & 0x0F) === 8) return null;
  $len = ord($d[1]) & 127;                      // payloads under 126 bytes only
  return substr($d, 6, $len) ^ str_repeat(substr($d, 2, 4), intdiv($len, 4) + 1);  // unmask
}
$server = stream_socket_server('tcp://127.0.0.1:8223');
$clients = $names = [];
while (true) {
  $read = $clients + ['listen' => $server];
  stream_select($read, $write, $except, null);
  foreach ($read as $id => $sock) {
    if ($id === 'listen') {
      $c = stream_socket_accept($server);
      $clients[(int) $c] = $c;
    } elseif (!isset($names[$id])) {            // first message: the HTTP upgrade request
      $data = fread($sock, 8192);
      preg_match('/Sec-WebSocket-Key: (\S+)/i', $data, $key);
      $accept = base64_encode(sha1($key[1] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));
      fwrite($sock, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\n"
        . "Connection: Upgrade\r\nSec-WebSocket-Accept: $accept\r\n\r\n");
      $names[$id] = preg_match('#GET /\?name=(\w+)#', $data, $m) ? $m[1] : "guest$id";
    } elseif (($text = unframe((string) fread($sock, 8192))) === null) {
      echo "{$names[$id]} left\n";
      fclose($sock);
      unset($clients[$id], $names[$id]);
    } else {
      $msg = "{$names[$id]}: $text";
      echo "$msg\n";                            // log it, then broadcast a text frame
      foreach ($clients as $peer) fwrite($peer, "\x81" . chr(strlen($msg)) . $msg);
    }
  }
}

Two Node 22 clients using the browser's WebSocket API (Front-End Web Development) joined as alice and bob and sent a message each. Both clients received both messages, and the server logged:

Output of 254
alice: Is the new PHP book out?
bob: Yes, with async.
alice left
bob left

Production also needs long frames, ping/pong, Origin checks and TLS, so use amphp/websocket-server or Swoole 18,923 . Ratchet 6,437 's last release was 0.4.4 (December 2021); Laravel 2,157 has Reverb (Laravel).