Chat en tiempo real con Socket.IO, Redis y Mongo DB

Este proyecto es una prueba de concepto de un sistema de chat en tiempo real con soporte para múltiples salas, historial de mensajes y notificaciones. Está desarrollado con tecnologías pensadas para escalar en una arquitectura basada en microservicios.

Tecnologías utilizadas

  • Node.js + Express: Servidor HTTP y manejo de rutas.
  • Socket.IO: Comunicación en tiempo real con WebSockets.
  • MongoDB: Persistencia de mensajes y salas.
  • Redis: Sistema pub/sub para distribuir mensajes entre múltiples instancias del servidor.
  • HTML + JS: Cliente web simple.
  • PrimeFlex (opcional): Mejora visual de la interfaz.

Por qué Redis

Redis se usa para habilitar la publicación y suscripción de mensajes entre múltiples procesos o instancias del servidor. Esto es necesario cuando el servidor se ejecuta en un entorno distribuido o balanceado, ya que Socket.IO por sí solo no comparte información entre instancias.

Usar Redis asegura que todos los usuarios conectados reciban los mensajes aunque estén atendidos por diferentes procesos o servidores.

Estructura del Proyecto chat_0.1

Este proyecto tiene una arquitectura organizada para el desarrollo de una aplicación de chat utilizando Node.js con TypeScript y Redis. A continuación se describen los directorios y archivos principales:

Directorios

  • .git/
    Carpeta de configuración del repositorio Git. Contiene el historial de versiones y configuraciones de control de versiones.
  • node_modules/
    Directorio generado automáticamente por npm, contiene todas las dependencias del proyecto.
  • src/
    Carpeta principal del código fuente de la aplicación.
    • public/
      Directorio opcionalmente usado para archivos públicos (como HTML, CSS o JS accesibles directamente).
    • index.html
      Este archivo es la página principal que se muestra al abrir la aplicación en el navegador. Sirve como punto de entrada visual y normalmente contiene el diseño básico de la interfaz, además de los scripts necesarios para que la aplicación funcione.
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8" />
  <title>Chat en Tiempo Real</title>
  <link rel="stylesheet" href="https://unpkg.com/primeflex@3.3.1/primeflex.min.css">
  <link rel="stylesheet" href="https://unpkg.com/primeicons/primeicons.css">
  <style>
    body {
      font-family: sans-serif;
      padding: 2rem;
    }
    #chatBox {
      border: 1px solid #ccc;
      height: 250px;
      overflow-y: auto;
      padding: 1rem;
      background: #f4f4f4;
      border-radius: 6px;
    }
    #chatBox p {
      margin: 0.5rem 0;
    }
  </style>
</head>
<body>
  <h2 class="mb-3">💬 Chat en Tiempo Real</h2>

  <div class="flex flex-column gap-2 mb-3">
    <input id="username" class="p-inputtext p-component" placeholder="Tu nombre" />
    <input id="roomName" class="p-inputtext p-component" placeholder="Nombre de la sala" />
    <button class="p-button p-component" onclick="joinRoom()">Entrar</button>
  </div>

  <h3>Salas disponibles:</h3>
  <ul id="roomList" class="mb-3"></ul>

  <div id="chat" style="display:none;">
    <h3>Chat en Sala</h3>
    <div id="chatBox" class="mb-2"></div>
    <div class="flex gap-2">
      <input id="inputMsg" class="p-inputtext p-component flex-1" placeholder="Mensaje..." />
      <button class="p-button p-component" onclick="sendMessage()">Enviar</button>
    </div>
  </div>

  <audio id="notifSound" src="https://notificationsounds.com/storage/sounds/file-sounds-1154-pristine.mp3" preload="auto"></audio>

  <script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
  <script>
    const socket = io();
    let currentRoom = '';

    // Pedir permiso para notificaciones
    if (Notification.permission !== 'granted') {
      Notification.requestPermission();
    }

    async function joinRoom() {
      const room = document.getElementById('roomName').value;
      const username = document.getElementById('username').value;
      if (!room || !username) return alert('Ingresa nombre y sala');

      currentRoom = room;
      document.getElementById('chat').style.display = 'block';
      clearMessages();

      socket.emit('joinRoom', room, username);

      socket.off('chatMessage');
      socket.on('chatMessage', (data) => {
        addMessage(`${data.user}: ${data.message}`);
        playNotificationSound();
        showNotification(data.user, data.message);
      });

      socket.off('notification');
      socket.on('notification', (msg) => {
        addMessage(`🔔 ${msg}`);
      });

      socket.off('chat-history');
      socket.on('chat-history', (messages) => {
        messages.forEach(msg => {
          addMessage(`${msg.user}: ${msg.message}`);
        });
      });

      addMessage(`🟢 Te uniste a la sala: ${room}`);
    }

    function sendMessage() {
      const message = document.getElementById('inputMsg').value;
      const user = document.getElementById('username').value;
      if (!message.trim()) return;
      socket.emit('chatMessage', { room: currentRoom, user, message });
      document.getElementById('inputMsg').value = '';
    }

    function addMessage(msg) {
      const box = document.getElementById('chatBox');
      const p = document.createElement('p');
      p.textContent = msg;
      box.appendChild(p);
      box.scrollTop = box.scrollHeight;
    }

    function clearMessages() {
      document.getElementById('chatBox').innerHTML = '';
    }

    async function fetchRooms() {
      try {
        const res = await fetch('/rooms');
        const rooms = await res.json();
        const list = document.getElementById('roomList');
        list.innerHTML = '';
        rooms.forEach(r => {
          const li = document.createElement('li');
          li.textContent = r.name;
          list.appendChild(li);
        });
      } catch (err) {
        console.error('Error al cargar salas:', err);
      }
    }

    function showNotification(user, message) {
      if (Notification.permission === 'granted') {
        new Notification(`💬 ${user}`, {
          body: message,
          icon: 'https://cdn-icons-png.flaticon.com/512/2331/2331942.png'
        });
      }
    }

    function playNotificationSound() {
      const sound = document.getElementById('notifSound');
      sound.currentTime = 0;
      sound.play().catch(() => {});
    }

    fetchRooms();
  </script>
</body>
</html>
  • db.ts
    Archivo de configuración y conexión a la base de datos (posiblemente MongoDB, PostgreSQL u otro).
import mongoose from 'mongoose';

await mongoose.connect('mongodb://localhost:27017/chat-app');

const messageSchema = new mongoose.Schema({
  room: String,
  user: String,
  message: String,
  timestamp: { type: Date, default: Date.now }
});

const roomSchema = new mongoose.Schema({
  name: { type: String, unique: true }
});

export const Message = mongoose.model('Message', messageSchema);
export const Room = mongoose.model('Room', roomSchema);
  • index.ts
    Punto de entrada principal de la aplicación. Es donde se inicializa y arranca el servidor o el core del sistema.
import express from 'express';
import http from 'http';
import { Server } from 'socket.io';
import { pub, sub } from './redis.js';
import { Message, Room } from './db.js';
import path from 'path';
import { fileURLToPath } from 'url';

const app = express();
const server = http.createServer(app);
const io = new Server(server);
const __dirname = path.dirname(fileURLToPath(import.meta.url));

app.use(express.static(path.join(__dirname, 'public')));
app.use(express.json());

// Listar salas existentes
app.get('/rooms', async (_req, res) => {
  const rooms = await Room.find().select('name -_id');
  res.json(rooms);
});

io.on('connection', (socket) => {
  console.log('Cliente conectado');

  socket.on('joinRoom', async (room, username) => {
    socket.join(room);
    // Enviar historial al nuevo usuario
    const history = await Message.find({ room }).sort({ timestamp: 1 }).lean();
    socket.emit('chat-history', history);

    // Crear sala si no existe
    const exists = await Room.findOne({ name: room });
    if (!exists) {
      await new Room({ name: room }).save();
    }

    socket.to(room).emit('notification', `${username} se unió al chat`);
  });

  socket.on('chatMessage', async (data) => {
    const { room, user, message } = data;
    const chatMessage = { room, user, message };

    await pub.publish('chat', JSON.stringify(chatMessage));
    await new Message(chatMessage).save();
  });
});

await sub.subscribe('chat', (msg) => {
  const parsed = JSON.parse(msg);
  io.to(parsed.room).emit('chatMessage', parsed);
});

server.listen(3000,'0.0.0.0', () => {
  console.log('Servidor en http://localhost:3000');
});
  • redis.ts
    Configuración y lógica de conexión con Redis, utilizado probablemente para almacenamiento en caché o manejo de sesiones/chat en tiempo real.
import { createClient } from 'redis';

export const pub = createClient();
export const sub = createClient();

await pub.connect();
await sub.connect();
  • types.ts
    Archivo que define tipos y estructuras TypeScript personalizadas para mantener el tipado fuerte y organizado.
export interface ChatMessage {
  room: string;
  user: string;
  message: string;
}

export interface ChatRoom {
  name: string;
}

Archivos de Configuración

  • .gitignore
    Lista de archivos y carpetas que Git debe ignorar (por ejemplo: node_modules, logs, claves, etc.).
  • docker-compose.yml
    Archivo de configuración para Docker Compose. Define los servicios y contenedores necesarios para correr la aplicación en un entorno de contenedores (como Node, Redis, etc.).
  • package.json
    Archivo principal de configuración del proyecto Node.js. Contiene información del proyecto, scripts y dependencias.
  • package-lock.json
    Archivo generado automáticamente para bloquear versiones exactas de las dependencias, garantizando entornos consistentes.
  • README.md
    Archivo de documentación del proyecto. Suele contener instrucciones de instalación, uso y propósito del sistema.
  • tsconfig.json
    Configuración del compilador TypeScript. Define cómo se debe compilar el proyecto (paths, target, módulo, etc.).

Funcionalidades

  • Crear o unirse a salas desde el frontend
  • Listar salas disponibles
  • Enviar y recibir mensajes en tiempo real
  • Recibir historial de mensajes al ingresar a una sala
  • Persistencia de mensajes en MongoDB
  • Notificaciones dentro del chat
  • Exposición en red local usando 0.0.0.0

Código fuente:

GITHUB

¡Mensaje enviado!