193 lines
5.9 KiB
Python
193 lines
5.9 KiB
Python
#!/usr/bin/env python3
|
|
import socket
|
|
import threading
|
|
from dataclasses import dataclass
|
|
|
|
DEFAULT_HOST = "0.0.0.0"
|
|
DEFAULT_PORT = 5050
|
|
ENCODING = "utf-8"
|
|
|
|
@dataclass
|
|
class ClientConn:
|
|
sock: socket.socket
|
|
addr: tuple
|
|
name: str
|
|
wfile: any
|
|
|
|
class ChatServer:
|
|
def __init__(self, host=DEFAULT_HOST, port=DEFAULT_PORT, on_log=None):
|
|
self.host = host
|
|
self.port = port
|
|
self.on_log = on_log if on_log else print
|
|
self.clients = {}
|
|
self.clients_lock = threading.Lock()
|
|
self.server_sock = None
|
|
self.running = False
|
|
self.thread = None
|
|
|
|
def log(self, message):
|
|
"""Envía el mensaje al callback configurado (GUI o print)"""
|
|
self.on_log(message)
|
|
|
|
def broadcast(self, message: str, exclude: socket.socket | None = None) -> None:
|
|
"""Envía mensaje a todos los clientes conectados"""
|
|
line = message.rstrip("\n") + "\n"
|
|
with self.clients_lock:
|
|
items = list(self.clients.items())
|
|
|
|
# Log local si no está excluido el propio servidor (opcional, pero útil ver lo que se envía)
|
|
# self.log(f"BRD: {message}")
|
|
|
|
for sock, client in items:
|
|
if exclude is not None and sock is exclude:
|
|
continue
|
|
try:
|
|
client.wfile.write(line)
|
|
client.wfile.flush()
|
|
except Exception:
|
|
pass
|
|
|
|
def send_server_message(self, text: str):
|
|
"""Mensaje desde el servidor (admin)"""
|
|
msg = f"[ADMIN] {text}"
|
|
self.log(msg)
|
|
self.broadcast(msg)
|
|
|
|
def _handle_client(self, conn: socket.socket, addr: tuple) -> None:
|
|
try:
|
|
rfile = conn.makefile("r", encoding=ENCODING, newline="\n")
|
|
wfile = conn.makefile("w", encoding=ENCODING, newline="\n")
|
|
|
|
name = None
|
|
|
|
# Protocolo simple: pedir nombre
|
|
try:
|
|
wfile.write("NAME?\n")
|
|
wfile.flush()
|
|
except Exception:
|
|
return
|
|
|
|
try:
|
|
raw_name = rfile.readline()
|
|
except Exception:
|
|
raw_name = None
|
|
|
|
if not raw_name:
|
|
return
|
|
|
|
raw_name = raw_name.strip()
|
|
# Compatibilidad si el cliente manda "NAME Pepe"
|
|
if raw_name.upper().startswith("NAME "):
|
|
raw_name = raw_name[5:].strip()
|
|
|
|
name = raw_name or f"{addr[0]}:{addr[1]}"
|
|
|
|
with self.clients_lock:
|
|
self.clients[conn] = ClientConn(sock=conn, addr=addr, name=name, wfile=wfile)
|
|
|
|
msg_join = f"* {name} se ha unido al chat *"
|
|
self.log(msg_join)
|
|
self.broadcast(msg_join)
|
|
|
|
while self.running:
|
|
try:
|
|
line = rfile.readline()
|
|
except Exception:
|
|
break
|
|
|
|
if not line:
|
|
break
|
|
|
|
msg = line.strip()
|
|
if not msg:
|
|
continue
|
|
|
|
if msg.lower() in {"/quit", "/exit"}:
|
|
break
|
|
|
|
# Mostrar en servidor y reenviar a otros
|
|
full_msg = f"[{name}] {msg}"
|
|
self.log(full_msg)
|
|
self.broadcast(full_msg)
|
|
|
|
except Exception as e:
|
|
self.log(f"Error gestionando cliente {addr}: {e}")
|
|
finally:
|
|
with self.clients_lock:
|
|
self.clients.pop(conn, None)
|
|
try:
|
|
conn.close()
|
|
except Exception:
|
|
pass
|
|
|
|
if name:
|
|
msg_left = f"* {name} ha salido del chat *"
|
|
self.log(msg_left)
|
|
self.broadcast(msg_left)
|
|
|
|
def start_background(self):
|
|
"""Inicia el servidor en un hilo secundario"""
|
|
if self.running:
|
|
return
|
|
self.running = True
|
|
self.thread = threading.Thread(target=self._run_server_loop, daemon=True)
|
|
self.thread.start()
|
|
|
|
def stop(self):
|
|
"""Detiene el servidor"""
|
|
self.running = False
|
|
if self.server_sock:
|
|
try:
|
|
self.server_sock.close()
|
|
except Exception:
|
|
pass
|
|
self.log("Servidor detenido.")
|
|
|
|
def _run_server_loop(self):
|
|
self.server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
self.server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
|
|
try:
|
|
self.server_sock.bind((self.host, self.port))
|
|
self.server_sock.listen(100)
|
|
self.server_sock.settimeout(1.0) # Timeout para permitir verificar self.running
|
|
self.log(f"Servidor escuchando en {self.host}:{self.port}")
|
|
|
|
while self.running:
|
|
try:
|
|
conn, addr = self.server_sock.accept()
|
|
t = threading.Thread(target=self._handle_client, args=(conn, addr), daemon=True)
|
|
t.start()
|
|
except TimeoutError:
|
|
continue
|
|
except OSError:
|
|
break
|
|
except Exception as e:
|
|
self.log(f"Error aceptando conexión: {e}")
|
|
|
|
except Exception as e:
|
|
self.log(f"Error fatal en servidor: {e}")
|
|
finally:
|
|
self.running = False
|
|
try:
|
|
self.server_sock.close()
|
|
except Exception:
|
|
pass
|
|
|
|
if __name__ == "__main__":
|
|
# Modo standalone para pruebas
|
|
import sys
|
|
try:
|
|
srv = ChatServer(port=5050)
|
|
srv.start_background()
|
|
print("Presiona Ctrl+C para salir.")
|
|
while True:
|
|
cmd = sys.stdin.readline()
|
|
if not cmd: break
|
|
if cmd.strip():
|
|
srv.send_server_message(cmd.strip())
|
|
except KeyboardInterrupt:
|
|
pass
|
|
finally:
|
|
srv.stop()
|