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.
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();
}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