WebSocket en PHP: de la teoría a la aplicación en tiempo real de nivel industrial
El protocolo WebSocket revolucionó el desarrollo web, permitiendo crear canales de comunicación bidireccionales completos entre el cliente y el servidor. A diferencia del AJAX clásico (long polling, Server-Sent Events), WebSocket proporciona una conexión permanente con una sobrecarga mínima. Para un desarrollador de PHP, la transición de ejemplos básicos de "servidor eco" a una aplicación en tiempo real de nivel industrial es un desafío importante, relacionado con la gestión de estado, el escalado y el procesamiento asíncrono.
En este artículo, exploraremos técnicas avanzadas para trabajar con WebSocket en PHP: arquitectura de chat multiusuario, integración con Redis para escalado horizontal, procesamiento de tareas en segundo plano y optimización del rendimiento.
1. Arquitectura de un servidor WebSocket avanzado con Ratchet
La biblioteca Ratchet (basada en ReactPHP) sigue siendo el estándar de oro para WebSocket en PHP. Sin embargo, para soluciones de producción, un simple onMessage no es suficiente. Analicemos una arquitectura basada en componentes con separación de responsabilidades:
<?phpuse Ratchet\\MessageComponentInterface;use Ratchet\\ConnectionInterface;use React\\EventLoop\\LoopInterface;
class RealTimeHub implements MessageComponentInterface { private $clients; private $loop; private $redis; private $userRegistry;
public function __construct(LoopInterface $loop, \\Redis $redis) { $this->clients = new \\SplObjectStorage; $this->loop = $loop; $this->redis = $redis; $this->userRegistry = []; }
public function onOpen(ConnectionInterface $conn) { $this->clients->attach($conn); echo "Nueva conexión: {$conn->resourceId}"; }
public function onMessage(ConnectionInterface $from, $msg) { $data = json_decode($msg, true);
switch ($data['type'] ?? '') { case 'auth': $this->handleAuth($from, $data); break; case 'message': $this->handleMessage($from, $data); break; case 'subscribe': $this->handleSubscribe($from, $data); break; default: $from->send(json_encode(['error' => 'Unknown command'])); } }
public function onClose(ConnectionInterface $conn) { $this->clients->detach($conn); // Limpiar registro de usuario if (isset($conn->userId)) { unset($this->userRegistry[$conn->userId]); } }
public function onError(ConnectionInterface $conn, \\Exception $e) { $conn->close(); }
private function handleAuth(ConnectionInterface $conn, array $data) { $token = $data['token'] ?? ''; // Verificar JWT token $userId = $this->validateToken($token); if ($userId) { $conn->userId = $userId; $this->userRegistry[$userId] = $conn; $conn->send(json_encode(['type' => 'auth_ok'])); } else { $conn->send(json_encode(['type' => 'auth_fail'])); } }
private function handleMessage(ConnectionInterface $from, array $data) { $targetUserId = $data['to'] ?? null; if ($targetUserId && isset($this->userRegistry[$targetUserId])) { $this->userRegistry[$targetUserId]->send(json_encode([ 'type' => 'message', 'from' => $from->userId ?? 'anonymous', 'text' => $data['text'] ])); } }
private function validateToken(string $token): ?int { // Simular verificación de JWT $payload = json_decode(base64_decode(explode('.', $token)[1] ?? ''), true); return $payl