libro / blog / websockets-socketio-chat

// tiempo real

Chat en tiempo real con Node.js y Socket.IO

Gustavo Arias10 min de lectura

TL;DR HTTP funciona con preguntas y respuestas; para que el servidor envíe datos cuando quiera necesitas una conexión persistente, como WebSockets. Socket.IO la simplifica con eventos con nombre, salas y reconexión automática. Valida cada mensaje en el servidor y escapa el texto al mostrarlo.

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));
  });
});
LlamadaLe 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

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