Saltar al contenido

Clase 21 - WebSockets con NestJS

#nest #socket-io #websockets

1. 🌐 Introducción a WebSockets

WebSockets es un protocolo de comunicación que permite establecer una conexión bidireccional persistente entre el cliente y el servidor sobre una única conexión TCP. Esto significa que una vez que se establece la conexión, ambos extremos pueden enviar y recibir datos en cualquier momento, sin necesidad de realizar solicitudes HTTP adicionales como ocurre en el modelo tradicional.

En el modelo HTTP, cada petición del cliente requiere una respuesta del servidor, y cada intercambio implica abrir y cerrar la conexión (a menos que se utilice HTTP Keep-Alive). WebSockets elimina esta fricción al mantener una conexión viva, lo cual permite una comunicación en tiempo real más fluida y eficiente.

Esta tecnología es particularmente útil en aplicaciones modernas que demandan inmediatez, interactividad y sincronización continua de datos.

🤔 ¿Por qué usar WebSockets?

  • Comunicación en tiempo real: Perfecto para aplicaciones que requieren respuesta inmediata, como chats, videojuegos online o sistemas de monitoreo.
  • Menor latencia: Al evitar establecer nuevas conexiones para cada mensaje, se reduce significativamente el tiempo de respuesta.
  • Bidireccionalidad: Permite que el servidor pueda enviar información al cliente sin que éste lo solicite.
  • Eficiencia en red: Genera menos overhead de red en comparación con técnicas como polling o long-polling, que requieren múltiples solicitudes HTTP.

📊 Casos de uso comunes

  • Aplicaciones de chat en tiempo real
  • Notificaciones push personalizadas
  • Dashboards con datos en vivo (analytics, IoT, etc.)
  • Juegos multijugador en línea
  • Plataformas de colaboración (como Google Docs)
  • Trading en bolsa y actualizaciones de precios

2. 🔧 Socket.IO: Más que WebSockets

Socket.IO es una librería de JavaScript para aplicaciones web que permite una comunicación bidireccional en tiempo real entre clientes y servidores. Aunque se apoya en WebSockets como su transporte principal, también ofrece una serie de mejoras y mecanismos de compatibilidad que lo hacen más robusto y versátil.

Socket.IO abstrae muchas complejidades del protocolo WebSocket nativo y agrega características avanzadas que facilitan la creación de aplicaciones en tiempo real confiables.

✨ Características principales

  • Fallback automático: Si WebSockets no está disponible (por ejemplo, en redes corporativas con firewalls restrictivos), Socket.IO utiliza otros transportes como HTTP long-polling para garantizar la conectividad.
  • Reconexión automática: Si se pierde la conexión, Socket.IO intenta reconectar al cliente de forma automática, lo que mejora la resiliencia.
  • Salas (Rooms): Permite agrupar clientes en “salas” para enviar mensajes a subconjuntos específicos.
  • Middlewares: Se pueden definir funciones intermedias para procesar o validar datos antes de ejecutar la lógica principal (ideal para autenticación o logging).
  • Eventos personalizados: Permite definir eventos semánticos específicos para cada acción, facilitando la organización del código y su mantenimiento.

🆚 Socket.IO vs WebSockets nativos

Comparación WebSocket vs Socket.IO
// WebSocket nativo
const ws = new WebSocket('ws://localhost:3000');
ws.onmessage = (event) => {
console.log('Mensaje recibido:', event.data);
};
ws.send('Hola servidor');
// Socket.IO
const socket = io('http://localhost:3000');
socket.on('mensaje_personalizado', (data) => {
console.log('Mensaje recibido:', data);
});
socket.emit('enviar_mensaje', { texto: 'Hola servidor' });

Como se puede observar, Socket.IO provee una interfaz más intuitiva y robusta que incluye nombres de eventos, soporte para reconexión, y mecanismos de agrupamiento que WebSockets por sí solos no proporcionan.

3. 🚀 Configuración inicial en NestJS

📦 Instalación de dependencias

Instalación de dependencias
pnpm add @nestjs/websockets @nestjs/platform-socket.io socket.io
pnpm add -D @types/socket.io

⚙️ Configuración básica del módulo

app.module.ts
// app.module.ts
import { Module } from '@nestjs/common';
import { ChatGateway } from './chat/chat.gateway';
@Module({
providers: [ChatGateway],
})
export class AppModule {}

4. 🌟 Gateways en NestJS

Los Gateways son clases que manejan las conexiones WebSocket en NestJS. Son similares a los controladores pero para comunicación en tiempo real.

📇 Estructura básica de un Gateway

chat.gateway.ts
// chat.gateway.ts
import {
WebSocketGateway,
WebSocketServer,
SubscribeMessage,
OnGatewayInit,
OnGatewayConnection,
OnGatewayDisconnect,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
@WebSocketGateway({
cors: {
origin: '*', // En producción, especifica dominios específicos
},
})
export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
@WebSocketServer()
server: Server;
afterInit(server: Server) {
console.log('Gateway inicializado');
}
handleConnection(client: Socket) {
console.log(`Cliente conectado: ${client.id}`);
}
handleDisconnect(client: Socket) {
console.log(`Cliente desconectado: ${client.id}`);
}
@SubscribeMessage('message')
handleMessage(client: Socket, payload: any): string {
return 'Mensaje recibido en el servidor';
}
}

🔧 Decoradores principales

@WebSocketGateway(options?)
Marca una clase como Gateway y permite configurar opciones:

Configuración del Gateway
@WebSocketGateway({
port: 3001,
namespace: '/chat',
cors: {
origin: ['http://localhost:3000', 'https://miapp.com'],
credentials: true,
},
})

@WebSocketServer()
Inyecta la instancia del servidor Socket.IO:

@WebSocketServer()
server: Server;

@SubscribeMessage(‘evento’)
Define manejadores para eventos específicos:

@SubscribeMessage('chat_message')
handleChatMessage(client: Socket, data: { message: string; user: string }) {
return { status: 'success', timestamp: new Date() };
}

5. 💬 Ejemplo práctico: Sistema de Chat

🖥️ Gateway completo

Gateway completo del sistema de chat
// chat.gateway.ts
import {
WebSocketGateway,
WebSocketServer,
SubscribeMessage,
OnGatewayConnection,
OnGatewayDisconnect,
MessageBody,
ConnectedSocket,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
interface ChatMessage {
user: string;
message: string;
timestamp: Date;
}
interface User {
id: string;
username: string;
room: string;
}
@WebSocketGateway({
cors: { origin: '*' },
})
export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
@WebSocketServer()
server: Server;
private users: Map<string, User> = new Map();
handleConnection(client: Socket) {
console.log(`Cliente conectado: ${client.id}`);
}
handleDisconnect(client: Socket) {
const user = this.users.get(client.id);
if (user) {
// Notificar a otros usuarios en la sala
client.to(user.room).emit('user_left', {
username: user.username,
message: `${user.username} ha salido del chat`,
});
this.users.delete(client.id);
}
console.log(`Cliente desconectado: ${client.id}`);
}
@SubscribeMessage('join_room')
handleJoinRoom(
@ConnectedSocket() client: Socket,
@MessageBody() data: { username: string; room: string },
) {
// Guardar información del usuario
this.users.set(client.id, {
id: client.id,
username: data.username,
room: data.room,
});
// Unir al cliente a la sala
client.join(data.room);
// Notificar a otros usuarios en la sala
client.to(data.room).emit('user_joined', {
username: data.username,
message: `${data.username} se ha unido al chat`,
});
// Confirmar al usuario que se unió
client.emit('joined_room', {
room: data.room,
message: `Te has unido a la sala: ${data.room}`,
});
}
@SubscribeMessage('send_message')
handleSendMessage(
@ConnectedSocket() client: Socket,
@MessageBody() data: { message: string },
) {
const user = this.users.get(client.id);
if (!user) {
client.emit('error', { message: 'Usuario no encontrado' });
return;
}
const chatMessage: ChatMessage = {
user: user.username,
message: data.message,
timestamp: new Date(),
};
// Enviar mensaje a todos los usuarios en la sala
this.server.to(user.room).emit('new_message', chatMessage);
}
@SubscribeMessage('get_users')
handleGetUsers(@ConnectedSocket() client: Socket) {
const user = this.users.get(client.id);
if (!user) return;
const roomUsers = Array.from(this.users.values())
.filter(u => u.room === user.room)
.map(u => ({ id: u.id, username: u.username }));
client.emit('users_list', roomUsers);
}
}

🌐 Cliente JavaScript (ejemplo)

Cliente JavaScript
// cliente.js
const socket = io('http://localhost:3000');
// Unirse a una sala
socket.emit('join_room', {
username: 'Santiago',
room: 'general'
});
// Enviar mensaje
function sendMessage() {
const message = document.getElementById('messageInput').value;
socket.emit('send_message', { message });
document.getElementById('messageInput').value = '';
}
// Escuchar nuevos mensajes
socket.on('new_message', (data) => {
const messagesDiv = document.getElementById('messages');
messagesDiv.innerHTML += `
<div>
<strong>${data.user}:</strong> ${data.message}
<small>(${new Date(data.timestamp).toLocaleTimeString()})</small>
</div>
`;
});
// Escuchar usuarios que se conectan
socket.on('user_joined', (data) => {
console.log(data.message);
});
// Escuchar errores
socket.on('error', (data) => {
console.error('Error:', data.message);
});

6. 🎯 Ejercicio Prático

Objetivo

Implementa un sistema de notificaciones en tiempo real usando NestJS y WebSockets, donde los usuarios reciban alertas personalizadas al estar conectados. Estas notificaciones se integrarán posteriormente en un componente tipo navbar (como una campana o panel lateral).

🧩 Requisitos

  1. Funcionalidad del servidor

    El servidor puede enviar notificaciones dirigidas a usuarios específicos.

  2. Comunicación en tiempo real

    El cliente recibe las notificaciones en tiempo real.

  3. Gestión de estado

    Las notificaciones pueden marcarse como leídas, y este estado debe sincronizarse con el servidor.

🧠 Reglas de negocio

  • Cada notificación debe tener: id, userId, message, type, read: boolean, timestamp.
  • Un usuario solo debe recibir notificaciones que le correspondan.
  • El contador de notificaciones no leídas debe actualizarse en tiempo real.

💡 Estructura sugerida

  • Directoriosrc/
    • Directorionotifications/
      • notifications.gateway.ts
      • notifications.service.ts
      • notifications.module.ts
      • Directoriodto/
        • notification.dto.ts
      • Directoriointerfaces/
        • notification.interface.ts
    • app.module.ts

🔧 Interfaces sugeridas

Interfaces de notificaciones
// interfaces/notification.interface.ts
export interface Notification {
id: string;
userId: string;
message: string;
type: 'info' | 'warning' | 'error' | 'success';
read: boolean;
timestamp: Date;
}
export interface NotificationUser {
id: string;
socketId: string;
userId: string;
}