WebSockets: Conexiones Bidireccionales en Tiempo Real
Imagina que estás en un chat y cada vez que quisieras saber si hay mensajes nuevos, tuvieras que refrescar la página manualmente. Frustrante, ¿verdad? Durante años, la web funcionó así: el cliente preguntaba, el servidor respondía, y la conexión se cerraba. Fin de la historia.
WebSockets cambió las reglas del juego. Permite que cliente y servidor mantengan una conversación abierta y continua, donde cualquiera de los dos puede enviar mensajes en cualquier momento.
El problema con HTTP tradicional
HTTP fue diseñado como un protocolo de petición-respuesta:
Cliente Servidor
| |
|-------- GET /mensajes -------->|
|<------- [respuesta] -----------|
| (conexión cerrada) |
| |
|-------- GET /mensajes -------->|
|<------- [respuesta] -----------|
| (conexión cerrada) |
Para simular “tiempo real”, los desarrolladores inventaron técnicas como:
| Técnica | Cómo funciona | Problema |
|---|---|---|
| Polling | Preguntar cada X segundos | Desperdicia recursos, latencia alta |
| Long Polling | Mantener petición abierta hasta que haya datos | Complejo, reconexiones constantes |
| Server-Sent Events | Servidor envía eventos al cliente | Unidireccional (solo servidor → cliente) |
Ninguna era ideal para comunicación bidireccional real.
WebSockets: la solución elegante
WebSockets establece una conexión persistente y full-duplex sobre TCP:
Cliente Servidor
| |
|==== Handshake HTTP Upgrade ===>|
|<=== 101 Switching Protocols ===|
| |
|====== WebSocket abierto =======|
| |
|<-------- mensaje --------------|
|--------- mensaje ------------->|
|<-------- mensaje --------------|
|--------- mensaje ------------->|
| |
| (conexión permanece abierta) |
Características clave
- Full-duplex: Ambas partes pueden enviar datos simultáneamente
- Persistente: Una sola conexión TCP para toda la sesión
- Baja latencia: Sin overhead de headers HTTP por cada mensaje
- Eficiente: Frames mínimos de 2-14 bytes vs cientos en HTTP
El handshake: de HTTP a WebSocket
Todo comienza con una petición HTTP normal con headers especiales:
GET /chat HTTP/1.1
Host: servidor.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
El servidor responde aceptando el “upgrade”:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
El valor de Sec-WebSocket-Accept es un hash del Sec-WebSocket-Key concatenado con un GUID específico, probando que el servidor entiende WebSockets.
A partir de este momento, la conexión TCP deja de hablar HTTP y comienza el protocolo WebSocket.
Anatomía de un frame WebSocket
Una vez establecida la conexión, los datos viajan en frames:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+-------------------------------+
| Extended payload length continued, if payload len == 127 |
+-------------------------------+-------------------------------+
| | Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------+-------------------------------+
| Payload Data continued ... |
+---------------------------------------------------------------+
Campos importantes
| Campo | Bits | Descripción |
|---|---|---|
| FIN | 1 | Indica si es el frame final del mensaje |
| Opcode | 4 | Tipo: texto (0x1), binario (0x2), close (0x8), ping (0x9), pong (0xA) |
| MASK | 1 | Los mensajes del cliente DEBEN estar enmascarados |
| Payload length | 7+ | Longitud de los datos |
Tamaño mínimo
Un mensaje pequeño puede viajar en solo 2 bytes de overhead (sin máscara) o 6 bytes (con máscara). Compara esto con los 500-800 bytes de headers HTTP típicos.
Implementación práctica
Cliente (JavaScript)
// Crear conexión
const ws = new WebSocket('wss://servidor.com/chat');
// Conexión establecida
ws.onopen = () => {
console.log('Conectado!');
ws.send(JSON.stringify({ tipo: 'saludo', mensaje: 'Hola!' }));
};
// Recibir mensajes
ws.onmessage = (evento) => {
const datos = JSON.parse(evento.data);
console.log('Mensaje recibido:', datos);
};
// Manejar errores
ws.onerror = (error) => {
console.error('Error WebSocket:', error);
};
// Conexión cerrada
ws.onclose = (evento) => {
console.log(`Desconectado: ${evento.code} - ${evento.reason}`);
};
Servidor (Node.js con ws)
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
// Set para trackear clientes conectados
const clientes = new Set();
wss.on('connection', (ws) => {
clientes.add(ws);
console.log(`Cliente conectado. Total: ${clientes.size}`);
ws.on('message', (data) => {
const mensaje = JSON.parse(data);
console.log('Recibido:', mensaje);
// Broadcast a todos los clientes
for (const cliente of clientes) {
if (cliente.readyState === ws.OPEN) {
cliente.send(JSON.stringify({
tipo: 'broadcast',
de: 'servidor',
contenido: mensaje
}));
}
}
});
ws.on('close', () => {
clientes.delete(ws);
console.log(`Cliente desconectado. Total: ${clientes.size}`);
});
});
console.log('Servidor WebSocket en puerto 8080');
Ping/Pong: manteniendo la conexión viva
WebSockets incluye un mecanismo de heartbeat para detectar conexiones muertas:
Cliente Servidor
| |
|<-------- Ping frame -----------|
|--------- Pong frame ---------->|
| |
| (conexión verificada activa) |
Los frames de ping/pong son de control (opcode 0x9 y 0xA) y pueden llevar datos de hasta 125 bytes. El navegador responde automáticamente a los pings.
Configuración típica
// Servidor envía ping cada 30 segundos
const intervalo = setInterval(() => {
wss.clients.forEach((ws) => {
if (!ws.isAlive) {
return ws.terminate();
}
ws.isAlive = false;
ws.ping();
});
}, 30000);
wss.on('connection', (ws) => {
ws.isAlive = true;
ws.on('pong', () => {
ws.isAlive = true;
});
});
Seguridad: WSS y consideraciones
WSS (WebSocket Secure)
Siempre usa wss:// en producción. Funciona igual que HTTPS:
ws:// → WebSocket sobre TCP (puerto 80)
wss:// → WebSocket sobre TLS/TCP (puerto 443)
Validaciones importantes
wss.on('connection', (ws, request) => {
// 1. Verificar origen
const origen = request.headers.origin;
if (!origenesPermitidos.includes(origen)) {
ws.close(1008, 'Origen no permitido');
return;
}
// 2. Autenticar (token en query o primer mensaje)
const token = new URL(request.url, 'http://localhost').searchParams.get('token');
if (!verificarToken(token)) {
ws.close(1008, 'No autorizado');
return;
}
// 3. Rate limiting
if (excedeLimite(request.socket.remoteAddress)) {
ws.close(1008, 'Demasiadas conexiones');
return;
}
});
Casos de uso ideales
| Aplicación | Por qué WebSockets |
|---|---|
| Chat en tiempo real | Mensajes instantáneos bidireccionales |
| Juegos multijugador | Sincronización de estado con baja latencia |
| Trading/Finanzas | Actualizaciones de precios en milisegundos |
| Colaboración en vivo | Google Docs, Figma, editores compartidos |
| Notificaciones push | Alertas inmediatas sin polling |
| IoT y telemetría | Streaming de datos de sensores |
WebSockets vs alternativas
| Criterio | WebSockets | SSE | Long Polling | HTTP/2 Push |
|---|---|---|---|---|
| Dirección | Bidireccional | Solo servidor→cliente | Bidireccional (simulado) | Solo servidor→cliente |
| Conexión | Persistente | Persistente | Reconexiones | Persistente |
| Overhead | Muy bajo | Bajo | Alto | Medio |
| Soporte navegador | Universal | Muy bueno | Universal | Bueno |
| Complejidad | Media | Baja | Alta | Alta |
Regla práctica: Si necesitas que el cliente envíe datos frecuentemente, usa WebSockets. Si solo el servidor envía actualizaciones, considera SSE por su simplicidad.
Escalando WebSockets
El desafío de escalar conexiones persistentes:
┌─────────────┐
│ Load │
│ Balancer │
│ (sticky) │
└──────┬──────┘
┌───────────────┼───────────────┐
│ │ │
┌─────┴─────┐ ┌─────┴─────┐ ┌─────┴─────┐
│ Server 1 │ │ Server 2 │ │ Server 3 │
│ (1000 ws) │ │ (1000 ws) │ │ (1000 ws) │
└─────┬─────┘ └─────┬─────┘ └─────┬─────┘
│ │ │
└───────────────┼───────────────┘
│
┌──────┴──────┐
│ Redis │
│ Pub/Sub │
└─────────────┘
Sticky sessions
Las conexiones WebSocket deben mantenerse en el mismo servidor:
upstream websocket {
ip_hash; # Sticky por IP
server server1:8080;
server server2:8080;
server server3:8080;
}
Pub/Sub para broadcast
Para enviar mensajes a usuarios en diferentes servidores:
import Redis from 'ioredis';
const pub = new Redis();
const sub = new Redis();
// Suscribirse al canal
sub.subscribe('chat:broadcast');
sub.on('message', (canal, mensaje) => {
// Enviar a todos los clientes locales
wss.clients.forEach(ws => ws.send(mensaje));
});
// Publicar mensaje (desde cualquier servidor)
pub.publish('chat:broadcast', JSON.stringify({ texto: 'Hola a todos!' }));
Códigos de cierre
Cuando una conexión se cierra, incluye un código que indica la razón:
| Código | Nombre | Significado |
|---|---|---|
| 1000 | Normal Closure | Cierre limpio, operación completada |
| 1001 | Going Away | Servidor apagándose o navegador cerrando |
| 1002 | Protocol Error | Error en el protocolo WebSocket |
| 1003 | Unsupported Data | Tipo de datos no soportado |
| 1006 | Abnormal Closure | Conexión cerrada sin frame de close |
| 1008 | Policy Violation | Mensaje viola política del servidor |
| 1011 | Internal Error | Error inesperado en el servidor |
ws.close(1000, 'Sesión terminada por el usuario');
Conclusión
WebSockets transformó la web de un modelo de petición-respuesta a uno de comunicación continua y bidireccional. Sus ventajas clave:
- Baja latencia: Ideal para tiempo real
- Eficiencia: Mínimo overhead por mensaje
- Simplicidad: API limpia en navegadores
- Flexibilidad: Soporta texto y binario
La próxima vez que uses un chat, juegues online, o veas precios actualizarse al instante, recuerda: hay un WebSocket trabajando detrás de escena, manteniendo esa conversación abierta entre tu navegador y el servidor.
Este artículo es parte de la serie sobre Protocolos de Red.