Chats, notificaciones, tableros que se actualizan solos, juegos multijugador: todos necesitan que el servidor envíe datos al navegador en el momento en que ocurren, sin esperar a que el cliente pregunte. Node.js es especialmente bueno para esto porque mantiene miles de conexiones abiertas con muy pocos recursos. En el libro Node.js desde cero uno de los tres proyectos es justamente una aplicación de chat en tiempo real; acá construimos una versión mínima y funcional.
Por qué HTTP no alcanza
En HTTP, el cliente pregunta y el servidor responde. Para “enterarse” de un mensaje nuevo, el navegador tendría que preguntar una y otra vez (polling), con mucho tráfico inútil y retraso. WebSocket abre una conexión persistente y bidireccional: cualquiera de los dos lados puede enviar datos en cualquier momento.
Socket.IO trabaja sobre WebSocket y añade lo que vas a necesitar en una aplicación real: eventos con nombre, salas, reconexión automática y confirmaciones de recepción.
El servidor
$ npm install express socket.io
import { createServer } from 'node:http';
import express from 'express';
import { Server } from 'socket.io';
const app = express();
app.use(express.static('public')); // sirve public/index.html
const httpServer = createServer(app);
const io = new Server(httpServer);
io.on('connection', (socket) => {
console.log('Conectado:', socket.id);
socket.on('mensaje', (texto) => {
// Validar SIEMPRE lo que llega del cliente
if (typeof texto !== 'string' || !texto.trim() || texto.length > 500) return;
// io.emit: a todos, incluido quien lo envió
io.emit('mensaje', { autor: socket.id.slice(0, 5), texto: texto.trim(), hora: Date.now() });
});
socket.on('disconnect', () => console.log('Desconectado:', socket.id));
});
httpServer.listen(3000, () => console.log('Chat en http://localhost:3000'));
Fijate en que Socket.IO se monta sobre el mismo servidor HTTP que Express: la página y el chat comparten el puerto 3000.
El 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" autocomplete="off" maxlength="500" placeholder="Escribí un mensaje">
<button>Enviar</button>
</form>
<!-- El servidor de Socket.IO sirve automáticamente su cliente en esta ruta -->
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io();
const lista = document.querySelector('#mensajes');
const input = document.querySelector('#texto');
document.querySelector('#form').addEventListener('submit', (e) => {
e.preventDefault();
if (input.value.trim()) socket.emit('mensaje', input.value);
input.value = '';
});
socket.on('mensaje', ({ autor, texto, hora }) => {
const li = document.createElement('li');
// textContent, no innerHTML: un mensaje con <script> se muestra como texto
li.textContent = `[${new Date(hora).toLocaleTimeString()}] ${autor}: ${texto}`;
lista.append(li);
});
</script>
</body>
</html>
Abrí http://localhost:3000 en dos pestañas y escribí en una: el mensaje aparece al instante
en la otra. Ese es el chat completo.
Salas y “está escribiendo…”
Las salas agrupan sockets para enviar mensajes solo a una parte de los usuarios: un canal de chat, el tablero de un proyecto, la conversación entre dos personas.
const SALAS = new Set(['general', 'node', 'empleos']);
io.on('connection', (socket) => {
socket.on('unirse', (sala, confirmar) => {
if (!SALAS.has(sala)) return confirmar?.({ ok: false, error: 'Sala inexistente' });
socket.join(sala);
socket.data.sala = sala;
socket.to(sala).emit('aviso', 'Alguien entró a la sala'); // a todos menos a quien entra
confirmar?.({ ok: true }); // acknowledgement: respuesta directa al emisor
});
socket.on('mensaje', (texto) => {
const sala = socket.data.sala;
if (!sala || typeof texto !== 'string' || !texto.trim()) return;
io.to(sala).emit('mensaje', { autor: socket.id.slice(0, 5), texto: texto.trim().slice(0, 500) });
});
// "está escribiendo": volatile = si se pierde, no importa
socket.on('escribiendo', () => {
if (socket.data.sala) socket.volatile.to(socket.data.sala).emit('escribiendo', socket.id.slice(0, 5));
});
});
| Llamada | Le llega a |
|---|---|
socket.emit(...) | Solo a ese cliente |
io.emit(...) | A todos los clientes conectados |
socket.broadcast.emit(...) | A todos menos a ese cliente |
io.to('sala').emit(...) | A todos los de la sala |
socket.to('sala').emit(...) | A los de la sala, menos a ese cliente |
Antes de llevarlo a producción
- Autenticación: verificá el usuario al conectar con un middleware de Socket.IO
(
io.use()), por ejemplo con el mismo JWT de tu API (ver autenticación con JWT), y usá el nombre real en lugar del id del socket. - Límites: tamaño máximo de mensaje y cantidad de mensajes por segundo por usuario, para evitar spam e inundaciones.
- Persistencia: guardá los mensajes en una base de datos (como MongoDB, ver Node.js con MySQL y MongoDB) para mostrar el historial a quien se conecta.
- Varios servidores: si ejecutás más de una instancia, los clientes conectados a una no reciben los mensajes emitidos en otra. Socket.IO lo resuelve con un adapter (por ejemplo, el de Redis) y requiere “sticky sessions” en el balanceador.
- Proxy inverso: si usás Nginx delante, configurá el paso de las cabeceras
UpgradeyConnectionpara que funcione WebSocket (lo vemos en despliegue en producción).
Un chat completo, paso a paso
El libro Node.js desde cero cubre WebSockets y comunicación en tiempo real, y lo aplica en el proyecto de una aplicación de chat en tiempo real.
Comprar en Amazon