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://, nows://. - Memoria que crece: en chats con muchos mensajes almacenados en el servidor, revisa que
SplObjectStorageno acumule conexiones cerradas. ElonClosedebe llamar siempre adetach(). - 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.
