🔴 WebSocket vs SSE vs Long Polling
📖 Definición
La comunicación en tiempo real es una tecnología que permite el intercambio instantáneo de datos entre el servidor y el cliente. WebSocket proporciona comunicación bidireccional en tiempo real, SSE (Server-Sent Events) solo envía datos desde el servidor al cliente, y Long Polling es un método de comunicación en tiempo real que utiliza HTTP. Cada uno tiene ventajas y desventajas, por lo que debe elegirse según el escenario de uso.
🎯 Entender con analogías
Teléfono vs Radio vs Mensajería
HTTP normal = Correo postal
Tú: "¡Hola!" (envías carta)
↓ (después de varios días)
Amigo: "¡Hola!" (respuesta)
↓ (después de varios días)
Tú: "¿Cómo estás?" (otra carta)
- Lento
- Nueva conexión cada vez
- No en tiempo real
Long Polling = Teléfono (esperando en llamada)
Tú: "¡Avísame si hay algo!" (llamas y esperas)
↓ (esperando mucho...)
Amigo: "¡Tengo algo que decir ahora!" (respuesta)
↓ (se corta la llamada)
Tú: "¡Avísame otra vez si hay algo!" (vuelves a llamar)
- Basado en HTTP
- Mantener conexión → respuesta → reconectar
- Ineficiente
SSE = Transmisión de radio
Amigo: "¡Hola a todos!" (comienza transmisión)
"El clima de hoy es..." (transmisión continua)
"La próxima noticia es..." (transmisión continua)
Tú: (solo escuchas)
- Servidor → Cliente (unidireccional)
- Conexión mantenida
- Simple
WebSocket = Videollamada
Tú: "¡Hola!" (envío instantáneo)
Amigo: "¡Me alegro de verte!" (respuesta instantánea)
Tú: "¿Qué haces?" (envío instantáneo)
Amigo: "¡Programando!" (respuesta instantánea)
- Bidireccional en tiempo real
- Conexión mantenida
- Rápido y eficiente
Pedido en restaurante
HTTP normal = Autoservicio
1. Vas al mostrador y pides comida
2. Esperas
3. Recibes comida y vuelves a tu mesa
4. Vuelves al mostrador y pides bebida
5. Esperas otra vez
Long Polling = Presionas timbre y esperas
1. Presionas timbre y esperas al empleado (mantener conexión)
2. Empleado viene → "Haga su pedido"
3. Pides y presionas timbre otra vez
4. Esperas otra vez...
SSE = Pantalla de cocina
Cocina: "¡Cliente 1, su comida está lista!"
Cocina: "¡Cliente 2, se está preparando!"
Cocina: "¡Cliente 3, saldrá pronto!"
Tú: (solo escuchas)
WebSocket = Servicio de mesa
Tú: "Agua, por favor"
Empleado: "Sí, se la traeré"
Tú: "Kimchi también"
Empleado: "Se lo traeré enseguida"
Empleado: "Aquí está su comida"
Tú: "Gracias"
- Conversación libre
- Respuesta rápida
⚙️ Principio de funcionamiento
1. HTTP normal vs Comunicación en tiempo real
========== HTTP normal (Solicitud-Respuesta) ==========
Cliente Servidor
│ │
│ 1. Conexión (Request) │
│────────────────────────>│
│ │
│ │ Procesando...
│ │
│ 2. Respuesta (Response) │
│<────────────────────────│
│ │
│ Conexión cerrada │
╳ ╳
│ 3. Reconectar │
│────────────────────────>│
│ │
Características:
- Solo responde cuando el cliente solicita
- Nueva conexión cada vez
- El servidor no puede enviar primero
- No en tiempo real
========== Comunicación en tiempo real ==========
Cliente Servidor
│ │
│ Conexión │
│<───────────────────────>│
│ │
│ Comunicación bidireccional mantenida │
│<───────────────────────>│
│ │
│ Intercambio de datos │
│<───────────────────────>│
│ │
Características:
- Conexión mantenida
- El servidor puede enviar primero
- En tiempo real posible
2. Long Polling
Cliente Servidor
│ │
│ 1. Solicitud (¿Hay nuevos datos?) │
│──────────────────────────────────>│
│ │
│ Conexión mantenida (esperando...)│
│ │
│ │ No hay datos...
│ │ Sigue esperando...
│ │
│ │ ¡Nuevos datos!
│ │
│ 2. Respuesta (Aquí hay datos) │
│<──────────────────────────────────│
│ │
│ Conexión cerrada │
╳ ╳
│ │
│ 3. Reconectar inmediatamente │
│──────────────────────────────────>│
│ │
│ Esperando otra vez... │
Proceso:
1. Cliente → Servidor: "¿Hay nuevos datos?"
2. Servidor: Espera hasta que haya datos
3. Datos disponibles → Respuesta
4. Conexión cerrada
5. Reconectar inmediatamente (repetir)
Ventajas:
✅ Basado en HTTP (usar infraestructura existente)
✅ Sin problemas de firewall
✅ Implementación simple
Desventajas:
❌ Ineficiente (reconexión continua)
❌ Gran carga en el servidor
❌ Sobrecarga de encabezados
3. Server-Sent Events (SSE)
Cliente Servidor
│ │
│ 1. Solicitud de conexión │
│──────────────────────────────────>│
│ │
│ 2. Mantener conexión (inicio de stream) │
│<══════════════════════════════════│
│ │
│ 3. Push de datos │
│<──────────────────────────────────│
│ │
│ 4. Otro push de datos │
│<──────────────────────────────────│
│ │
│ Conexión mantenida... │
│<══════════════════════════════════│
Características:
- Servidor → Cliente (unidireccional)
- Conexión mantenida
- Basado en HTTP
- Reconexión automática
Ventajas:
✅ Simple (integrado en navegador)
✅ Reconexión automática
✅ Puede reanudar con ID de evento
✅ Eficiente en HTTP/2
Desventajas:
❌ Unidireccional (Servidor → Cliente)
❌ No admite datos binarios
❌ No compatible con IE
4. WebSocket
Cliente Servidor
│ │
│ 1. Solicitud HTTP Upgrade │
│──────────────────────────────────>│
│ │
│ 2. Aprobación de Upgrade │
│<──────────────────────────────────│
│ │
│ Cambio a protocolo WebSocket │
│<══════════════════════════════════>│
│ │
│ 3. Envío de datos │
│──────────────────────────────────>│
│ │
│ 4. Recepción de datos │
│<──────────────────────────────────│
│ │
│ 5. Envío de datos │
│──────────────────────────────────>│
│ │
│ Comunicación bidireccional continua... │
│<══════════════════════════════════>│
Características:
- Comunicación bidireccional en tiempo real
- Protocolo separado (ws://, wss://)
- Conexión mantenida
- Baja latencia
Ventajas:
✅ Tiempo real verdadero
✅ Comunicación bidireccional
✅ Baja sobrecarga
✅ Soporte binario
Desventajas:
❌ Complejo
❌ Posibles problemas con proxy/firewall
❌ Carga del servidor (mantener conexión)
💡 Ejemplos prácticos
Ejemplo de Long Polling
// ========== Servidor (Express.js) ==========
const express = require('express');
const app = express();
let messages = [];
let waitingClients = [];
// Agregar mensaje (llamado desde otro lugar)
function addMessage(message) {
messages.push(message);
// Responder inmediatamente a clientes en espera
waitingClients.forEach(client => {
client.json({ messages });
});
waitingClients = [];
}
// Endpoint de Long Polling
app.get('/messages', (req, res) => {
const lastId = parseInt(req.query.lastId) || 0;
// Responder inmediatamente si hay nuevos mensajes
if (messages.length > lastId) {
return res.json({ messages: messages.slice(lastId) });
}
// Si no, agregar a lista de espera (máximo 30 segundos)
waitingClients.push(res);
// Timeout de 30 segundos
req.setTimeout(30000, () => {
const index = waitingClients.indexOf(res);
if (index > -1) {
waitingClients.splice(index, 1);
res.json({ messages: [] }); // Respuesta vacía
}
});
});
app.listen(3000);
// ========== Cliente ==========
let lastMessageId = 0;
async function longPolling() {
while (true) {
try {
const response = await fetch(`/messages?lastId=${lastMessageId}`);
const data = await response.json();
// Procesar nuevos mensajes
if (data.messages.length > 0) {
data.messages.forEach(msg => {
console.log('Nuevo mensaje:', msg);
displayMessage(msg);
});
lastMessageId += data.messages.length;
}
// Reconectar inmediatamente
await longPolling();
} catch (error) {
console.error('Error:', error);
// Reintentar después de 3 segundos
await new Promise(resolve => setTimeout(resolve, 3000));
}
}
}
// Iniciar
longPolling();
// ========== Problemas ==========
/*
1. Reconexión continua (ineficiente)
2. Alto uso de red
3. Gran carga del servidor
4. Consumo de batería (móvil)
*/
Ejemplo de Server-Sent Events (SSE)
// ========== Servidor (Express.js) ==========
const express = require('express');
const app = express();
app.use(express.static('public'));
// Endpoint SSE
app.get('/events', (req, res) => {
// Configurar encabezados SSE
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// CORS (si es necesario)
res.setHeader('Access-Control-Allow-Origin', '*');
// Mensaje de confirmación de conexión
res.write('data: Connected\n\n');
// ID del cliente
const clientId = Date.now();
console.log(`Cliente ${clientId} conectado`);
// Enviar hora cada 5 segundos
const intervalId = setInterval(() => {
const data = {
time: new Date().toLocaleTimeString(),
message: '¡Hola!'
};
// Enviar en formato SSE
res.write(`data: ${JSON.stringify(data)}\n\n`);
}, 5000);
// Manejar desconexión del cliente
req.on('close', () => {
console.log(`Cliente ${clientId} desconectado`);
clearInterval(intervalId);
res.end();
});
});
// API para publicar eventos
const clients = [];
app.get('/events/stream', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// Guardar cliente
clients.push(res);
req.on('close', () => {
const index = clients.indexOf(res);
clients.splice(index, 1);
});
});
// Enviar mensaje a todos los clientes
app.post('/broadcast', express.json(), (req, res) => {
const { message } = req.body;
clients.forEach(client => {
client.write(`data: ${JSON.stringify({ message })}\n\n`);
});
res.json({ success: true });
});
app.listen(3000);
// ========== Cliente (HTML) ==========
/*
<!DOCTYPE html>
<html>
<body>
<div id="messages"></div>
<script>
// Conexión SSE
const eventSource = new EventSource('/events');
// Recibir mensajes
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Datos recibidos:', data);
const div = document.getElementById('messages');
div.innerHTML += `<p>${data.time}: ${data.message}</p>`;
};
// Conexión abierta
eventSource.onopen = () => {
console.log('SSE conectado');
};
// Manejo de errores
eventSource.onerror = (error) => {
console.error('Error SSE:', error);
if (eventSource.readyState === EventSource.CLOSED) {
console.log('Conexión SSE cerrada');
}
};
// Cerrar conexión (al salir de la página)
window.addEventListener('beforeunload', () => {
eventSource.close();
});
</script>
</body>
</html>
*/
// ========== Funciones avanzadas ==========
// 1. Especificar tipo de evento
app.get('/events/typed', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
// Varios tipos de eventos
setInterval(() => {
// Mensaje general
res.write(`event: message\ndata: Hello\n\n`);
// Notificación
res.write(`event: notification\ndata: New notification!\n\n`);
// Actualización
res.write(`event: update\ndata: {"count": 10}\n\n`);
}, 5000);
});
// Manejo por tipo de evento en cliente
/*
eventSource.addEventListener('message', (e) => {
console.log('Mensaje:', e.data);
});
eventSource.addEventListener('notification', (e) => {
console.log('Notificación:', e.data);
});
eventSource.addEventListener('update', (e) => {
const data = JSON.parse(e.data);
console.log('Actualización:', data);
});
*/
// 2. ID de evento (reanudar al reconectar)
let eventId = 0;
app.get('/events/resumable', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
const lastEventId = parseInt(req.headers['last-event-id']) || 0;
console.log('Último ID de evento:', lastEventId);
// Enviar solo eventos después de lastEventId
setInterval(() => {
eventId++;
res.write(`id: ${eventId}\ndata: Event ${eventId}\n\n`);
}, 1000);
});
// 3. Configurar tiempo de reintento
app.get('/events/retry', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
// Reconectar después de 5 segundos
res.write('retry: 5000\n');
res.write('data: Connected\n\n');
});
Ejemplo de WebSocket (Socket.io)
// ========== Servidor (Socket.io) ==========
const express = require('express');
const http = require('http');
const socketIo = require('socket.io');
const app = express();
const server = http.createServer(app);
const io = socketIo(server, {
cors: {
origin: '*'
}
});
app.use(express.static('public'));
// Gestión de usuarios conectados
const users = new Map();
// Conexión WebSocket
io.on('connection', (socket) => {
console.log('Nuevo usuario conectado:', socket.id);
// Guardar información del usuario
socket.on('register', (username) => {
users.set(socket.id, { username, socket });
console.log(`${username} registrado`);
// Notificar a todos los usuarios
io.emit('user-joined', {
username,
totalUsers: users.size
});
});
// Recibir mensaje
socket.on('chat-message', (message) => {
const user = users.get(socket.id);
console.log(`${user.username}: ${message}`);
// Transmitir a todos los usuarios
io.emit('chat-message', {
username: user.username,
message,
timestamp: new Date().toISOString()
});
});
// Indicador de escritura
socket.on('typing', () => {
const user = users.get(socket.id);
socket.broadcast.emit('user-typing', user.username);
});
socket.on('stop-typing', () => {
const user = users.get(socket.id);
socket.broadcast.emit('user-stop-typing', user.username);
});
// Mensaje privado
socket.on('private-message', ({ to, message }) => {
const targetSocket = Array.from(users.values())
.find(u => u.username === to)?.socket;
if (targetSocket) {
const sender = users.get(socket.id);
targetSocket.emit('private-message', {
from: sender.username,
message
});
}
});
// Desconexión
socket.on('disconnect', () => {
const user = users.get(socket.id);
if (user) {
console.log(`${user.username} desconectado`);
users.delete(socket.id);
// Notificar a todos los usuarios
io.emit('user-left', {
username: user.username,
totalUsers: users.size
});
}
});
});
server.listen(3000, () => {
console.log('Servidor ejecutándose: http://localhost:3000');
});
// ========== Cliente (HTML + Socket.io) ==========
/*
<!DOCTYPE html>
<html>
<head>
<script src="/socket.io/socket.io.js"></script>
</head>
<body>
<div id="chat">
<div id="messages"></div>
<input id="username" placeholder="Nombre" />
<input id="message" placeholder="Mensaje" />
<button onclick="sendMessage()">Enviar</button>
</div>
<script>
// Conexión Socket.io
const socket = io('http://localhost:3000');
// Conexión exitosa
socket.on('connect', () => {
console.log('Conectado:', socket.id);
});
// Registrar usuario
function register() {
const username = document.getElementById('username').value;
socket.emit('register', username);
}
// Enviar mensaje
function sendMessage() {
const message = document.getElementById('message').value;
socket.emit('chat-message', message);
document.getElementById('message').value = '';
}
// Recibir mensaje
socket.on('chat-message', (data) => {
const div = document.getElementById('messages');
div.innerHTML += `
<p><strong>${data.username}:</strong> ${data.message}</p>
`;
});
// Usuario se une
socket.on('user-joined', (data) => {
console.log(`${data.username} entró (total ${data.totalUsers})`);
});
// Usuario sale
socket.on('user-left', (data) => {
console.log(`${data.username} salió (total ${data.totalUsers})`);
});
// Escribiendo
let typingTimeout;
document.getElementById('message').addEventListener('input', () => {
socket.emit('typing');
clearTimeout(typingTimeout);
typingTimeout = setTimeout(() => {
socket.emit('stop-typing');
}, 1000);
});
socket.on('user-typing', (username) => {
console.log(`${username} está escribiendo...`);
});
// Desconexión
socket.on('disconnect', () => {
console.log('Desconectado');
});
// Reconexión
socket.on('reconnect', () => {
console.log('Reconectado');
});
</script>
</body>
</html>
*/
API WebSocket nativa
// ========== Servidor (librería ws) ==========
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws) => {
console.log('Cliente conectado');
// Recibir mensaje
ws.on('message', (message) => {
console.log('Mensaje recibido:', message.toString());
// Transmitir a todos los clientes
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(message.toString());
}
});
});
// Desconexión
ws.on('close', () => {
console.log('Cliente desconectado');
});
// Error
ws.on('error', (error) => {
console.error('Error WebSocket:', error);
});
// Mensaje de bienvenida
ws.send('¡Conectado al servidor!');
});
console.log('Servidor WebSocket ejecutándose: ws://localhost:8080');
// ========== Cliente (navegador) ==========
// Conexión WebSocket
const ws = new WebSocket('ws://localhost:8080');
// Conexión abierta
ws.addEventListener('open', (event) => {
console.log('WebSocket conectado');
ws.send('¡Hola!');
});
// Recibir mensaje
ws.addEventListener('message', (event) => {
console.log('Del servidor:', event.data);
});
// Error
ws.addEventListener('error', (error) => {
console.error('Error WebSocket:', error);
});
// Desconexión
ws.addEventListener('close', (event) => {
console.log('WebSocket desconectado');
if (event.code === 1000) {
console.log('Cierre normal');
} else {
console.log('Cierre anormal:', event.code);
}
});
// Enviar mensaje
function sendMessage(message) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(message);
} else {
console.error('WebSocket no está abierto');
}
}
// Cerrar conexión
function closeConnection() {
ws.close(1000, 'Cierre normal');
}
// ========== Transmisión de datos binarios ==========
// Envío de archivo
async function sendFile(file) {
const arrayBuffer = await file.arrayBuffer();
ws.send(arrayBuffer);
}
// Recepción binaria en servidor
ws.addEventListener('message', (event) => {
if (event.data instanceof ArrayBuffer) {
console.log('Datos binarios recibidos:', event.data.byteLength, 'bytes');
} else {
console.log('Datos de texto:', event.data);
}
});
// ========== Ping/Pong (mantener conexión) ==========
// Servidor
wss.on('connection', (ws) => {
ws.isAlive = true;
ws.on('pong', () => {
ws.isAlive = true;
});
});
// Ping cada 30 segundos
setInterval(() => {
wss.clients.forEach((ws) => {
if (ws.isAlive === false) {
return ws.terminate(); // Terminar conexión si no hay respuesta
}
ws.isAlive = false;
ws.ping();
});
}, 30000);
// ========== Reconexión automática ==========
let ws;
let reconnectAttempts = 0;
const maxReconnectAttempts = 10;
function connect() {
ws = new WebSocket('ws://localhost:8080');
ws.addEventListener('open', () => {
console.log('Conectado');
reconnectAttempts = 0;
});
ws.addEventListener('close', (event) => {
console.log('Desconectado');
// Reconexión automática
if (reconnectAttempts < maxReconnectAttempts) {
reconnectAttempts++;
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
console.log(`Intentando reconectar en ${delay}ms...`);
setTimeout(connect, delay);
} else {
console.error('Fallo en reconexión');
}
});
ws.addEventListener('error', (error) => {
console.error('Error:', error);
ws.close();
});
}
connect();
🤔 Preguntas frecuentes
P1. ¿Qué método debo elegir?
R:
✅ Elegir Long Polling cuando:
├─ Entorno sin soporte de WebSocket/SSE
├─ Sistema legacy
├─ Notificaciones simples
└─ Ej: Soporte de navegadores antiguos, polling simple
Ejemplos de uso:
- Cotizaciones bursátiles (actualización poco frecuente)
- Verificación de correo electrónico
- Notificaciones simples
✅ Elegir SSE cuando:
├─ Unidireccional Servidor → Cliente
├─ Actualizaciones en tiempo real
├─ Desea implementación simple
├─ Necesita reconexión automática
└─ Ej: Feed de noticias, notificaciones, panel de control
Ejemplos de uso:
- Feed de noticias (actualización en tiempo real)
- Gráficos de acciones (push desde servidor)
- Indicador de progreso
- Streaming de logs
- Monitoreo de servidor
✅ Elegir WebSocket cuando:
├─ Comunicación bidireccional en tiempo real
├─ Baja latencia importante
├─ Intercambio frecuente de mensajes
├─ Datos binarios
└─ Ej: Chat, juegos, herramientas colaborativas
Ejemplos de uso:
- Aplicación de chat
- Juegos multijugador
- Edición colaborativa de documentos (Google Docs)
- Videoconferencia
- Control en tiempo real de IoT
📊 Tabla comparativa:
Característica | Long Polling | SSE | WebSocket
--------------|-------------|----------|----------
Dirección | Bidireccional | Unidireccional | Bidireccional
Protocolo | HTTP | HTTP | WebSocket
Latencia | Alta | Baja | Muy baja
Sobrecarga | Alta | Media | Baja
Navegador | Todos | Mayoría | Todos
Complejidad | Baja | Baja | Alta
Reconexión | Manual | Automática | Manual
Binario | Posible | No | Sí
Recomendación:
- Tiempo real simple: SSE
- Chat/juegos: WebSocket
- Soporte legacy: Long Polling
P2. ¿Diferencia de rendimiento?
R:
// ========== Ejemplo de benchmark ==========
// 1. Long Polling
// Por solicitud:
// - Encabezado HTTP: ~800 bytes
// - Tiempo de reconexión: ~50ms
// - Recursos del servidor: 1 hilo por conexión
// 100 usuarios simultáneos:
// - Solicitudes por segundo: 100
// - Transferencia de datos: 80KB/s (solo encabezados)
// - Carga del servidor: Alta
// 2. SSE
// Por conexión:
// - Encabezado HTTP: Solo 1 vez inicial (~800 bytes)
// - Sobrecarga de mensaje: ~10 bytes
// - Recursos del servidor: Mantener conexión (Keep-Alive)
// 100 usuarios simultáneos:
// - Conexión inicial: 80KB
// - Por mensaje: 1KB (sin encabezados)
// - Carga del servidor: Media
// 3. WebSocket
// Por conexión:
// - Encabezado Upgrade: Solo 1 vez (~500 bytes)
// - Sobrecarga de frame: 2~6 bytes
// - Recursos del servidor: Mantener conexión
// 100 usuarios simultáneos:
// - Conexión inicial: 50KB
// - Por mensaje: ~0.002KB (solo frame)
// - Carga del servidor: Baja
// ========== Medición real ==========
// Prueba: 100 usuarios enviando 1 mensaje por segundo
// Long Polling
const longPollingBenchmark = {
solicitudesPorSegundo: 100,
latenciaPromedio: '200ms',
anchoDeBanda: '8MB/min',
cpuServidor: '70%',
memoria: '500MB'
};
// SSE
const sseBenchmark = {
solicitudesPorSegundo: 0, // Conexión mantenida
latenciaPromedio: '10ms',
anchoDeBanda: '600KB/min',
cpuServidor: '30%',
memoria: '200MB'
};
// WebSocket
const webSocketBenchmark = {
solicitudesPorSegundo: 0, // Conexión mantenida
latenciaPromedio: '2ms',
anchoDeBanda: '100KB/min',
cpuServidor: '15%',
memoria: '100MB'
};
// ========== Optimización práctica ==========
// 1. Limitar número de conexiones
const MAX_CONNECTIONS = 1000;
io.on('connection', (socket) => {
if (io.engine.clientsCount > MAX_CONNECTIONS) {
socket.disconnect();
return;
}
});
// 2. Compresión de mensajes
const io = socketIo(server, {
perMessageDeflate: {
threshold: 1024 // Comprimir solo si > 1KB
}
});
// 3. Procesamiento por lotes
const messageQueue = [];
setInterval(() => {
if (messageQueue.length > 0) {
io.emit('batch', messageQueue);
messageQueue.length = 0;
}
}, 100); // Envío por lotes cada 100ms
// 4. Uso de salas (Room)
socket.join('room1');
io.to('room1').emit('message', data); // Solo room1
// 5. Transmisión binaria
// WebSocket es eficiente con binarios
const buffer = Buffer.from('Hello');
socket.send(buffer); // Más rápido que texto
// 6. Optimización de Heartbeat
const HEARTBEAT_INTERVAL = 25000; // 25 segundos
const HEARTBEAT_TIMEOUT = 30000; // 30 segundos
setInterval(() => {
wss.clients.forEach((ws) => {
if (ws.isAlive === false) {
return ws.terminate();
}
ws.isAlive = false;
ws.ping();
});
}, HEARTBEAT_INTERVAL);
P3. ¿Y en aplicaciones móviles?
R:
// ========== Consideraciones móviles ==========
// 1. Consumo de batería
// Long Polling: ❌ Alto (reconexión continua)
// SSE: ⚠️ Medio
// WebSocket: ✅ Bajo (conexión mantenida)
// 2. Cambio de red
// Cambio WiFi <-> Datos móviles
// Reconexión automática
let ws;
function connect() {
ws = new WebSocket('wss://api.example.com');
ws.onclose = () => {
// Backoff exponencial
const delay = Math.min(1000 * Math.pow(2, attempts), 30000);
setTimeout(connect, delay);
};
}
// Detectar estado de red
window.addEventListener('online', () => {
console.log('En línea - reconectando');
connect();
});
window.addEventListener('offline', () => {
console.log('Fuera de línea');
ws.close();
});
// 3. Procesamiento en segundo plano
// ¿Qué pasa cuando la app pasa a segundo plano?
// Ejemplo React Native
import { AppState } from 'react-native';
AppState.addEventListener('change', (nextAppState) => {
if (nextAppState === 'background') {
// Segundo plano → Mantener conexión o cerrar
ws.close();
} else if (nextAppState === 'active') {
// Primer plano → Reconectar
connect();
}
});
// 4. Modo ahorro de datos
// Según configuración del usuario
const isDataSaverMode = await getDataSaverSetting();
if (isDataSaverMode) {
// Long Polling (menos frecuente)
setInterval(poll, 60000); // Cada 1 minuto
} else {
// WebSocket (tiempo real)
connect();
}
// 5. Integración con notificaciones push
// Combinación de WebSocket + FCM/APNs
// App en ejecución: WebSocket
if (appIsActive) {
useWebSocket();
}
// Segundo plano: Notificación push
if (appIsBackground) {
usePushNotification();
}
// ========== Ejemplo React Native ==========
import React, { useEffect, useState } from 'react';
import { View, Text } from 'react-native';
function ChatScreen() {
const [ws, setWs] = useState(null);
const [messages, setMessages] = useState([]);
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
const websocket = new WebSocket('wss://api.example.com');
websocket.onopen = () => {
console.log('Connected');
setIsConnected(true);
};
websocket.onmessage = (event) => {
const message = JSON.parse(event.data);
setMessages(prev => [...prev, message]);
};
websocket.onerror = (error) => {
console.error('WebSocket error:', error);
};
websocket.onclose = () => {
console.log('Disconnected');
setIsConnected(false);
// Reconectar después de 3 segundos
setTimeout(() => {
// Lógica de reconexión
}, 3000);
};
setWs(websocket);
// Limpieza
return () => {
websocket.close();
};
}, []);
const sendMessage = (text) => {
if (ws && isConnected) {
ws.send(JSON.stringify({ text }));
}
};
return (
<View>
<Text>Connected: {isConnected ? 'Yes' : 'No'}</Text>
{messages.map((msg, i) => (
<Text key={i}>{msg.text}</Text>
))}
</View>
);
}
P4. ¿Cómo asegurar?
R:
// ========== 1. Usar HTTPS/WSS ==========
// ❌ HTTP/WS (transmisión en texto plano)
const ws = new WebSocket('ws://api.example.com');
// ✅ HTTPS/WSS (encriptado)
const ws = new WebSocket('wss://api.example.com');
// ========== 2. Autenticación ==========
// Método 1: JWT en URL (simple pero no recomendado)
const token = getJWTToken();
const ws = new WebSocket(`wss://api.example.com?token=${token}`);
// ⚠️ Riesgo de exposición de token en URL
// Método 2: Autenticación después de conectar (recomendado)
const ws = new WebSocket('wss://api.example.com');
ws.onopen = () => {
// Enviar mensaje de autenticación
ws.send(JSON.stringify({
type: 'auth',
token: getJWTToken()
}));
};
// Servidor
io.use((socket, next) => {
const token = socket.handshake.auth.token;
try {
const decoded = jwt.verify(token, SECRET_KEY);
socket.userId = decoded.userId;
next();
} catch (error) {
next(new Error('Autenticación fallida'));
}
});
// Método 3: Cookie (HttpOnly)
// Cliente
const ws = new WebSocket('wss://api.example.com');
// La cookie se envía automáticamente
// Servidor
io.use((socket, next) => {
const cookies = parseCookies(socket.request.headers.cookie);
const sessionId = cookies.sessionId;
if (isValidSession(sessionId)) {
next();
} else {
next(new Error('Autenticación fallida'));
}
});
// ========== 3. Configuración CORS ==========
const io = socketIo(server, {
cors: {
origin: 'https://myapp.com', // Solo dominio específico
credentials: true
}
});
// Múltiples dominios
const allowedOrigins = ['https://myapp.com', 'https://admin.myapp.com'];
const io = socketIo(server, {
cors: {
origin: (origin, callback) => {
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('CORS rechazado'));
}
},
credentials: true
}
});
// ========== 4. Rate Limiting ==========
const rateLimitMap = new Map();
io.on('connection', (socket) => {
socket.on('message', (data) => {
const userId = socket.userId;
const now = Date.now();
// Rastrear número de mensajes por usuario
if (!rateLimitMap.has(userId)) {
rateLimitMap.set(userId, []);
}
const userMessages = rateLimitMap.get(userId);
// Filtrar mensajes del último minuto
const recentMessages = userMessages.filter(
timestamp => now - timestamp < 60000
);
// Límite de 10 por minuto
if (recentMessages.length >= 10) {
socket.emit('error', 'Límite de envío de mensajes excedido');
return;
}
recentMessages.push(now);
rateLimitMap.set(userId, recentMessages);
// Procesar mensaje
handleMessage(data);
});
});
// ========== 5. Validación de entrada ==========
socket.on('message', (data) => {
// Validación de tipo
if (typeof data !== 'object') {
return socket.emit('error', 'Formato de datos incorrecto');
}
// Validación de campos obligatorios
if (!data.type || !data.content) {
return socket.emit('error', 'Campos obligatorios faltantes');
}
// Validación de longitud
if (data.content.length > 1000) {
return socket.emit('error', 'Mensaje demasiado largo');
}
// Prevención XSS
const sanitizedContent = sanitizeHtml(data.content);
// Procesar
broadcastMessage(sanitizedContent);
});
// ========== 6. Namespace y Room ==========
// Aislar con namespace
const chatNamespace = io.of('/chat');
const adminNamespace = io.of('/admin');
chatNamespace.on('connection', (socket) => {
// Chat general
});
adminNamespace.use(authenticateAdmin);
adminNamespace.on('connection', (socket) => {
// Solo administradores
});
// Control de permisos con Room
socket.on('join-room', (roomId) => {
if (canAccessRoom(socket.userId, roomId)) {
socket.join(roomId);
} else {
socket.emit('error', 'Sin permiso de acceso a la sala');
}
});
// ========== 7. Prevención DDoS ==========
// Limitar número de conexiones
const MAX_CONNECTIONS_PER_IP = 5;
const connectionsByIP = new Map();
io.on('connection', (socket) => {
const ip = socket.handshake.address;
const count = connectionsByIP.get(ip) || 0;
if (count >= MAX_CONNECTIONS_PER_IP) {
socket.disconnect();
return;
}
connectionsByIP.set(ip, count + 1);
socket.on('disconnect', () => {
const newCount = connectionsByIP.get(ip) - 1;
if (newCount <= 0) {
connectionsByIP.delete(ip);
} else {
connectionsByIP.set(ip, newCount);
}
});
});
P5. ¿Manejo de errores?
R:
// ========== Manejo de errores del cliente ==========
class WebSocketClient {
constructor(url) {
this.url = url;
this.ws = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 10;
this.reconnectDelay = 1000;
}
connect() {
try {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('Conexión exitosa');
this.reconnectAttempts = 0;
this.onConnected();
};
this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.onMessage(data);
} catch (error) {
console.error('Error al parsear mensaje:', error);
}
};
this.ws.onerror = (error) => {
console.error('Error WebSocket:', error);
this.onError(error);
};
this.ws.onclose = (event) => {
console.log('Desconectado:', event.code, event.reason);
// Cierre normal (1000)
if (event.code === 1000) {
console.log('Cierre normal');
return;
}
// Intentar reconexión
this.reconnect();
};
} catch (error) {
console.error('Fallo de conexión:', error);
this.reconnect();
}
}
reconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('Abandono de reconexión');
this.onReconnectFailed();
return;
}
this.reconnectAttempts++;
const delay = Math.min(
this.reconnectDelay * Math.pow(2, this.reconnectAttempts),
30000
);
console.log(`Intento de reconexión en ${delay}ms (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
setTimeout(() => {
this.connect();
}, delay);
}
send(data) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
try {
this.ws.send(JSON.stringify(data));
} catch (error) {
console.error('Error de envío:', error);
}
} else {
console.error('WebSocket no está abierto');
// Guardar en cola de mensajes
this.queueMessage(data);
}
}
close() {
if (this.ws) {
this.ws.close(1000, 'Cierre del cliente');
}
}
// Manejadores de eventos (sobrescribir)
onConnected() {}
onMessage(data) {}
onError(error) {}
onReconnectFailed() {}
queueMessage(data) {
// Implementar cola de mensajes offline
}
}
// Uso
const client = new WebSocketClient('wss://api.example.com');
client.onConnected = () => {
console.log('¡Conectado!');
};
client.onMessage = (data) => {
console.log('Mensaje:', data);
};
client.onError = (error) => {
console.error('Error ocurrido:', error);
// Mostrar error en UI
showErrorNotification('Error de conexión');
};
client.onReconnectFailed = () => {
// Manejar fallo de reconexión
showErrorModal('No se puede conectar al servidor');
};
client.connect();
// ========== Manejo de errores del servidor ==========
io.on('connection', (socket) => {
// Manejador de errores
socket.on('error', (error) => {
console.error('Error de socket:', error);
});
// Error de procesamiento de mensaje
socket.on('message', async (data) => {
try {
// Validación de entrada
if (!isValidMessage(data)) {
throw new Error('Mensaje inválido');
}
// Procesar
await processMessage(data);
} catch (error) {
console.error('Error al procesar mensaje:', error);
// Enviar error al cliente
socket.emit('error', {
code: 'MESSAGE_PROCESSING_ERROR',
message: error.message
});
}
});
// Manejar desconexión
socket.on('disconnect', (reason) => {
console.log('Desconectado:', reason);
// Limpiar usuario
cleanupUser(socket.userId);
// Notificar a otros usuarios
socket.broadcast.emit('user-left', socket.userId);
});
});
// Manejador de errores global
io.engine.on('connection_error', (error) => {
console.error('Error de conexión:', error);
});
// ========== Manejo de timeout ==========
socket.on('message', (data, callback) => {
// Configurar timeout (5 segundos)
const timeout = setTimeout(() => {
callback({
error: 'TIMEOUT',
message: 'Tiempo de respuesta excedido'
});
}, 5000);
// Procesar
processMessage(data)
.then(result => {
clearTimeout(timeout);
callback({ success: true, data: result });
})
.catch(error => {
clearTimeout(timeout);
callback({ error: error.message });
});
});
🎓 Siguientes pasos
Una vez que comprenda la comunicación en tiempo real, puede aprender lo siguiente:
- HTTP básico (documento en preparación) - Comprender el protocolo HTTP
- ¿Qué es una API? (documento en preparación) - Concepto básico de API
- REST API vs GraphQL - Diseño de API
Práctica
# ========== 1. App de chat con Socket.io ==========
mkdir chat-app
cd chat-app
npm init -y
npm install express socket.io
# Después de escribir server.js
node server.js
# ========== 2. Notificaciones en tiempo real con SSE ==========
mkdir sse-demo
cd sse-demo
npm install express
# Escribir server.js
# Escribir index.html
node server.js
# Acceder a http://localhost:3000
# ========== 3. Juego con WebSocket ==========
mkdir websocket-game
cd websocket-game
npm install ws express
# Implementar juego multijugador
node server.js
🎬 Conclusión
La comunicación en tiempo real es una tecnología central en la web moderna:
- Long Polling: Soporte legacy, basado en HTTP
- SSE: Push del servidor, implementación simple
- WebSocket: Bidireccional en tiempo real, máximo rendimiento
- Criterios de selección: Requisitos, entorno, complejidad
¡Elija el método de comunicación en tiempo real adecuado para su proyecto y cree una excelente experiencia de usuario! 🔴