proyecto/chat_server.py

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()