WebSockets en PHP con Ratchet: servidor de mensajes en tiempo real

Los WebSockets permiten mantener una conexión bidireccional persistente entre servidor y cliente, algo imposible con el modelo petición-respuesta de HTTP. En PHP, la librería más usada para implementar un servidor WebSocket es Ratchet, construida sobre ReactPHP.

Instalación

composer require cboden/ratchet

Ratchet incluye React como dependencia, así que no necesitas instalar nada más.

MessageComponentInterface

Todo servidor Ratchet implementa MessageComponentInterface, que obliga a definir cuatro métodos:

<?php
use RatchetMessageComponentInterface;
use RatchetConnectionInterface;

class Chat implements MessageComponentInterface
{
    protected SplObjectStorage $clients;

    public function __construct()
    {
        $this->clients = new SplObjectStorage();
    }

    public function onOpen(ConnectionInterface $conn): void
    {
        $this->clients->attach($conn);
        echo "Nueva conexión ({$conn->resourceId})n";
    }

    public function onMessage(ConnectionInterface $from, $msg): void
    {
        foreach ($this->clients as $client) {
            if ($from !== $client) {
                $client->send($msg);
            }
        }
    }

    public function onClose(ConnectionInterface $conn): void
    {
        $this->clients->detach($conn);
        echo "Conexión {$conn->resourceId} cerradan";
    }

    public function onError(ConnectionInterface $conn, Exception $e): void
    {
        echo "Error: {$e->getMessage()}n";
        $conn->close();
    }
}
?>

SplObjectStorage para gestionar conexiones

SplObjectStorage actúa como un conjunto de objetos únicos. Cada vez que un cliente se conecta, lo añadimos con attach(); cuando se va, lo eliminamos con detach(). El bucle foreach recorre todos los clientes activos en O(n).

Si necesitas buscar conexiones por usuario, puedes usar un array asociativo en paralelo:

<?php
class Chat implements MessageComponentInterface
{
    protected SplObjectStorage $clients;
    protected array $userMap = []; // resourceId => userId

    public function onOpen(ConnectionInterface $conn): void
    {
        $this->clients->attach($conn);
        // El userId llega en la query string del handshake
        $query = $conn->httpRequest->getUri()->getQuery();
        parse_str($query, $params);
        $this->userMap[$conn->resourceId] = $params['user_id'] ?? 'anon';
    }
}
?>

Arrancar el servidor

<?php
require 'vendor/autoload.php';

use RatchetServerIoServer;
use RatchetHttpHttpServer;
use RatchetWebSocketWsServer;

$server = IoServer::factory(
    new HttpServer(
        new WsServer(
            new Chat()
        )
    ),
    8080
);

$server->run();
?>

Arranca el proceso con php server.php. Mientras esté en ejecución, acepta conexiones en el puerto 8080.

Enviar un mensaje a un cliente concreto

Si quieres notificar a un usuario específico en lugar de hacer broadcast, recorre el storage y compara el ID:

<?php
public function notifyUser(int $userId, string $msg): void
{
    foreach ($this->clients as $client) {
        if (($this->userMap[$client->resourceId] ?? null) === $userId) {
            $client->send($msg);
            break;
        }
    }
}
?>

Supervisor para mantener el proceso activo

En producción, el proceso PHP se debe reiniciar si cae. Supervisor es el estándar:

[program:ratchet_chat]
command=php /var/www/mi-app/server.php
autostart=true
autorestart=true
stderr_logfile=/var/log/ratchet.err.log
stdout_logfile=/var/log/ratchet.out.log
user=www-data

Recarga Supervisor con supervisorctl reread && supervisorctl update.

nginx como proxy SSL (WSS)

Los navegadores modernos requieren WebSockets sobre TLS (wss://). nginx hace el proxy entre el exterior (443) y Ratchet (8080):

server {
    listen 443 ssl;
    server_name ws.ejemplo.com;

    ssl_certificate     /etc/letsencrypt/live/ws.ejemplo.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/ws.ejemplo.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_set_header Host $host;
        proxy_read_timeout 3600;
    }
}

Cliente JavaScript

const ws = new WebSocket('wss://ws.ejemplo.com');

ws.onopen    = () => console.log('Conectado');
ws.onmessage = e  => console.log('Mensaje:', e.data);
ws.onclose   = () => console.log('Desconectado');

ws.send(JSON.stringify({ tipo: 'chat', texto: 'Hola' }));

Errores comunes

  • Puerto bloqueado por firewall: abre el 8080 o usa el proxy nginx descrito arriba.
  • Mixed content: si la web va por HTTPS, el WebSocket debe usar wss://, no ws://.
  • Memoria que crece: en chats con muchos mensajes almacenados en el servidor, revisa que SplObjectStorage no acumule conexiones cerradas. El onClose debe llamar siempre a detach().
  • Bloqueos en el loop: no hagas operaciones síncronas lentas (consultas MySQL bloqueantes, sleep) dentro de los callbacks. ReactPHP tiene adaptadores para PDO asíncrono.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP