Trabajo avanzado con WebSocket en JavaScript: del prototipo a producción
\nWebSocket es un protocolo que proporciona comunicación full-duplex entre cliente y servidor a través de una única conexión TCP persistente. En artículos anteriores analizamos los conceptos básicos: establecimiento de conexión, envío de mensajes y chats simples. Sin embargo, para crear aplicaciones en tiempo real verdaderamente robustas y escalables (juegos en línea, plataformas de trading, sistemas de edición colaborativa), es necesario dominar técnicas avanzadas. En este material, exploraremos la arquitectura de un servidor WebSocket de grado de producción en Node.js, incluyendo escalabilidad, gestión de estado, autorización y manejo de errores.
\n\n1. Arquitectura avanzada del servidor y gestión de salas
\nUn servidor WebSocket simple (basado en la biblioteca ws) maneja a todos los clientes en un único objeto global. En proyectos reales, se requiere aislamiento lógico: salas (rooms) y canales.
Consideremos un ejemplo de arquitectura utilizando la clase RoomManager:
const WebSocket = require('ws');\n\nclass RoomManager {\n constructor() {\n this.rooms = new Map(); // roomName -> Set<WebSocket>\n }\n\n join(ws, roomName) {\n if (!this.rooms.has(roomName)) {\n this.rooms.set(roomName, new Set());\n }\n this.rooms.get(roomName).add(ws);\n ws.room = roomName; // Guardamos la sala en el socket\n }\n\n leave(ws) {\n if (ws.room && this.rooms.has(ws.room)) {\n this.rooms.get(ws.room).delete(ws);\n if (this.rooms.get(ws.room).size === 0) {\n this.rooms.delete(ws.room);\n }\n }\n }\n\n broadcast(roomName, message, excludeWs = null) {\n if (!this.rooms.has(roomName)) return;\n this.rooms.get(roomName).forEach(client => {\n if (client !== excludeWs && client.readyState === WebSocket.OPEN) {\n client.send(JSON.stringify(message));\n }\n });\n }\n}\n\n// Uso\nconst wss = new WebSocket.Server({ port: 8080 });\nconst roomManager = new RoomManager();\n\nwss.on('connection', (ws) => {\n ws.on('message', (data) => {\n const msg = JSON.parse(data);\n switch (msg.type) {\n case 'join':\n roomManager.join(ws, msg.room);\n break;\n case 'message':\n roomManager.broadcast(ws.room, { user: msg.user, text: msg.text }, ws);\n break;\n }\n });\n\n ws.on('close', () => roomManager.leave(ws));\n});\nEste código permite crear chats aislados o sesiones de juego. Observe el manejo del cierre de conexión, esto es crítico para prevenir fugas de memoria.
\n\n2. Escalabilidad: clusterización y Redis Pub/Sub
\nUn solo proceso de Node.js no puede utilizar eficientemente más de un núcleo de CPU. Para escalar, usamos el módulo incorporado cluster. Sin embargo, surge un problema: las conexiones WebSocket están vinculadas a un worker específico. ¿Cómo enviar un mensaje a un usuario conectado a otro worker?
La solución es un broker de mensajes externo, por ejemplo, Redis Pub/Sub.
\nconst cluster = require('cluster');\nconst http = require('http');\nconst { Server } = require('ws');\nconst redis = require('redis');\n\nconst REDIS_CHANNEL = 'ws:messages';\n\nif (cluster.isMaster) {\n // Iniciamos workers según el número de núcleos\n const numCPUs = require('os').cpus().length;\n for (let i = 0; i < numCPUs; i++) {\n cluster.fork();\n }\n\n cluster.on('exit', (worker) => {\n console.log(`Worker ${worker.process.pid} died`);\n cluster.fork(); // Reinicio\n });\n} else {\n // Worker\n const server = http.createServer();\n const wss = new Server({ server });\n\n // Nos conectamos a Redis\n const pub = redis.createClient();\n const sub