HTTP fue diseñado para un modelo de petición-respuesta: el cliente pide, el servidor responde y la conexión se cierra. Ese modelo funciona perfectamente para cargar páginas web o consultar datos, pero falla cuando necesitas que el servidor envíe información al cliente sin que este la pida: notificaciones en tiempo real, mensajes de un chat, precios de bolsa actualizados al segundo o el estado de una carrera en vivo. WebSocket resuelve exactamente ese problema.
En esta guía aprenderás cómo funciona WebSocket, en qué se diferencia de HTTP, y cómo implementar un servidor y un cliente desde cero.
El problema que resuelve WebSocket
Antes de WebSocket, los desarrolladores usaban técnicas como el polling y el long polling para simular comunicación en tiempo real, con resultados mediocres.
# Polling: preguntar al servidor cada N segundos
# "¿Hay mensajes nuevos?" → "No"
# "¿Hay mensajes nuevos?" → "No"
# "¿Hay mensajes nuevos?" → "Sí, aquí están"
# "¿Hay mensajes nuevos?" → "No"
# JavaScript: polling cada 2 segundos
setInterval(async () => {
const respuesta = await fetch('/api/mensajes-nuevos');
const datos = await respuesta.json();
if (datos.length > 0) mostrarMensajes(datos);
}, 2000);
# Problemas:
# - Ineficiente: la mayoría de peticiones devuelven "no hay nada nuevo"
# - Latencia: si llega un mensaje justo después de una petición, esperas ~2s
# - Carga innecesaria en el servidor: cientos de peticiones por minuto por usuario
# Long polling: mantener la conexión abierta hasta que haya datos
# El servidor no responde hasta que tiene algo que enviar
async function longPolling() {
while (true) {
try {
// La petición se queda "colgada" hasta que el servidor responde
const respuesta = await fetch('/api/esperar-mensajes');
const datos = await respuesta.json();
mostrarMensajes(datos);
} catch (error) {
await new Promise(r => setTimeout(r, 1000)); // esperar antes de reintentar
}
}
}
# Mejor que polling, pero sigue siendo una petición HTTP nueva para cada batch de datos
# No es bidireccional: el cliente no puede enviar datos mientras espera
WebSocket establece una conexión persistente y bidireccional: tanto el cliente como el servidor pueden enviar datos en cualquier momento, sin necesidad de hacer nuevas peticiones.
Cómo funciona WebSocket
WebSocket empieza como una petición HTTP normal (el handshake) y luego "actualiza" la conexión a WebSocket. A partir de ese momento, la conexión TCP permanece abierta y ambos extremos pueden enviarse mensajes cuando quieran.
# El handshake WebSocket: empieza como HTTP, luego cambia de protocolo
# 1. El cliente envía una petición HTTP con cabeceras especiales
GET /chat HTTP/1.1
Host: servidor.com
Upgrade: websocket # quiero cambiar a WebSocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== # clave aleatoria en base64
Sec-WebSocket-Version: 13
# 2. El servidor acepta el cambio de protocolo
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= # respuesta firmada con la clave
# 3. A partir de aquí, la conexión TCP sigue abierta pero ya no es HTTP
# Los datos viajan en frames WebSocket (binario, muy eficiente)
# No hay más peticiones/respuestas: es un canal bidireccional continuo
# URL WebSocket:
# ws://dominio.com/ruta → no cifrado (como HTTP)
# wss://dominio.com/ruta → cifrado con TLS (como HTTPS, usar siempre en producción)
# Comparativa HTTP vs WebSocket
# HTTP:
# Cliente → [PETICIÓN] → Servidor
# Cliente ← [RESPUESTA] ← Servidor
# (conexión se cierra)
# (para más datos: nueva petición)
# WebSocket:
# Cliente ⟷ [HANDSHAKE] ⟷ Servidor (una sola vez)
# Cliente → [MENSAJE] → Servidor (cualquier momento)
# Cliente ← [MENSAJE] ← Servidor (cualquier momento)
# Cliente → [MENSAJE] → Servidor
# Cliente ← [MENSAJE] ← Servidor
# ... (la conexión permanece abierta indefinidamente)
# Cliente → [CLOSE] → Servidor (cuando se quiere cerrar)
WebSocket en el navegador: la API nativa
Los navegadores modernos incluyen la API WebSocket de forma nativa, sin necesidad de librerías.
# JavaScript: API WebSocket del navegador
// Crear una conexión WebSocket
const ws = new WebSocket('wss://mi-servidor.com/ws');
// Estado de la conexión
console.log(ws.readyState);
// 0 = CONNECTING: estableciendo la conexión
// 1 = OPEN: conexión establecida, se puede enviar y recibir
// 2 = CLOSING: cerrando la conexión
// 3 = CLOSED: conexión cerrada
// Evento: conexión establecida
ws.addEventListener('open', (evento) => {
console.log('Conexión WebSocket establecida');
// Enviar un mensaje al servidor
ws.send('Hola servidor');
// Enviar JSON (hay que serializar manualmente)
ws.send(JSON.stringify({
tipo: 'unirse_sala',
sala: 'general',
usuario: 'Ana'
}));
});
// Evento: mensaje recibido del servidor
ws.addEventListener('message', (evento) => {
console.log('Mensaje recibido:', evento.data);
// Si el servidor envía JSON, hay que parsear
try {
const datos = JSON.parse(evento.data);
procesarMensaje(datos);
} catch {
// El mensaje era texto plano, no JSON
console.log('Texto:', evento.data);
}
});
// Evento: error
ws.addEventListener('error', (evento) => {
console.error('Error en WebSocket:', evento);
});
// Evento: conexión cerrada
ws.addEventListener('close', (evento) => {
console.log(`Conexión cerrada. Código: ${evento.code}, Motivo: ${evento.reason}`);
// Códigos habituales:
// 1000: cierre normal
// 1001: el endpoint se fue (navegador cerró la pestaña)
// 1006: cierre anormal (sin frame de cierre, problema de red)
// 1011: error interno del servidor
});
// Cerrar la conexión desde el cliente
ws.close(1000, 'El usuario cerró el chat');
Reconexión automática
WebSocket no reconecta automáticamente si la conexión se pierde. En producción es fundamental implementar reconexión con backoff exponencial.
# Clase de WebSocket con reconexión automática
class WebSocketConReconexion {
constructor(url, opciones = {}) {
this.url = url;
this.maxReintentos = opciones.maxReintentos || 10;
this.esperaInicial = opciones.esperaInicial || 1000; // 1 segundo
this.esperaMaxima = opciones.esperaMaxima || 30000; // 30 segundos
this.intento = 0;
this.cerradoManual = false;
this.handlers = { open: [], message: [], close: [], error: [] };
this.conectar();
}
conectar() {
this.ws = new WebSocket(this.url);
this.ws.addEventListener('open', (e) => {
console.log('WebSocket conectado');
this.intento = 0; // resetear contador al reconectar
this.handlers.open.forEach(h => h(e));
});
this.ws.addEventListener('message', (e) => {
this.handlers.message.forEach(h => h(e));
});
this.ws.addEventListener('close', (e) => {
this.handlers.close.forEach(h => h(e));
if (!this.cerradoManual && this.intento < this.maxReintentos) {
// Backoff exponencial: 1s, 2s, 4s, 8s... hasta el máximo
const espera = Math.min(
this.esperaInicial * Math.pow(2, this.intento),
this.esperaMaxima
);
this.intento++;
console.log(`Reconectando en ${espera / 1000}s (intento ${this.intento})...`);
setTimeout(() => this.conectar(), espera);
}
});
this.ws.addEventListener('error', (e) => {
this.handlers.error.forEach(h => h(e));
});
}
on(evento, handler) { this.handlers[evento].push(handler); return this; }
send(datos) { if (this.ws.readyState === WebSocket.OPEN) this.ws.send(datos); }
close(codigo, motivo) { this.cerradoManual = true; this.ws.close(codigo, motivo); }
}
// Uso
const ws = new WebSocketConReconexion('wss://mi-servidor.com/ws');
ws.on('open', () => ws.send(JSON.stringify({ tipo: 'ping' })));
ws.on('message', (e) => console.log('Mensaje:', e.data));
Servidor WebSocket en Node.js
# npm install ws
const { WebSocketServer } = require('ws');
const http = require('http');
// Crear servidor HTTP (WebSocket se monta sobre él)
const servidor = http.createServer();
const wss = new WebSocketServer({ server: servidor });
// Gestionar conexiones
wss.on('connection', (ws, peticion) => {
const ip = peticion.socket.remoteAddress;
console.log(`Cliente conectado desde ${ip}`);
console.log(`Clientes conectados: ${wss.clients.size}`);
// Enviar mensaje de bienvenida al cliente recién conectado
ws.send(JSON.stringify({
tipo: 'bienvenida',
mensaje: 'Conectado al servidor',
hora: new Date().toISOString()
}));
// Recibir mensajes del cliente
ws.on('message', (datos) => {
try {
const mensaje = JSON.parse(datos.toString());
console.log('Mensaje recibido:', mensaje);
manejarMensaje(ws, mensaje);
} catch {
console.warn('Mensaje no es JSON válido:', datos.toString());
}
});
// Cuando el cliente se desconecta
ws.on('close', (codigo, motivo) => {
console.log(`Cliente desconectado. Código: ${codigo}`);
});
ws.on('error', (error) => {
console.error('Error en WebSocket del cliente:', error);
});
});
function manejarMensaje(ws, mensaje) {
switch (mensaje.tipo) {
case 'ping':
ws.send(JSON.stringify({ tipo: 'pong', hora: new Date().toISOString() }));
break;
case 'eco':
ws.send(JSON.stringify({ tipo: 'eco', datos: mensaje.datos }));
break;
default:
ws.send(JSON.stringify({ tipo: 'error', mensaje: 'Tipo de mensaje desconocido' }));
}
}
servidor.listen(8080, () => {
console.log('Servidor WebSocket en ws://localhost:8080');
});
Broadcast: enviar a todos los clientes conectados
# Enviar un mensaje a todos los clientes conectados
function broadcast(wss, datos) {
const mensaje = JSON.stringify(datos);
wss.clients.forEach(cliente => {
if (cliente.readyState === WebSocket.OPEN) {
cliente.send(mensaje);
}
});
}
// Ejemplo: notificar a todos cuando alguien se conecta
wss.on('connection', (ws) => {
broadcast(wss, {
tipo: 'notificacion',
mensaje: 'Un nuevo usuario se unió'
});
});
// Ejemplo: emisión periódica a todos los clientes
setInterval(() => {
broadcast(wss, {
tipo: 'hora_servidor',
hora: new Date().toISOString()
});
}, 5000);
Ejemplo completo: chat en tiempo real
# Servidor: servidor-chat.js
const { WebSocketServer, WebSocket } = require('ws');
const http = require('http');
const servidor = http.createServer();
const wss = new WebSocketServer({ server: servidor });
// Mantener registro de usuarios conectados
const usuarios = new Map(); // ws → { nombre, sala }
wss.on('connection', (ws) => {
ws.on('message', (datos) => {
let mensaje;
try {
mensaje = JSON.parse(datos.toString());
} catch {
return;
}
switch (mensaje.tipo) {
case 'unirse': {
// Registrar al usuario
usuarios.set(ws, { nombre: mensaje.nombre, sala: mensaje.sala });
console.log(`${mensaje.nombre} se unió a #${mensaje.sala}`);
// Confirmar al usuario que entró
ws.send(JSON.stringify({
tipo: 'confirmacion_union',
sala: mensaje.sala,
usuarios: [...usuarios.values()]
.filter(u => u.sala === mensaje.sala)
.map(u => u.nombre)
}));
// Notificar a la sala
enviarASala(mensaje.sala, ws, {
tipo: 'usuario_unio',
nombre: mensaje.nombre,
hora: new Date().toISOString()
});
break;
}
case 'mensaje': {
const usuario = usuarios.get(ws);
if (!usuario) return;
// Reenviar el mensaje a todos en la misma sala
enviarASala(usuario.sala, null, {
tipo: 'mensaje',
nombre: usuario.nombre,
texto: mensaje.texto,
hora: new Date().toISOString()
});
break;
}
}
});
ws.on('close', () => {
const usuario = usuarios.get(ws);
if (usuario) {
usuarios.delete(ws);
enviarASala(usuario.sala, null, {
tipo: 'usuario_salio',
nombre: usuario.nombre,
hora: new Date().toISOString()
});
}
});
});
function enviarASala(sala, excluir, datos) {
const mensaje = JSON.stringify(datos);
usuarios.forEach((info, clienteWs) => {
if (info.sala === sala && clienteWs !== excluir
&& clienteWs.readyState === WebSocket.OPEN) {
clienteWs.send(mensaje);
}
});
}
servidor.listen(8080, () => console.log('Chat en ws://localhost:8080'));
# Cliente HTML: index.html
<!DOCTYPE html>
<html lang="es">
<head><meta charset="UTF-8"><title>Chat WebSocket</title></head>
<body>
<div id="mensajes" style="height:400px;overflow-y:auto;border:1px solid #ccc;padding:10px"></div>
<input id="texto" placeholder="Escribe un mensaje..." style="width:80%">
<button onclick="enviar()">Enviar</button>
<script>
const nombre = prompt('Tu nombre:') || 'Anónimo';
const sala = 'general';
const ws = new WebSocket('ws://localhost:8080');
ws.addEventListener('open', () => {
ws.send(JSON.stringify({ tipo: 'unirse', nombre, sala }));
});
ws.addEventListener('message', (e) => {
const datos = JSON.parse(e.data);
const div = document.getElementById('mensajes');
switch (datos.tipo) {
case 'mensaje':
div.innerHTML += `<p><b>${datos.nombre}</b>: ${datos.texto}</p>`;
break;
case 'usuario_unio':
div.innerHTML += `<p style="color:green">→ ${datos.nombre} entró al chat</p>`;
break;
case 'usuario_salio':
div.innerHTML += `<p style="color:red">← ${datos.nombre} salió del chat</p>`;
break;
}
div.scrollTop = div.scrollHeight;
});
function enviar() {
const input = document.getElementById('texto');
if (!input.value.trim()) return;
ws.send(JSON.stringify({ tipo: 'mensaje', texto: input.value }));
input.value = '';
}
document.getElementById('texto').addEventListener('keydown', (e) => {
if (e.key === 'Enter') enviar();
});
</script>
</body>
</html>
Servidor WebSocket en Python
# pip install websockets
# servidor.py
import asyncio
import json
import websockets
from websockets.server import WebSocketServerProtocol
clientes: set[WebSocketServerProtocol] = set()
async def manejar_conexion(websocket: WebSocketServerProtocol):
clientes.add(websocket)
print(f"Cliente conectado. Total: {len(clientes)}")
try:
async for mensaje_raw in websocket:
try:
mensaje = json.loads(mensaje_raw)
await procesar_mensaje(websocket, mensaje)
except json.JSONDecodeError:
await websocket.send(json.dumps({
"tipo": "error",
"mensaje": "El mensaje debe ser JSON válido"
}))
except websockets.exceptions.ConnectionClosed:
pass
finally:
clientes.discard(websocket)
print(f"Cliente desconectado. Total: {len(clientes)}")
async def procesar_mensaje(ws, mensaje: dict):
tipo = mensaje.get("tipo")
if tipo == "ping":
await ws.send(json.dumps({"tipo": "pong"}))
elif tipo == "broadcast":
# Enviar a todos menos al remitente
texto = mensaje.get("texto", "")
await broadcast({"tipo": "mensaje", "texto": texto}, excluir=ws)
async def broadcast(datos: dict, excluir=None):
if not clientes:
return
mensaje = json.dumps(datos)
destinatarios = clientes - {excluir} if excluir else clientes
await asyncio.gather(*(
ws.send(mensaje) for ws in destinatarios
if ws.open
))
async def main():
async with websockets.serve(manejar_conexion, "localhost", 8765):
print("Servidor WebSocket en ws://localhost:8765")
await asyncio.Future() # correr indefinidamente
if __name__ == "__main__":
asyncio.run(main())
Heartbeat: detectar conexiones muertas
Las conexiones WebSocket pueden quedar "zombi": el cliente se desconectó (cerró el portátil, perdió WiFi) pero el servidor no lo sabe porque no hubo un frame de cierre limpio. El heartbeat (latido) es un ping periódico para detectar estas conexiones.
# Node.js: heartbeat con el ping/pong nativo de WebSocket
const { WebSocketServer, WebSocket } = require('ws');
const wss = new WebSocketServer({ port: 8080 });
// Marcar todos los clientes como vivos al recibirles
wss.on('connection', (ws) => {
ws.isAlive = true;
// Cuando el cliente responde al ping con un pong
ws.on('pong', () => {
ws.isAlive = true;
});
ws.on('close', () => {
ws.isAlive = false;
});
});
// Enviar ping a todos los clientes cada 30 segundos
const intervaloHeartbeat = setInterval(() => {
wss.clients.forEach((ws) => {
if (!ws.isAlive) {
console.log('Terminando conexión zombi');
return ws.terminate(); // cerrar sin esperar respuesta
}
ws.isAlive = false; // marcar como no vivo hasta recibir el pong
ws.ping(); // enviar ping (el cliente responde automáticamente con pong)
});
}, 30000);
wss.on('close', () => clearInterval(intervaloHeartbeat));
WebSocket vs Server-Sent Events (SSE)
No siempre necesitas comunicación bidireccional. Si solo necesitas que el servidor envíe datos al cliente (notificaciones, actualizaciones de estado, feeds), los Server-Sent Events son una alternativa más simple.
| Característica | WebSocket | Server-Sent Events (SSE) |
|---|---|---|
| Dirección | Bidireccional (cliente ↔ servidor) | Solo del servidor al cliente |
| Protocolo | WebSocket (ws:// / wss://) | HTTP estándar |
| Reconexión automática | No (hay que implementarla) | Sí (el navegador lo gestiona) |
| Formato de datos | Texto o binario | Solo texto (UTF-8) |
| Complejidad del servidor | Mayor | Menor (streaming HTTP) |
| Ideal para | Chat, juegos, colaboración, trading | Notificaciones, feeds, actualizaciones de estado |
# Server-Sent Events: ejemplo en Node.js
app.get('/eventos', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// Enviar un evento cada 5 segundos
const intervalo = setInterval(() => {
res.write(`data: ${JSON.stringify({ hora: new Date() })}\n\n`);
}, 5000);
// Limpiar al desconectar
req.on('close', () => clearInterval(intervalo));
});
# Cliente: API EventSource (nativa del navegador)
const fuente = new EventSource('/eventos');
fuente.addEventListener('message', (e) => {
const datos = JSON.parse(e.data);
console.log('Actualización:', datos.hora);
});
Casos de uso reales de WebSocket
- Chat y mensajería: mensajes entregados en milisegundos, indicador de "escribiendo", estado de presencia (conectado/desconectado).
- Colaboración en tiempo real: edición simultánea de documentos (Google Docs), pizarras colaborativas, comentarios en vivo.
- Notificaciones push: alertas de sistema, contadores de mensajes no leídos, actualizaciones de estado de pedidos.
- Trading y finanzas: precios de acciones, criptomonedas y divisas actualizados al tick, libros de órdenes en vivo.
- Juegos multijugador: sincronización del estado del juego entre jugadores, chat en partida.
- Monitorización: dashboards con métricas en tiempo real, logs en streaming, estado de servidores.
- Deportes y apuestas en vivo: marcadores actualizados al instante, cuotas en tiempo real.
Resumen
- WebSocket establece una conexión persistente y bidireccional entre cliente y servidor. A diferencia de HTTP, el servidor puede enviar datos al cliente en cualquier momento sin que este los pida.
- El proceso empieza con un handshake HTTP especial (con cabecera
Upgrade: websocket) que actualiza la conexión al protocolo WebSocket. A partir de ese momento se usa la misma conexión TCP para todos los mensajes. - En el navegador, la API
WebSocketes nativa. Los eventos principales sonopen,message,erroryclose. WebSocket no reconecta automáticamente: hay que implementar reconexión con backoff exponencial. - En el servidor, la librería
wsen Node.js ywebsocketsen Python son las opciones más habituales. El servidor puede hacer broadcast a todos los clientes conectados o enviar mensajes a grupos específicos. - El heartbeat (ping/pong cada 30 segundos) es esencial para detectar y limpiar conexiones zombi: clientes que se desconectaron sin enviar un frame de cierre limpio.
- Si solo necesitas que el servidor envíe datos al cliente (sin que el cliente envíe datos), considera los Server-Sent Events: son más simples, usan HTTP estándar y el navegador gestiona la reconexión automáticamente.
No hay comentarios todavía. Sé el primero en compartir tu opinión.