Comunicación WebSocket en Tiempo Real con Socket.io en Node.js
Construye aplicaciones WebSocket en tiempo real en Node.js con Socket.io
Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.
Visión General
La comunicación WebSocket en tiempo real permite el intercambio instantáneo de datos entre servidor y clientes. Socket.io provee una capa robusta sobre WebSockets con auto-reconexión, rooms, namespaces y broadcasting. Esta recipe cubre la configuración de Socket.io con Express, rooms para mensajería grupal, namespaces para separación, middleware de autenticación y escalado con Redis adapter.
Cuándo Usar
- Estás construyendo una aplicación de chat, sistema de notificaciones o dashboard en vivo
- Necesitas push del servidor al cliente sin polling
- Quieres broadcasting basado en rooms (ej., enviar mensajes solo a usuarios en un canal específico)
- Necesitas escalar conexiones WebSocket a través de múltiples instancias de Node.js
Solución
Servidor Socket.io básico con Express
const express = require("express");
const http = require("http");
const { Server } = require("socket.io");
const app = express();
const server = http.createServer(app);
const io = new Server(server, {
cors: { origin: "http://localhost:5173", methods: ["GET", "POST"] }
});
io.on("connection", (socket) => {
console.log(`User connected: ${socket.id}`);
socket.on("message", (data) => {
io.emit("message", { ...data, timestamp: Date.now() });
});
socket.on("disconnect", (reason) => {
console.log(`User disconnected: ${socket.id} — ${reason}`);
});
});
server.listen(3000, () => console.log("Server running on port 3000"));
Conexión del lado del cliente
import { io } from "socket.io-client";
const socket = io("http://localhost:3000");
socket.on("connect", () => {
console.log("Connected:", socket.id);
});
socket.on("message", (data) => {
console.log("Received:", data);
});
socket.emit("message", { user: "alice", text: "Hello world" });
socket.on("disconnect", () => {
console.log("Disconnected from server");
});
Rooms para mensajería grupal
io.on("connection", (socket) => {
socket.on("join-room", (room) => {
socket.join(room);
socket.to(room).emit("user-joined", { id: socket.id });
console.log(`${socket.id} joined room: ${room}`);
});
socket.on("leave-room", (room) => {
socket.leave(room);
socket.to(room).emit("user-left", { id: socket.id });
});
socket.on("room-message", ({ room, message }) => {
socket.to(room).emit("room-message", {
from: socket.id,
message,
timestamp: Date.now()
});
});
});
Namespaces para separación
const chatNamespace = io.of("/chat");
const adminNamespace = io.of("/admin");
chatNamespace.on("connection", (socket) => {
console.log(`Chat user: ${socket.id}`);
socket.on("send-message", (msg) => {
chatNamespace.emit("new-message", { from: socket.id, msg });
});
});
adminNamespace.on("connection", (socket) => {
console.log(`Admin connected: ${socket.id}`);
adminNamespace.emit("stats", {
chatUsers: chatNamespace.sockets.size,
adminUsers: adminNamespace.sockets.size
});
});
Middleware de autenticación
io.use((socket, next) => {
const token = socket.handshake.auth.token;
if (!token) {
return next(new Error("Authentication token required"));
}
try {
const payload = verifyToken(token);
socket.userId = payload.userId;
socket.username = payload.username;
next();
} catch (err) {
next(new Error("Invalid authentication token"));
}
});
io.on("connection", (socket) => {
console.log(`Authenticated user: ${socket.username} (${socket.userId})`);
socket.on("private-message", ({ to, message }) => {
io.to(to).emit("private-message", {
from: socket.id,
fromUser: socket.username,
message
});
});
});
function verifyToken(token) {
if (token === "valid-token") {
return { userId: 1, username: "alice" };
}
throw new Error("Invalid token");
}
Escalado con Redis adapter
const express = require("express");
const http = require("http");
const { Server } = require("socket.io");
const { createAdapter } = require("@socket.io/redis-adapter");
const Redis = require("ioredis");
const app = express();
const server = http.createServer(app);
const io = new Server(server);
const pubClient = new Redis({ host: "localhost", port: 6379 });
const subClient = pubClient.duplicate();
io.adapter(createAdapter(pubClient, subClient));
io.on("connection", (socket) => {
socket.on("broadcast", (data) => {
io.emit("broadcast", data);
});
});
server.listen(3000, () => console.log("Server running on port 3000"));
Aplicación de chat completa
const express = require("express");
const http = require("http");
const { Server } = require("socket.io");
const app = express();
const server = http.createServer(app);
const io = new Server(server, {
cors: { origin: "*" }
});
const users = new Map();
io.on("connection", (socket) => {
socket.on("user-join", (username) => {
users.set(socket.id, { username, joinedAt: Date.now() });
io.emit("user-list", [...users.values()].map(u => u.username));
socket.broadcast.emit("notification", `${username} joined the chat`);
});
socket.on("chat-message", (message) => {
const user = users.get(socket.id);
if (!user) return;
io.emit("chat-message", {
username: user.username,
message,
timestamp: Date.now()
});
});
socket.on("typing", (isTyping) => {
const user = users.get(socket.id);
if (!user) return;
socket.broadcast.emit("typing", { username: user.username, isTyping });
});
socket.on("disconnect", () => {
const user = users.get(socket.id);
if (user) {
users.delete(socket.id);
io.emit("user-list", [...users.values()].map(u => u.username));
socket.broadcast.emit("notification", `${user.username} left the chat`);
}
});
});
server.listen(3000, () => console.log("Chat server running on port 3000"));
Explicación
Socket.io envuelve el protocolo WebSocket con características adicionales:
- Auto-reconexión: Si la conexión cae, el cliente reintenta automáticamente con backoff exponencial. Puedes configurar intentos y delays.
- Rooms: Los sockets pueden unirse a rooms nombrados.
socket.to(room).emit()envía a todos los sockets en ese room excepto al remitente.io.to(room).emit()envía a todos incluyendo al remitente. - Namespaces: Canales separados en el mismo servidor. Útiles para dividir features (ej.,
/chaty/admin) con diferentes middleware y handlers. - Broadcasting:
socket.broadcast.emit()envía a todos los sockets conectados excepto al remitente.io.emit()envía a todos. - Redis adapter: Cuando ejecutas múltiples instancias de Node.js, el Redis adapter asegura que los mensajes se transmitan entre todas las instancias. Sin él, los mensajes solo llegan a los sockets conectados a la instancia actual.
- Fallback: Socket.io automáticamente hace fallback a HTTP long-polling si WebSocket no está disponible, asegurando compatibilidad con redes restrictivas.
Variantes
| Feature | Método | Usar Cuando |
|---|---|---|
| Broadcast a todos | io.emit() | Anuncios globales |
| Broadcast excluyendo remitente | socket.broadcast.emit() | Mensajes de chat |
| Mensajería por room | io.to(room).emit() | Canales grupales |
| Separación por namespace | io.of("/namespace") | Aislamiento de features |
| Mensaje privado | io.to(socketId).emit() | Mensajería directa |
| Multi-instancia | Redis adapter | Escalado horizontal |
Pautas
- Siempre maneja el evento
disconnectpara limpiar el estado del usuario. - Usa rooms para mensajería grupal en vez de iterar sobre sockets conectados.
- Configura los orígenes CORS explícitamente en producción.
- Usa el Redis adapter cuando ejecutes más de una instancia de Node.js.
- Valida y autentica conexiones de socket vía middleware.
- Evita enviar payloads grandes por WebSockets. Usa REST para datos masivos.
- Implementa rate limiting en eventos de socket para prevenir abuso.
Errores Comunes
- No limpiar el estado del usuario al desconectar. Los usuarios aparecen online para siempre.
- Usar
io.emit()cuando se necesitasocket.broadcast.emit(). El remitente recibe mensajes duplicados. - No configurar CORS en producción. Los clientes del navegador no pueden conectarse.
- Ejecutar múltiples instancias sin Redis adapter. Los mensajes solo llegan a sockets de una instancia.
- No manejar eventos de reconexión. Los usuarios pierden sus membresías de room tras reconectar.
- Emitir datos sensibles a todos los sockets en vez de rooms o usuarios específicos.
Preguntas Frecuentes
¿Cómo obtengo el socket ID de un usuario específico?
Mantén un mapeo de userId a socketId:
const userSockets = new Map();
io.on("connection", (socket) => {
socket.on("authenticate", (userId) => {
userSockets.set(userId, socket.id);
socket.userId = userId;
});
socket.on("disconnect", () => {
userSockets.delete(socket.userId);
});
});
function sendToUser(userId, event, data) {
const socketId = userSockets.get(userId);
if (socketId) {
io.to(socketId).emit(event, data);
}
}
¿Cómo manejo la reconexión y el reingreso a rooms?
Escucha el evento connect en el cliente y vuelve a unirte a los rooms:
socket.on("connect", () => {
socket.emit("join-room", currentRoom);
});
Socket.io maneja la reconexión automáticamente. El evento connect se dispara después de cada reconexión exitosa.
¿Socket.io o WebSocket puro — cuál debería usar?
Usa Socket.io cuando necesites auto-reconexión, rooms, namespaces o fallback a long-polling. Usa WebSocket puro (paquete ws) cuando quieras overhead mínimo, sin dependencias extra, o necesites interoperar con clientes no-Socket.io.
¿Cómo limito el número de conexiones concurrentes?
Usa middleware para imponer límites:
const MAX_CONNECTIONS = 1000;
io.use((socket, next) => {
if (io.engine.clientsCount > MAX_CONNECTIONS) {
return next(new Error("Server at capacity"));
}
next();
}); Recursos Relacionados
WebSocket Server
How to build a WebSocket server for bidirectional real-time communication, with connection management, message broadcasting, and heartbeat keepalive.
RecipeBuild a Bidirectional Chat with WebSocket and Node.js
How to build a real-time bidirectional chat application using WebSocket with room-based messaging, presence tracking, and message persistence
RecipeWebSocket Authentication and Security Patterns
How to authenticate WebSocket connections, implement token validation, and handle authorization for real-time messaging in production