Librería ws
Crea servidores WebSocket de producción en Node.js con la librería ws, incluyendo latido, autenticación y apagado elegante.
Receta
Tarjeta de receta de referencia rápida: lista para copiar y pegar.
import { WebSocketServer, WebSocket } from "ws";
import { createServer } from "node:http";
const server = createServer();
const wss = new WebSocketServer({ noServer: true });
server.on("upgrade", (req, socket, head) => {
wss.handleUpgrade(req, socket, head, (ws) => wss.emit("connection", ws, req));
});
wss.on("connection", (ws) => {
ws.on("message", (data) => ws.send(data));
ws.on("close", () => console.log("Cliente desconectado"));
});
server.listen(3000);Cuándo usar esto: Necesitas control puro de WebSocket sin la sobrecarga de Socket.IO, o un gateway WebSocket de NestJS/Fastify.
Ejemplo de trabajo
import { WebSocketServer, WebSocket } from "ws";
import { createServer } from "node:http";
const server = createServer();
const wss = new WebSocketServer({ noServer: true });
function heartbeat() {
// @ts-expect-error custom property
this.isAlive = true;
}
server.on("upgrade", (req, socket, head) => {
const url = new URL(req.url ?? "/", "http://localhost");
const token = url.searchParams.get("token");
if (token !== process.env.WS_TOKEN) {
socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
socket.destroy();
return;
}
wss.handleUpgrade(req, socket, head, (ws) => wss.emit("connection", ws, req));
});
wss.on("connection", (ws: WebSocket) => {
ws.isAlive = true;
ws.on("pong", heartbeat);
ws.on("message", (raw) => {
const msg = JSON.parse(String(raw));
if (msg.type === "ping") {
ws.send(JSON.stringify({ type: "pong" }));
return;
}
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify({ type: "broadcast", data: msg }));
}
});
});
ws.on("close", () => console.log("El cliente se fue"));
});
// Latido cada 30s
const interval = setInterval(() => {
wss.clients.forEach((ws) => {
if (!ws.isAlive) { ws.terminate(); return; }
ws.isAlive = false;
ws.ping();
});
}, 30_000);
wss.on("close", () => clearInterval(interval));
server.listen(3000);Lo que esto demuestra:
- Autenticación de token durante la actualización HTTP
- Latido de ping/pong para detectar conexiones muertas
- Protocolo de mensajes JSON con campo de tipo
- Difusión a todos los clientes conectados
terminate()para clientes que no responden
Análisis profundo
Cómo funciona
wsimplementa el protocolo WebSocket (RFC 6455)noServer: truete permite manejar la actualización manualmente (para autenticación)ping()/pong()son tramas a nivel de protocolo (no mensajes de aplicación)wss.clientses un Set de todas las instancias de WebSocket conectadas
Conexión de cliente (navegador)
const ws = new WebSocket("ws://localhost:3000?token=secret");
ws.onopen = () => ws.send(JSON.stringify({ type: "hello" }));
ws.onmessage = (e) => console.log(JSON.parse(e.data));
ws.onclose = () => console.log("Desconectado");Integración con NestJS
@WebSocketGateway({ path: "/ws" })
export class EventsGateway {
@SubscribeMessage("message")
handleMessage(@MessageBody() data: string) {
return { event: "response", data };
}
}Errores comunes
- Sin autenticación en la actualización - cualquiera puede conectarse y enviar mensajes. Solución: valida el token antes de
handleUpgrade. - Sin latido - las conexiones muertas permanecen en
wss.clientspara siempre. Solución: intervalo de ping/pong conterminate(). - Difusión a todos los clientes - picos de CPU a escala. Solución: distribución basada en salas o pub/sub de Redis.
- No manejar
closeen el apagado del servidor - los clientes reciben RST. Solución: notifica a los clientes, luegowss.close(). - Análisis de JSON no validado - falla en mensajes mal formados. Solución: try/catch con respuesta de error.
- Fuga de memoria de los event listeners - los listeners se acumulan al reconectar. Solución: elimina los listeners en
close.
Alternativas
| Alternativa | Usar cuándo | No usar cuándo |
|---|---|---|
| Socket.IO | Necesitas salas, fallback, adaptador de Redis | Quieres una sobrecarga mínima del protocolo |
| SSE | Solo de servidor a cliente | Mensajería bidireccional |
| uWebSockets.js | Máximo rendimiento de WebSocket | Se prefiere la API más simple de ws |
| NestJS Gateway | Aplicación NestJS con decoradores | Proyecto que no es NestJS |
Preguntas frecuentes
¿ws vs Socket.IO?
ws es WebSocket puro. Socket.IO añade salas, espacios de nombres, fallback y adaptador de Redis. Usa ws cuando quieras control; Socket.IO cuando quieras características.
¿Cómo limito las conexiones por IP?
Rastrea las conexiones en un Map con la IP como clave durante la actualización. Rechaza cuando se excede el límite.
¿Funciona ws con Fastify?
Sí, a través del plugin @fastify/websocket, que envuelve ws internamente.
¿Cómo envío datos binarios?
ws.send(buffer) con Buffer o ArrayBuffer. Útil para fragmentos de archivos o protobuf.
¿Qué pasa con WSS (WebSocket seguro)?
Termina TLS en el proxy (igual que HTTPS). El cliente se conecta a wss:// que hace proxy a ws:// en Node.
¿Cuántos clientes puede manejar ws?
10k-50k inactivos por proceso. Depende de la tasa de mensajes y el patrón de distribución. Perfila tu carga de trabajo.
¿Cómo pruebo los servidores WebSocket?
Usa el cliente WebSocket ws en las pruebas, o el paquete npm websocket. Conéctate al puerto server.address().
¿Debo usar JSON o un protocolo binario?
JSON para la simplicidad. Binario (protobuf, msgpack) cuando el volumen o tamaño del mensaje lo exija.
Relacionado
- Conceptos básicos en tiempo real - selección de protocolo
- Socket.IO - alternativa de nivel superior
- Escalado en tiempo real - multi-instancia
- Conceptos básicos de HTTP en Node - actualización HTTP
- Mejores prácticas en tiempo real - lista de verificación de la sección
Versiones de la pila: Esta página fue escrita para Node.js 24.18.0 (LTS activa), npm 10+, TypeScript 5.6+, Express 5, Fastify 5 y NestJS 11.