Intermedio
25 min
Lección 5 de 10
API
WebSockets y APIs en Tiempo Real
Construye aplicaciones en tiempo real con WebSockets, Server-Sent Events y Socket.io
¿Qué son los WebSockets?
WebSockets proporcionan comunicación bidireccional full-duplex entre cliente y servidor a través de una conexión única y persistente. A diferencia de HTTP donde el cliente siempre inicia las solicitudes, WebSockets permiten que tanto el cliente como el servidor envíen mensajes en cualquier momento.
Esto hace que WebSockets sea perfecto para aplicaciones en tiempo real como aplicaciones de chat, notificaciones en vivo, juegos, edición colaborativa y feeds de datos en vivo.
🔄 HTTP vs WebSocket
HTTP (Solicitud/Respuesta)
- • El cliente inicia cada solicitud
- • La conexión se cierra después de la respuesta
- • El servidor no puede enviar datos
- • Se necesita polling para actualizaciones
WebSocket (Bidireccional)
- • Ambos pueden enviar en cualquier momento
- • Conexión persistente
- • El servidor envía instantáneamente
- • Actualizaciones verdaderamente en tiempo real
Conexión Básica de WebSocket
// Creando una conexión WebSocket
const socket = new WebSocket('wss://api.example.com/ws');
// Conexión abierta
socket.onopen = function(event) {
console.log('Conectado al servidor WebSocket');
// Enviar un mensaje
socket.send(JSON.stringify({
type: 'subscribe',
channel: 'notifications'
}));
};
// Recibiendo mensajes
socket.onmessage = function(event) {
const data = JSON.parse(event.data);
console.log('Recibido:', data);
// Manejar diferentes tipos de mensajes
switch (data.type) {
case 'notification':
showNotification(data.payload);
break;
case 'chat_message':
addMessage(data.payload);
break;
case 'user_status':
updateUserStatus(data.payload);
break;
}
};
// Conexión cerrada
socket.onclose = function(event) {
if (event.wasClean) {
console.log(`Conexión cerrada limpiamente, código=${event.code}`);
} else {
console.log('Conexión perdida');
}
};
// Manejo de errores
socket.onerror = function(error) {
console.error('Error de WebSocket:', error);
};
// Enviando mensajes
function sendMessage(type, payload) {
if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({ type, payload }));
} else {
console.warn('WebSocket no conectado');
}
}
// Cerrar conexión
socket.close(1000, 'Usuario cerró sesión');
Estados de Conexión de WebSocket
🔄
CONECTANDO
0
✅
ABIERTO
1
⏳
CERRANDO
2
❌
CERRADO
3
// Verificando el estado de conexión
if (socket.readyState === WebSocket.OPEN) {
socket.send(message);
}
// Constantes de estado
WebSocket.CONNECTING // 0 - Conexión en progreso
WebSocket.OPEN // 1 - Conexión establecida
WebSocket.CLOSING // 2 - Handshake de cierre en progreso
WebSocket.CLOSED // 3 - Conexión cerrada
Reconexión con Backoff Exponencial
// WebSocket robusto con auto-reconexión
class ReconnectingWebSocket {
constructor(url, options = {}) {
this.url = url;
this.options = {
maxRetries: 10,
baseDelay: 1000,
maxDelay: 30000,
...options
};
this.retryCount = 0;
this.handlers = new Map();
this.messageQueue = [];
this.connect();
}
connect() {
this.socket = new WebSocket(this.url);
this.socket.onopen = () => {
console.log('WebSocket conectado');
this.retryCount = 0;
// Enviar mensajes en cola
while (this.messageQueue.length > 0) {
const message = this.messageQueue.shift();
this.socket.send(message);
}
this.emit('open');
};
this.socket.onmessage = (event) => {
const data = JSON.parse(event.data);
this.emit('message', data);
// Emitir eventos tipados
if (data.type) {
this.emit(data.type, data.payload);
}
};
this.socket.onclose = (event) => {
this.emit('close', event);
if (!event.wasClean) {
this.reconnect();
}
};
this.socket.onerror = (error) => {
console.error('Error de WebSocket:', error);
this.emit('error', error);
};
}
reconnect() {
if (this.retryCount >= this.options.maxRetries) {
console.error('Se alcanzó el máximo de intentos de reconexión');
this.emit('max_retries');
return;
}
// Backoff exponencial con jitter
const delay = Math.min(
this.options.baseDelay * Math.pow(2, this.retryCount) +
Math.random() * 1000,
this.options.maxDelay
);
console.log(`Reconectando en ${delay}ms (intento ${this.retryCount + 1})`);
setTimeout(() => {
this.retryCount++;
this.connect();
}, delay);
}
send(type, payload) {
const message = JSON.stringify({ type, payload });
if (this.socket.readyState === WebSocket.OPEN) {
this.socket.send(message);
} else {
// Encolar mensaje para cuando se abra la conexión
this.messageQueue.push(message);
}
}
on(event, handler) {
if (!this.handlers.has(event)) {
this.handlers.set(event, []);
}
this.handlers.get(event).push(handler);
}
emit(event, data) {
const handlers = this.handlers.get(event) || [];
handlers.forEach(handler => handler(data));
}
close() {
this.socket.close(1000, 'Cierre iniciado por usuario');
}
}
// Uso
const ws = new ReconnectingWebSocket('wss://api.example.com/ws');
ws.on('open', () => console.log('¡Conectado!'));
ws.on('notification', (data) => showNotification(data));
ws.on('chat_message', (data) => addMessage(data));
ws.send('subscribe', { channel: 'updates' });
Ejemplo de Aplicación de Chat
// Chat en tiempo real con WebSockets
class ChatClient {
constructor(serverUrl, userId) {
this.userId = userId;
this.ws = new ReconnectingWebSocket(serverUrl);
this.messageCallbacks = [];
this.ws.on('open', () => {
// Autenticar al conectar
this.ws.send('auth', { userId: this.userId });
});
this.ws.on('chat_message', (message) => {
this.messageCallbacks.forEach(cb => cb(message));
});
this.ws.on('user_joined', (user) => {
console.log(`${user.name} se unió al chat`);
});
this.ws.on('user_left', (user) => {
console.log(`${user.name} salió del chat`);
});
this.ws.on('typing', ({ userId, isTyping }) => {
this.updateTypingIndicator(userId, isTyping);
});
}
joinRoom(roomId) {
this.currentRoom = roomId;
this.ws.send('join_room', { roomId });
}
leaveRoom() {
if (this.currentRoom) {
this.ws.send('leave_room', { roomId: this.currentRoom });
this.currentRoom = null;
}
}
sendMessage(text) {
this.ws.send('chat_message', {
roomId: this.currentRoom,
text,
timestamp: Date.now()
});
}
startTyping() {
this.ws.send('typing', { roomId: this.currentRoom, isTyping: true });
}
stopTyping() {
this.ws.send('typing', { roomId: this.currentRoom, isTyping: false });
}
onMessage(callback) {
this.messageCallbacks.push(callback);
}
updateTypingIndicator(userId, isTyping) {
// Actualizar UI para mostrar "Usuario está escribiendo..."
}
}
// Uso
const chat = new ChatClient('wss://chat.example.com', 'user-123');
chat.joinRoom('room-456');
chat.onMessage((message) => {
addMessageToUI(message);
});
// Enviar mensaje
sendButton.onclick = () => {
chat.sendMessage(input.value);
input.value = '';
};
// Indicador de escritura
let typingTimeout;
input.oninput = () => {
chat.startTyping();
clearTimeout(typingTimeout);
typingTimeout = setTimeout(() => chat.stopTyping(), 2000);
};
Server-Sent Events (SSE)
Para streaming unidireccional de servidor a cliente, SSE es más simple que WebSockets:
// Server-Sent Events - alternativa más simple para actualizaciones unidireccionales
const eventSource = new EventSource('https://api.example.com/events');
// Evento de mensaje predeterminado
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Recibido:', data);
};
// Eventos nombrados
eventSource.addEventListener('notification', (event) => {
const notification = JSON.parse(event.data);
showNotification(notification);
});
eventSource.addEventListener('price_update', (event) => {
const price = JSON.parse(event.data);
updatePriceDisplay(price);
});
// Eventos de conexión
eventSource.onopen = () => {
console.log('Conexión SSE abierta');
};
eventSource.onerror = (error) => {
if (eventSource.readyState === EventSource.CLOSED) {
console.log('Conexión SSE cerrada');
} else {
console.error('Error SSE:', error);
}
};
// Cerrar conexión
eventSource.close();
// SSE con autenticación (usando fetch para headers personalizados)
async function createAuthenticatedSSE(url, token) {
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${token}`
}
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { value, done } = await reader.read();
if (done) break;
const text = decoder.decode(value);
const lines = text.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = JSON.parse(line.slice(6));
handleEvent(data);
}
}
}
}
Socket.io - Librería WebSocket Popular
// Socket.io proporciona características adicionales:
// - Auto-reconexión
// - Soporte para salas/espacios de nombres
// - Fallback a HTTP polling
// - Confirmaciones de mensajes
import { io } from 'socket.io-client';
// Conectar con opciones
const socket = io('https://api.example.com', {
auth: {
token: 'tu-jwt-token'
},
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000
});
// Eventos de conexión
socket.on('connect', () => {
console.log('Conectado con ID:', socket.id);
});
socket.on('disconnect', (reason) => {
console.log('Desconectado:', reason);
});
socket.on('connect_error', (error) => {
console.error('Error de conexión:', error);
});
// Enviando y recibiendo mensajes
socket.emit('chat_message', {
room: 'general',
text: '¡Hola a todos!'
});
socket.on('chat_message', (message) => {
addMessageToUI(message);
});
// Con confirmación (como un callback)
socket.emit('send_message', { text: 'Hola' }, (response) => {
console.log('Mensaje enviado, servidor respondió:', response);
});
// Salas (unirse/salir)
socket.emit('join_room', 'room-123');
socket.emit('leave_room', 'room-123');
// Espacios de nombres
const adminSocket = io('https://api.example.com/admin');
const chatSocket = io('https://api.example.com/chat');
// Mensajes volátiles (pueden descartarse si no están listos)
socket.volatile.emit('cursor_position', { x: 100, y: 200 });
// Datos binarios
socket.emit('file_upload', fileBuffer);
Visualización de Datos en Tiempo Real
// Actualizaciones de precios de acciones en vivo
class StockTicker {
constructor() {
this.ws = new ReconnectingWebSocket('wss://stocks.example.com');
this.subscriptions = new Set();
this.priceCallbacks = new Map();
this.ws.on('price_update', (data) => {
const callbacks = this.priceCallbacks.get(data.symbol) || [];
callbacks.forEach(cb => cb(data));
});
}
subscribe(symbol, callback) {
if (!this.subscriptions.has(symbol)) {
this.subscriptions.add(symbol);
this.ws.send('subscribe', { symbol });
}
if (!this.priceCallbacks.has(symbol)) {
this.priceCallbacks.set(symbol, []);
}
this.priceCallbacks.get(symbol).push(callback);
}
unsubscribe(symbol) {
this.subscriptions.delete(symbol);
this.priceCallbacks.delete(symbol);
this.ws.send('unsubscribe', { symbol });
}
}
// Uso
const ticker = new StockTicker();
ticker.subscribe('AAPL', (data) => {
document.getElementById('aapl-price').textContent = data.price;
document.getElementById('aapl-change').textContent = data.change;
});
ticker.subscribe('GOOGL', (data) => {
updateChart('GOOGL', data);
});
💡 Mejores Prácticas de WebSocket
- ✓ Implementar lógica de reconexión - Las conexiones se caerán
- ✓ Usar backoff exponencial - No bombardear el servidor
- ✓ Enviar heartbeats/pings - Mantener la conexión viva
- ✓ Encolar mensajes cuando desconectado - Enviar al reconectar
- ✓ Usar JSON para mensajes - Datos estructurados y parseables
- ✓ Manejar todos los estados de conexión - Eventos open, close, error