¿Qué son los WebSockets?
Un WebSocket es una conexión persistente y full-duplex entre un cliente y un servidor. Tras un handshake inicial de HTTP, ambas partes pueden enviar mensajes en cualquier momento a través de la misma conexión TCP, con una sobrecarga muy baja por cada mensaje. El servidor puede enviar datos en el instante en que algo cambia, en lugar de esperar a que el cliente los solicite.
Esto convierte a los WebSockets en la herramienta ideal para chats, indicadores de presencia, dashboards en tiempo real, edición colaborativa, juegos multijugador y cualquier otra funcionalidad donde las actualizaciones deban llegar de inmediato. Para actualizaciones sencillas en una sola dirección, los Server-Sent Events suelen ser una opción más simple.
El handshake de actualización
Una conexión WebSocket comienza su vida como una solicitud HTTP.
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Si el servidor la acepta, responde:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Después de 101 Switching Protocols, la conexión deja de ser HTTP. Se convierte en un WebSocket y los datos fluyen en frames. Debido a que comienza como HTTP, funciona a través de los mismos puertos (80 y 443), pasa por la mayoría de los proxies y firewalls, y se actualiza a TLS mediante wss://.
Frames
Los mensajes de WebSocket se dividen en frames con un encabezado pequeño. Existen frames de texto, frames binarios y frames de control para ping, pong y close. El framing es mínimo, razón por la cual los mensajes de WebSocket son mucho más eficientes que las solicitudes HTTP repetidas.
El protocolo también define códigos de cierre, como 1000 para un cierre normal y 1001 para “going away”, los cuales ayudan a ambas partes a entender por qué terminó una conexión.
La API del navegador
La API del cliente es pequeña y está basada en eventos.
// client.js
const socket = new WebSocket("wss://api.example.com/chat");
socket.addEventListener("open", () => {
socket.send(JSON.stringify({ type: "join", room: "general" }));
});
socket.addEventListener("message", (event) => {
const message = JSON.parse(event.data);
render(message);
});
socket.addEventListener("close", (event) => {
console.log("closed", event.code, event.reason);
});
socket.addEventListener("error", () => {
console.error("connection error");
});
Envías datos con socket.send() y recibes eventos message. La propiedad readyState te indica si el socket se está conectando, está abierto, cerrándose o cerrado.
Un servidor Node.js
La librería ws es la forma estándar de ejecutar un servidor WebSocket en Node.
// server.js
import { WebSocketServer } from "ws";
const wss = new WebSocketServer({ port: 8080 });
wss.on("connection", (socket, request) => {
socket.on("message", (data, isBinary) => {
for (const client of wss.clients) {
if (client !== socket && client.readyState === client.OPEN) {
client.send(data, { binary: isBinary });
}
}
});
socket.on("close", () => console.log("client left"));
});
El servidor rastrea cada conexión y puede enviar mensajes a un cliente, a un subconjunto (una sala) o a todos ellos. Socket.IO es una alternativa de más alto nivel que añade salas, confirmaciones (acknowledgements), reconexión automática y fallbacks, a cambio de utilizar un protocolo y un cliente personalizados.
Salas y presencia
Las aplicaciones reales rara vez emiten mensajes a todo el mundo. En su lugar, rastreas a qué usuario o sala pertenece cada conexión.
// rooms.js
const rooms = new Map();
function join(socket, room) {
if (!rooms.has(room)) rooms.set(room, new Set());
rooms.get(room).add(socket);
}
function broadcast(room, payload) {
for (const socket of rooms.get(room) ?? []) {
if (socket.readyState === socket.OPEN) socket.send(payload);
}
}
Las salas, la presencia (quién está conectado) y los indicadores de escritura se construyen basándose en este patrón, sumando heartbeats para detectar las desconexiones.
Heartbeats y reconexión
Una conexión puede morir sin que ninguna de las partes se dé cuenta, especialmente en redes móviles o detrás de proxies. Hay dos cosas que ayudan a mantenerla saludable:
- Heartbeats. Envía un ping cada 30 segundos y termina la conexión si no llega un pong. Muchos proxies también cierran las conexiones inactivas, por lo que el tráfico constante las mantiene abiertas.
- Reconexión. El cliente debe reconectarse utilizando un exponential backoff y restablecer sus suscripciones. Aquí es donde una librería como Socket.IO ahorra mucho esfuerzo.
// heartbeat.js
const interval = setInterval(() => {
for (const socket of wss.clients) {
if (socket.isAlive === false) return socket.terminate();
socket.isAlive = false;
socket.ping();
}
}, 30_000);
wss.on("close", () => clearInterval(interval));
Escalabilidad
Una conexión WebSocket reside en una única instancia de servidor, por lo que para realizar un broadcast entre instancias es necesario un canal compartido.
- Pub/sub: publica los mensajes en Redis, NATS u otro broker, y haz que cada instancia los entregue a sus clientes locales.
- Sticky sessions: si tu infraestructura lo requiere, mantén al cliente en la misma instancia durante toda la vida de la conexión.
- Estado en un almacenamiento compartido: mantén la presencia y la membresía de las salas en Redis para que cualquier instancia pueda responder.
- Backpressure y límites: limita las conexiones por instancia y monitoriza la memoria, ya que cada conexión mantiene un socket y cierto estado.
Seguridad
- Utiliza siempre
wss://en producción para que el tráfico esté cifrado. - Autentica durante el handshake y rechaza las conexiones no autorizadas.
- Valida el header
Originpara prevenir el cross-site WebSocket hijacking. - Autoriza cada mensaje; nunca asumas que un cliente conectado tiene permiso para realizar una acción.
- Limita el tamaño y la frecuencia de los mensajes para evitar abusos.
- Configura timeouts para evitar que las conexiones abandonadas provoquen fugas de memoria.
Mejores prácticas
- Usa WebSockets para actualizaciones bidireccionales de baja latencia y SSE para el envío de datos unidireccional.
- Implementa heartbeat y reconexión; nunca asumas que una conexión sigue activa.
- Autentica durante el handshake y autoriza cada mensaje.
- Mantén el estado por conexión reducido y gestiona las salas en un store compartido.
- Usa pub/sub para realizar broadcasts entre instancias.
- Establece límites de tamaño de mensaje y de tasa de envío (rate limits).
- Finaliza las conexiones inactivas y monitorea el número de conexiones.
Errores comunes
- Usar WebSockets para peticiones y respuestas simples donde HTTP sería suficiente.
- Olvidar los heartbeats y provocar fugas de conexiones inactivas.
- Hacer broadcast a todos los clientes cuando solo una sala debería recibir el mensaje.
- Almacenar el estado autoritativo únicamente en memoria y perderlo al reiniciar.
- Omitir las comprobaciones de origen y la autorización de los mensajes.
- Asumir que el load balancer mantendrá la conexión sin la configuración adecuada.
Próximos pasos
Los WebSockets son la capa de conexión persistente sobre TCP. Refuerza estos conceptos con la guía de HTTP y la de TCP/IP & Sockets, ejecútalos en Node.js y compara el modelo push con las GraphQL subscriptions. Después, construye una pequeña sala de chat con canales y heartbeats para ver estos patrones en la práctica.