Construyendo Aplicaciones en Tiempo Real con WebSockets
Resumen
WebSockets habilitan comunicación persistente y bidireccional para características en tiempo real. Maneja el ciclo de vida de conexión cuidadosamente, implementa heartbeats para detectar conexiones muertas, usa Redis pub/sub para escalado horizontal y siempre ten mecanismos de fallback para confiabilidad.
Las características en tiempo real—chat en vivo, notificaciones, edición colaborativa, juegos—requieren conexiones persistentes entre cliente y servidor. WebSockets proveen exactamente eso. Este tutorial recorre la construcción de aplicaciones en tiempo real listas para producción.
Entendiendo WebSockets
┌─────────────────────────────────────────────────────────────────┐
│ Comunicación HTTP vs WebSocket │
├─────────────────────────────────────────────────────────────────┤
│ │
│ HTTP (Solicitud-Respuesta): │
│ │
│ Cliente ──────► Servidor (Solicitud) │
│ Cliente ◄────── Servidor (Respuesta) │
│ [Conexión cerrada] │
│ │
│ ───────────────────────────────────────────────────────────── │
│ │
│ WebSocket (Persistente, Bidireccional): │
│ │
│ Cliente ══════► Servidor (HTTP Upgrade handshake) │
│ Cliente ◄══════► Servidor [Conexión permanece abierta] │
│ │
│ Cliente ──────► Servidor (Mensaje en cualquier momento) │
│ Cliente ◄────── Servidor (Mensaje en cualquier momento) │
│ Cliente ◄────── Servidor (Push sin solicitud) │
│ │
└─────────────────────────────────────────────────────────────────┘
Implementación Básica del Servidor
Node.js con Librería ws
// server.ts
import { WebSocketServer, WebSocket } from 'ws';
import { createServer } from 'http';
import { v4 as uuid } from 'uuid';
interface Client {
id: string;
socket: WebSocket;
userId?: string;
rooms: Set<string>;
lastPing: number;
}
class WebSocketManager {
private wss: WebSocketServer;
private clients: Map<string, Client> = new Map();
private rooms: Map<string, Set<string>> = new Map();
constructor(server: ReturnType<typeof createServer>) {
this.wss = new WebSocketServer({ server });
this.setupConnectionHandler();
this.startHeartbeat();
}
private setupConnectionHandler() {
this.wss.on('connection', (socket, request) => {
const clientId = uuid();
const client: Client = {
id: clientId,
socket,
rooms: new Set(),
lastPing: Date.now(),
};
this.clients.set(clientId, client);
console.log(`Cliente conectado: ${clientId}`);
// Enviar reconocimiento de conexión
this.send(socket, {
type: 'connected',
clientId,
});
// Manejar mensajes entrantes
socket.on('message', (data) => {
try {
const message = JSON.parse(data.toString());
this.handleMessage(client, message);
} catch (error) {
console.error('Formato de mensaje inválido:', error);
}
});
// Manejar desconexión
socket.on('close', () => {
this.handleDisconnect(client);
});
// Responder a pings
socket.on('pong', () => {
client.lastPing = Date.now();
});
});
}
private startHeartbeat() {
setInterval(() => {
const now = Date.now();
const timeout = 30000; // 30 segundos
for (const [clientId, client] of this.clients) {
if (now - client.lastPing > timeout) {
// La conexión está muerta
console.log(`Cliente ${clientId} expiró`);
client.socket.terminate();
this.handleDisconnect(client);
} else if (client.socket.readyState === WebSocket.OPEN) {
// Enviar ping
client.socket.ping();
}
}
}, 10000); // Verificar cada 10 segundos
}
}Insight Clave
Siempre implementa heartbeat/ping-pong para detectar conexiones muertas. TCP no te notifica cuando una conexión se cae silenciosamente (ej. falla de red del cliente). Sin heartbeats, tendrás conexiones zombie consumiendo recursos.
Implementación del Cliente
React Hook para WebSocket
// useWebSocket.ts
import { useEffect, useRef, useCallback, useState } from 'react';
interface UseWebSocketOptions {
url: string;
onMessage?: (message: any) => void;
onOpen?: () => void;
reconnect?: boolean;
reconnectAttempts?: number;
reconnectInterval?: number;
}
export function useWebSocket({
url,
onMessage,
onOpen,
reconnect = true,
reconnectAttempts = 5,
reconnectInterval = 3000,
}: UseWebSocketOptions) {
const [isConnected, setIsConnected] = useState(false);
const wsRef = useRef<WebSocket | null>(null);
const reconnectCountRef = useRef(0);
const connect = useCallback(() => {
const ws = new WebSocket(url);
ws.onopen = () => {
setIsConnected(true);
reconnectCountRef.current = 0;
onOpen?.();
// Iniciar heartbeat del lado del cliente
const heartbeat = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'ping' }));
}
}, 25000);
ws.addEventListener('close', () => clearInterval(heartbeat));
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
onMessage?.(message);
};
ws.onclose = () => {
setIsConnected(false);
// Intentar reconexión
if (reconnect && reconnectCountRef.current < reconnectAttempts) {
reconnectCountRef.current++;
const delay = reconnectInterval * Math.pow(2, reconnectCountRef.current - 1);
console.log(`Reconectando en ${delay}ms (intento ${reconnectCountRef.current})`);
setTimeout(connect, delay);
}
};
wsRef.current = ws;
}, [url, onMessage, onOpen, reconnect, reconnectAttempts, reconnectInterval]);
const send = useCallback((message: any) => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(JSON.stringify(message));
}
}, []);
useEffect(() => {
connect();
return () => wsRef.current?.close();
}, [connect]);
return { send, isConnected };
}Escalando con Redis Pub/Sub
// Escalando WebSockets a través de múltiples servidores
import Redis from 'ioredis';
class ScaledWebSocketManager extends WebSocketManager {
private publisher: Redis;
private subscriber: Redis;
constructor(server: ReturnType<typeof createServer>) {
super(server);
this.publisher = new Redis(process.env.REDIS_URL);
this.subscriber = new Redis(process.env.REDIS_URL);
this.setupPubSub();
}
private setupPubSub() {
// Suscribirse a todos los canales de sala
this.subscriber.psubscribe('room:*');
this.subscriber.on('pmessage', (pattern, channel, message) => {
const roomId = channel.replace('room:', '');
const parsed = JSON.parse(message);
// Solo transmitir si el mensaje no se originó en este servidor
if (parsed.serverId !== this.serverId) {
this.localBroadcastToRoom(roomId, parsed.message);
}
});
}
// Sobrescribir para publicar a Redis en lugar de broadcast solo local
protected broadcastToRoom(roomId: string, message: any, excludeClientId?: string) {
// Publicar a Redis para otros servidores
this.publisher.publish(`room:${roomId}`, JSON.stringify({
serverId: this.serverId,
message,
excludeClientId,
}));
// También transmitir localmente
this.localBroadcastToRoom(roomId, message, excludeClientId);
}
}Consideraciones de Seguridad
Autenticación Durante el Handshake
// Servidor WebSocket seguro con autenticación JWT
const wss = new WebSocketServer({
server,
verifyClient: async (info, callback) => {
try {
const url = new URL(info.req.url!, `http://${info.req.headers.host}`);
const token = url.searchParams.get('token');
if (!token) {
callback(false, 401, 'No autorizado');
return;
}
const user = await verifyJWT(token);
(info.req as any).user = user;
callback(true);
} catch (error) {
callback(false, 401, 'Token inválido');
}
},
});Conclusión
Construir aplicaciones WebSocket de producción requiere atención a:
- Ciclo de vida de conexión - Maneja connect, disconnect y errores graciosamente
- Heartbeats - Detecta conexiones muertas con ping/pong
- Escalado - Usa pub/sub para despliegues multi-servidor
- Seguridad - Autentica durante handshake, valida todos los mensajes
- Confiabilidad - Implementa reconexión con backoff exponencial
- Rate limiting - Protege contra abuso
WebSockets desbloquean experiencias en tiempo real poderosas. Úsalos cuando el beneficio de UX justifique la complejidad operacional.
Referencias
Fette, I., & Melnikov, A. (2011). The WebSocket Protocol (RFC 6455). IETF. https://tools.ietf.org/html/rfc6455
MDN Web Docs. (2024). WebSocket API. https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API
Socket.IO. (2024). Socket.IO documentation. https://socket.io/docs/v4/
¿Construyendo características en tiempo real? Contáctame para discutir estrategias de arquitectura WebSocket.
Frequently Asked Questions
Osvaldo Restrepo
Senior Full Stack AI & Software Engineer. Building production AI systems that solve real problems.