Servidor WebSocket en Dart para Chat Multi-Sala con Gestión de Usuarios en Línea

Servidor WebSocket en Dart para chat multi-sala con gestión de usuarios, mensajes en tiempo real y comandos para cambiar sala o apodo fácilmente.

Breve Introducción

Servidor WebSocket en Dart para Chat Multi-Sala con Gestión de Usuarios en Línea (dart)
DESCRIPCIÓN:
Implementación de un servidor WebSocket en Dart que soporta chat multi-sala, gestión de usuarios con nombres únicos, difusión de mensajes dentro de las salas y lista de usuarios en línea para cada sala. El servidor maneja múltiples conexiones, desconexiones y comandos especiales para cambiar de sala u obtener la lista de usuarios.

NOTAS ADICIONALES:
El servidor está listo para ejecutarse en un entorno Dart. Para probar, se pueden usar clientes WebSocket como navegadores o herramientas como 'websocat'. El código es robusto y maneja errores de conexión, nombres duplicados y comandos erróneos. La estructura JSON de los mensajes facilita futuras extensiones.

EXPLICACIONES:
- La clase Client representa un usuario conectado, mantiene el nombre, la sala y el sink WebSocket para enviar mensajes.
- El servidor mantiene dos mapas principales: uno para las salas y sus usuarios, otro para los nombres de usuario para evitar duplicados.
- Cuando un cliente se conecta, debe enviar un mensaje de tipo 'join' con el nombre de usuario y la sala deseada.
- Los mensajes de tipo 'message' se transmiten a todos los usuarios en la misma sala.
- Los comandos especiales comienzan con '/' y permiten cambiar de sala (/join), cambiar apodo (/nick) u obtener la lista de usuarios (/list).
- Al desconectarse, el usuario se elimina de la sala y se notifica a los demás usuarios.
- El servidor usa JSON para comunicarse, con tipos de mensajes 'info', 'error', 'message' y 'userlist'.

Código de Ejemplo: dart

import 'dart:convert';
import 'dart:io';

import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as io;
import 'package:shelf_web_socket/shelf_web_socket.dart';
import 'package:web_socket_channel/web_socket_channel.dart';

// Clase que representa un cliente conectado
class Client {
  String nomeUtente;
  String stanza;
  WebSocketChannel socket;

  Client({required this.nomeUtente, required this.stanza, required this.socket});

  // Envía un mensaje JSON al cliente
  void invia(Map<String, dynamic> messaggio) {
    socket.sink.add(jsonEncode(messaggio));
  }

  // Cierra la conexión
  void chiudi() {
    socket.sink.close();
  }
}

class ChatServer {
  // Mapa sala -> lista de clientes
  final Map<String, List<Client>> stanze = {};

  // Mapa nombreUsuario -> Client para evitar duplicados
  final Map<String, Client> utentiConnessi = {};

  // Puerto del servidor
  final int porta;

  ChatServer({this.porta = 8080});

  // Inicia el servidor WebSocket
  Future<void> avvia() async {
    final handler = webSocketHandler(_handleWebSocket);
    final server = await io.serve(handler, '0.0.0.0', porta);
    print('Servidor WebSocket iniciado en ws://0.0.0.0:${server.port}');
  }

  // Maneja la conexión WebSocket de un cliente
  void _handleWebSocket(WebSocketChannel socket) {
    Client? client;

    // Stream de mensajes entrantes
    socket.stream.listen((dato) {
      try {
        final Map<String, dynamic> messaggio = jsonDecode(dato);

        final tipo = messaggio['tipo'];

        if (client == null) {
          // El primer mensaje debe ser join con nombre y sala
          if (tipo == 'join') {
            final nome = messaggio['nomeUtente'];
            final stanza = messaggio['stanza'];

            if (nome == null || stanza == null || nome.isEmpty || stanza.isEmpty) {
              socket.sink.add(jsonEncode({
                'tipo': 'error',
                'messaggio': 'Nombre de usuario y sala son obligatorios para conectarse.'
              }));
              socket.sink.close();
              return;
            }

            if (utentiConnessi.containsKey(nome)) {
              socket.sink.add(jsonEncode({
                'tipo': 'error',
                'messaggio': 'Nombre de usuario ya en uso. Elige otro nombre.'
              }));
              socket.sink.close();
              return;
            }

            client = Client(nomeUtente: nome, stanza: stanza, socket: socket);
            utentiConnessi[nome] = client!;
            stanze.putIfAbsent(stanza, () => []);
            stanze[stanza]!.add(client!);

            // Envía confirmación
            client!.invia({
              'tipo': 'info',
              'messaggio': 'Conectado a la sala $stanza como $nome'
            });

            // Notifica a otros usuarios de la sala
            _broadcast(
                stanza,
                {
                  'tipo': 'info',
                  'messaggio': '$nome se ha unido a la sala.'
                },
                escludi: client);

            // Envía lista de usuarios actualizada
            _inviaListaUtenti(stanza);
          } else {
            socket.sink.add(jsonEncode({
              'tipo': 'error',
              'messaggio': 'Debes primero enviar un mensaje de tipo join con nombreUsuario y sala.'
            }));
            socket.sink.close();
          }
          return;
        }

        // Cliente ya conectado: manejamos mensajes y comandos
        switch (tipo) {
          case 'message':
            final testo = messaggio['messaggio'];
            if (testo == null || testo.isEmpty) {
              client!.invia({
                'tipo': 'error',
                'messaggio': 'Mensaje vacío no permitido.'
              });
              return;
            }

            // Si es un comando especial
            if (testo.startsWith('/')) {
              _gestisciComando(client!, testo);
            } else {
              // Difunde mensaje a todos en la sala
              _broadcast(client!.stanza, {
                'tipo': 'message',
                'nomeUtente': client!.nomeUtente,
                'messaggio': testo
              });
            }
            break;

          default:
            client!.invia({
              'tipo': 'error',
              'messaggio': 'Tipo de mensaje no reconocido.'
            });
        }
      } catch (e) {
        socket.sink.add(jsonEncode({
          'tipo': 'error',
          'messaggio': 'Error al analizar el mensaje: $e'
        }));
      }
    }, onDone: () {
      // Manejo de desconexión
      if (client != null) {
        _rimuoviClient(client!);
      }
    }, onError: (error) {
      if (client != null) {
        _rimuoviClient(client!);
      }
    });
  }

  // Maneja los comandos especiales enviados por el cliente
  void _gestisciComando(Client client, String comando) {
    final parti = comando.split(' ');
    final cmd = parti[0].toLowerCase();

    switch (cmd) {
      case '/join':
        if (parti.length < 2) {
          client.invia({
            'tipo': 'error',
            'messaggio': 'Uso correcto: /join nombreSala'
          });
          return;
        }
        final nuovaStanza = parti[1];
        if (nuovaStanza.isEmpty) {
          client.invia({
            'tipo': 'error',
            'messaggio': 'El nombre de la sala no puede estar vacío.'
          });
          return;
        }

        // Elimina de la sala actual
        stanze[client.stanza]?.remove(client);
        _broadcast(client.stanza, {
          'tipo': 'info',
          'messaggio': '${client.nomeUtente} ha dejado la sala.'
        });
        _inviaListaUtenti(client.stanza);

        // Añade a la nueva sala
        client.stanza = nuovaStanza;
        stanze.putIfAbsent(nuovaStanza, () => []);
        stanze[nuovaStanza]!.add(client);

        client.invia({
          'tipo': 'info',
          'messaggio': 'Has entrado en la sala $nuovaStanza'
        });

        _broadcast(nuovaStanza, {
          'tipo': 'info',
          'messaggio': '${client.nomeUtente} se ha unido a la sala.'
        }, escludi: client);

        _inviaListaUtenti(nuovaStanza);
        break;

      case '/list':
        // Envía lista de usuarios en la sala
        _inviaListaUtenti(client.stanza, destinatario: client);
        break;

      case '/nick':
        if (parti.length < 2) {
          client.invia({
            'tipo': 'error',
            'messaggio': 'Uso correcto: /nick nuevoNombre'
          });
          return;
        }
        final nuovoNome = parti[1];
        if (nuovoNome.isEmpty) {
          client.invia({
            'tipo': 'error',
            'messaggio': 'El nombre de usuario no puede estar vacío.'
          });
          return;
        }
        if (utentiConnessi.containsKey(nuovoNome)) {
          client.invia({
            'tipo': 'error',
            'messaggio': 'Nombre de usuario ya en uso.'
          });
          return;
        }

        // Cambia nombre
        utentiConnessi.remove(client.nomeUtente);
        final vecchioNome = client.nomeUtente;
        client.nomeUtente = nuovoNome;
        utentiConnessi[nuovoNome] = client;

        client.invia({
          'tipo': 'info',
          'messaggio': 'Nombre de usuario cambiado a $nuovoNome'
        });

        _broadcast(client.stanza, {
          'tipo': 'info',
          'messaggio': '$vecchioNome cambió su nombre a $nuovoNome'
        });

        _inviaListaUtenti(client.stanza);
        break;

      default:
        client.invia({
          'tipo': 'error',
          'messaggio': 'Comando no reconocido.'
        });
    }
  }

  // Envía la lista de usuarios de una sala a todos o a un destinatario específico
  void _inviaListaUtenti(String stanza, {Client? destinatario}) {
    final listaUtenti = stanze[stanza]?.map((c) => c.nomeUtente).toList() ?? [];
    final messaggio = {
      'tipo': 'userlist',
      'utenti': listaUtenti
    };

    if (destinatario != null) {
      destinatario.invia(messaggio);
    } else {
      _broadcast(stanza, messaggio);
    }
  }

  // Envía un mensaje a todos los clientes de una sala, opcionalmente excluyendo uno
  void _broadcast(String stanza, Map<String, dynamic> messaggio, {Client? escludi}) {
    final lista = stanze[stanza];
    if (lista == null) return;

    final jsonMsg = jsonEncode(messaggio);
    for (final client in lista) {
      if (client != escludi) {
        client.socket.sink.add(jsonMsg);
      }
    }
  }

  // Elimina un cliente de la gestión del servidor cuando se desconecta
  void _rimuoviClient(Client client) {
    utentiConnessi.remove(client.nomeUtente);
    stanze[client.stanza]?.remove(client);

    // Notifica a otros usuarios
    _broadcast(client.stanza, {
      'tipo': 'info',
      'messaggio': '${client.nomeUtente} se ha desconectado.'
    });

    _inviaListaUtenti(client.stanza);
  }
}

void main() async {
  final server = ChatServer(porta: 8080);
  await server.avvia();
}
×
×
×
¿Quieres regenerar la respuesta?
×
Exportar chat
Elige el formato de exportación
⏳ Generazione PDF in corso…
Allegati
×
⚠️ Estás a punto de cerrar el chat y pasar al generador de imágenes. Si no has iniciado sesión, perderás nuestro chat. ¿Confirmas?
👁 Estás viendo un chat compartido en modo temporal. No se guardará.
💬
×
Prompts guardados
×
Nota privada
×
Etiqueta
×
Buscar en todos los chats
×
Tus insights
Analizando…
×
Preferencias de IA
Nombre del asistente 30
Longitud de las respuestas
Modelo de IA
Respuestas estructuradas
Incluir ejemplos
Usar emojis
Guíame, no resuelvas Pistas y preguntas en vez de la solución hecha
Más ajustes →
×
Compartir este chat
Quien abra este enlace podrá ver el chat o añadirlo a su perfil como chat propio.
⚠️ Atención: también se compartirán los archivos adjuntos del chat. Quien lo añada recibirá una copia en su propia carpeta.
Chat compartido
Alguien ha compartido un chat contigo. ¿Solo quieres verlo o añadirlo a tus chats?
⚠️ Atención: también se compartirán los archivos adjuntos del chat. Quien lo añada recibirá una copia en su propia carpeta.
×

📌 Mensajes guardados

Cargando...

×

Historial de Chat

programas-ejemplo · HISTORIAL DE CHAT

Cargando...

Preferencias de IA

×
  • 🟢 BásicoRespuestas rápidas y esenciales para estudiar
  • 🔵 MedioMayor calidad para estudio y programación
  • 🟣 AvanzadoRazonamiento complejo y análisis detallado
Explicar Pasos

¿Qué se necesita?

Dart SDK >= 2.12 (null safety habilitado)

Dependencia: paquete 'web_socket_channel' para manejo de WebSocket

Dependencia: paquetes 'shelf' y 'shelf_web_socket' para servidor HTTP y WebSocket

Configuración: archivo pubspec.yaml con dependencias 'shelf', 'shelf_web_socket', 'web_socket_channel'

Configuración: puerto del servidor configurable (ej. 8080)

No se requiere hardware externo (solo software)

Protocolo: mensajes JSON con campos tipo, sala, usuario, mensaje

Gestión de usuarios: mapa de usuarios con nombres únicos, asociación conexión-sala

Gestión de salas: mapa de salas con lista de usuarios conectados

Comandos especiales: /join sala, /list usuarios, /nick nuevoNombre

Temas Similares

Conversión de Cadena a Número Entero en Dart
Aprende a convertir cadenas en números enteros en Dart con manejo de errores usando int.parse() y excepciones para evitar fallos en entradas inválidas.
Menú de Texto con Selección mediante Switch en Dart
Ejemplo en Dart de menú de texto con selección mediante switch para principiantes, mostrando opciones y respuestas según la elección del usuario.
Generador de Tabla de Multiplicar de un Número en Dart
Programa en Dart que genera la tabla de multiplicar de un número ingresado, ideal para principiantes que aprenden entrada/salida y ciclos.
Simulador de Cuenta Regresiva con Ciclo While en Dart
Aprende a crear un simulador de cuenta regresiva en Dart usando un ciclo while y Timer.periodic para manejar retrasos sin bloquear el hilo principal.
Unión de Dos Listas de Cadenas Sin Duplicados en Dart
Aprende a unir dos listas de cadenas en Dart sin duplicados usando Set, con código listo para ejecutar y compatible con Dart 2.12 o superior.
Gestión de una Biblioteca de Libros con Clases y Búsqueda Avanzada en Dart
Ejemplo completo en Dart para gestionar una biblioteca con clases, búsqueda avanzada, ordenación y manejo robusto de libros sin librerías externas.
Sistema de Reserva de Asientos para un Evento con Validaciones y Listas Dinámicas
Sistema de reserva de asientos en Dart con validaciones y listas dinámicas para gestionar eventos, controlando disponibilidad y datos de reservas.
Analizador de Archivos CSV en Dart con Filtros y Ordenación de Datos
Ejemplo en Dart para leer, filtrar y ordenar archivos CSV con manejo de errores y configuración de dependencias para análisis eficiente de datos.