Saltar al contenido principal

🔴 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:

  1. HTTP básico (documento en preparación) - Comprender el protocolo HTTP
  2. ¿Qué es una API? (documento en preparación) - Concepto básico de API
  3. 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! 🔴