Capítulo 23 — WebSockets y tiempo real¶
Introducción¶
HTTP funciona con un modelo petición-respuesta: el cliente pregunta, el servidor responde, la conexión se cierra. Perfecto para la mayoría de casos, pero insuficiente para aplicaciones en tiempo real:
- Chats.
- Notificaciones instantáneas.
- Juegos multijugador.
- Edición colaborativa (tipo Google Docs).
- Paneles con datos en vivo.
Para eso existen los WebSockets: un canal bidireccional y persistente entre cliente y servidor.
HTTP vs WebSockets¶
| Aspecto | HTTP | WebSockets |
|---|---|---|
| Dirección | Cliente → Servidor | Bidireccional |
| Conexión | Se cierra tras cada respuesta | Persistente |
| Latencia | Alta (handshake cada vez) | Muy baja |
| Uso típico | Cargar páginas, APIs | Tiempo real |
Cómo funciona un WebSocket¶
- El cliente envía una petición HTTP especial con
Upgrade: websocket. - El servidor acepta (
101 Switching Protocols). - La conexión pasa a ser WebSocket: ya no es HTTP.
- Ambos lados pueden enviarse mensajes cuando quieran.
- La conexión se cierra cuando alguien la cierra o falla la red.
API de WebSocket en el navegador¶
const socket = new WebSocket('wss://api.miapp.com/ws');
socket.addEventListener('open', () => {
console.log('Conectado');
socket.send(JSON.stringify({ tipo: 'saludo', nombre: 'Urukais' }));
});
socket.addEventListener('message', (event) => {
const datos = JSON.parse(event.data);
console.log('Recibido:', datos);
});
socket.addEventListener('error', (err) => {
console.error('Error:', err);
});
socket.addEventListener('close', (event) => {
console.log('Cerrado:', event.code, event.reason);
});
wss vs ws
ws://→ WebSocket sin cifrar (solo para desarrollo).wss://→ WebSocket sobre TLS (obligatorio en producción).
Enviar y recibir¶
Enviar texto¶
socket.send('Hola servidor');
Enviar JSON¶
socket.send(JSON.stringify({
tipo: 'mensaje',
sala: 'general',
texto: 'Hola a todos',
}));
Mensajes binarios¶
socket.binaryType = 'arraybuffer';
socket.send(new Uint8Array([1, 2, 3]));
Cerrar¶
socket.close(1000, 'Cierre normal');
Reconexión automática¶
Las conexiones se caen. Un buen cliente se reconecta solo, con backoff exponencial para no saturar.
class SocketReconectable {
#url;
#socket;
#intentos = 0;
#maxIntentos = 5;
constructor(url) {
this.#url = url;
this.#conectar();
}
#conectar() {
this.#socket = new WebSocket(this.#url);
this.#socket.addEventListener('open', () => {
this.#intentos = 0;
console.log('Conectado');
});
this.#socket.addEventListener('close', () => {
if (this.#intentos < this.#maxIntentos) {
const espera = 2 ** this.#intentos * 1000;
console.log(`Reintentando en ${espera}ms`);
setTimeout(() => {
this.#intentos++;
this.#conectar();
}, espera);
}
});
}
enviar(datos) {
if (this.#socket.readyState === WebSocket.OPEN) {
this.#socket.send(JSON.stringify(datos));
}
}
}
Servidor WebSocket con Node.js¶
Usaremos la librería ws, la más popular y ligera.
Instalación¶
npm install ws
Servidor básico¶
// server.js
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (socket) => {
console.log('Cliente conectado');
socket.on('message', (data) => {
const mensaje = data.toString();
console.log('Recibido:', mensaje);
// Eco al mismo cliente
socket.send(`Eco: ${mensaje}`);
});
socket.on('close', () => {
console.log('Cliente desconectado');
});
});
Ejecuta con node server.js y prueba desde el navegador con new WebSocket('ws://localhost:8080').
Difusión a todos (broadcast)¶
wss.on('connection', (socket) => {
socket.on('message', (data) => {
// Reenviar a todos los clientes conectados
wss.clients.forEach((cliente) => {
if (cliente.readyState === cliente.OPEN) {
cliente.send(data.toString());
}
});
});
});
Salas (rooms)¶
Para chats con varios canales, agrupa clientes en salas.
const salas = new Map(); // sala -> Set de sockets
wss.on('connection', (socket) => {
socket.on('message', (raw) => {
const msg = JSON.parse(raw);
if (msg.tipo === 'unirse') {
if (!salas.has(msg.sala)) salas.set(msg.sala, new Set());
salas.get(msg.sala).add(socket);
socket.sala = msg.sala;
}
if (msg.tipo === 'mensaje') {
const miembros = salas.get(socket.sala) || new Set();
miembros.forEach((s) => {
if (s.readyState === s.OPEN) {
s.send(JSON.stringify({ tipo: 'mensaje', de: msg.de, texto: msg.texto }));
}
});
}
});
socket.on('close', () => {
if (socket.sala) salas.get(socket.sala)?.delete(socket);
});
});
Integrar WebSocket con HTTP¶
Lo habitual es HTTP para la API y WebSocket para eventos en vivo. Ambos conviven en el mismo servidor.
import http from 'node:http';
import express from 'express';
import { WebSocketServer } from 'ws';
const app = express();
app.get('/api/salud', (req, res) => res.json({ ok: true }));
const server = http.createServer(app);
const wss = new WebSocketServer({ server });
wss.on('connection', (socket) => {
socket.send(JSON.stringify({ tipo: 'bienvenida' }));
});
server.listen(3000, () => console.log('http://localhost:3000'));
El cliente se conecta a ws://localhost:3000 y la API sigue en http://localhost:3000/api/....
Socket.IO: la alternativa con esteroides¶
Socket.IO no es WebSocket puro; es una capa sobre WebSocket que añade:
- Fallback a long-polling si WebSocket falla.
- Reconexión automática.
- Salas y namespaces ya implementados.
- Emisión con acks (confirmaciones).
Instalación¶
npm install socket.io socket.io-client
Servidor¶
import { Server } from 'socket.io';
import http from 'node:http';
import express from 'express';
const app = express();
const server = http.createServer(app);
const io = new Server(server, {
cors: { origin: 'http://localhost:5173' },
});
io.on('connection', (socket) => {
console.log('Conectado:', socket.id);
socket.on('unirse', (sala) => {
socket.join(sala);
});
socket.on('mensaje', ({ sala, texto }) => {
io.to(sala).emit('mensaje', { texto, autor: socket.id });
});
socket.on('disconnect', () => {
console.log('Desconectado:', socket.id);
});
});
server.listen(3000);
Cliente¶
import { io } from 'socket.io-client';
const socket = io('http://localhost:3000');
socket.emit('unirse', 'general');
socket.on('mensaje', (msg) => {
console.log(`${msg.autor}: ${msg.texto}`);
});
socket.emit('mensaje', { sala: 'general', texto: 'Hola' });
¿WebSocket puro o Socket.IO?
- WebSocket puro: más control, protocolo estándar, ideal para APIs públicas.
- Socket.IO: más cómodo, reconexión y salas incluidas, ideal para apps propias.
Autenticación¶
WebSocket no tiene cabeceras HTTP, así que la autenticación se hace de otras formas.
Token en la URL¶
const socket = new WebSocket(`wss://api.miapp.com/ws?token=${jwt}`);
// Servidor
wss.on('connection', (socket, req) => {
const url = new URL(req.url, 'http://localhost');
const token = url.searchParams.get('token');
try {
socket.usuario = jwt.verify(token, process.env.JWT_SECRET);
} catch {
socket.close(4001, 'Token inválido');
}
});
Primer mensaje de autenticación¶
socket.onopen = () => {
socket.send(JSON.stringify({ tipo: 'auth', token: jwt }));
};
El servidor espera al primer mensaje antes de aceptar cualquier otra acción.
Nunca autentiques sin verificar
Un WebSocket abierto sin autenticación es una puerta abierta. Verifica siempre el token al conectar.
Casos de uso reales¶
Chat en tiempo real¶
- Salas por conversación.
- Mensajes con timestamp.
- Indicador de "escribiendo…".
- Historial persistido en BD.
Notificaciones¶
- Nuevo mensaje, nueva venta, nueva alerta.
- El servidor hace
emita un usuario concreto. - El cliente muestra la notificación.
Panel en vivo¶
- KPIs que se actualizan al instante.
- Actualizaciones push cuando cambia algo en el servidor.
- Mejor que hacer polling cada 5 s.
Edición colaborativa¶
- Varios usuarios editan el mismo documento.
- Los cambios se sincronizan vía WebSocket.
- Combina con CRDTs (Yjs, Automerge) para resolver conflictos.
Alternativas a WebSocket¶
| Tecnología | Dirección | Uso |
|---|---|---|
| WebSocket | Bidireccional | Uso general tiempo real |
| SSE (Server-Sent Events) | Servidor → Cliente | Notificaciones unidireccionales |
| Long polling | Cliente → Servidor (simulado) | Fallback legacy |
| WebRTC | Peer-to-peer | Audio, vídeo, datos P2P |
| MQTT | Bidireccional (broker) | IoT |
Server-Sent Events (SSE)¶
Unidireccional, más simple que WebSocket si solo necesitas que el servidor empuje datos.
// Servidor
app.get('/eventos', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
const intervalo = setInterval(() => {
res.write(`data: ${JSON.stringify({ hora: new Date() })}\n\n`);
}, 1000);
req.on('close', () => clearInterval(intervalo));
});
// Cliente
const eventos = new EventSource('/eventos');
eventos.onmessage = (e) => console.log(JSON.parse(e.data));
Buenas prácticas¶
- HTTPS/WSS siempre en producción.
- Autentica al conectar. Un WebSocket abierto es un agujero de seguridad.
- Reconecta con backoff exponencial. Evita tormentas de reconexión.
- Limita el tamaño de los mensajes. Los WebSockets no son para enviar archivos grandes.
- Heartbeat (ping/pong). Detecta conexiones muertas.
- Escala con Redis pub/sub si tienes varias instancias del servidor.
- Cierra conexiones inactivas tras un tiempo.
- Registra eventos para diagnóstico.
- Cuidado con la seguridad: valida todo lo que llega.
Ejemplo: chat mínimo completo¶
Servidor¶
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
const clientes = new Set();
wss.on('connection', (socket) => {
clientes.add(socket);
socket.on('message', (raw) => {
const msg = JSON.parse(raw);
const salida = JSON.stringify({
autor: msg.autor,
texto: msg.texto,
hora: Date.now(),
});
clientes.forEach((c) => {
if (c.readyState === c.OPEN) c.send(salida);
});
});
socket.on('close', () => clientes.delete(socket));
});
Cliente¶
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Chat</title>
</head>
<body>
<ul id="mensajes"></ul>
<form id="form">
<input id="texto" placeholder="Escribe..." autocomplete="off">
<button>Enviar</button>
</form>
<script>
const socket = new WebSocket('ws://localhost:8080');
const ul = document.getElementById('mensajes');
const form = document.getElementById('form');
const input = document.getElementById('texto');
socket.onmessage = (e) => {
const msg = JSON.parse(e.data);
const li = document.createElement('li');
li.textContent = `${msg.autor}: ${msg.texto}`;
ul.appendChild(li);
};
form.addEventListener('submit', (e) => {
e.preventDefault();
socket.send(JSON.stringify({ autor: 'Urukais', texto: input.value }));
input.value = '';
});
</script>
</body>
</html>
Ejercicios¶
- Crea un servidor WebSocket que haga eco de los mensajes.
- Añade broadcast: cada mensaje se reenvía a todos los clientes.
- Implementa salas: los mensajes solo van a los de la misma sala.
- Añade reconexión automática con backoff exponencial al cliente.
- Autentica el WebSocket con un JWT en el primer mensaje.
- Añade un indicador de "escribiendo…" en el chat.
- Persiste el historial de mensajes en PostgreSQL.
- Despliega en Railway y prueba con dos navegadores distintos.
⬅️ Capítulo 22 — Progressive Web Apps | ➡️ Capítulo 24 — Animaciones avanzadas