# compatibile Windows 11 # compatibile Ubuntu 24.10 # compatibile python 3.12.7 import socket import threading import time import select import sys # Added for printing errors to stderr # --- Client Class Definition (originally nested concept in servertcp.py) --- class Client: """ Rappresenta un client connesso al server. """ def __init__(self, name, endpoint, thread, sock): """ Inizializza l'oggetto Client. Args: name (str): Un nome identificativo per il client (attualmente fisso a "cl"). endpoint (tuple): Una tupla (indirizzo_ip, porta) del client. thread (threading.Thread): Il thread che gestisce questo client. sock (socket.socket): L'oggetto socket per la connessione con questo client. """ self.name = name self.endpoint = endpoint self.thread = thread self.sock = sock self.keep_alive = True # Flag per indicare se il thread del client deve continuare # --- ServerTCP Class Definition (originally from servertcp.py) --- class ServerTCP: """ Un server TCP che gestisce connessioni multiple, riceve e invia messaggi. """ MAXLENBUF = 10240 # Dimensione massima del buffer di ricezione per messaggio TIMEOUT_MS = 100 # Timeout per select() nel loop di servizio client (in ms) ACCEPT_TIMEOUT = 0.5 # Timeout per socket.accept() in secondi def __init__(self, host="0.0.0.0", port=5000): """ Inizializza l'oggetto ServerTCP. Args: host (str): L'indirizzo IP su cui ascoltare (default "0.0.0.0" per tutte le interfacce). port (int): La porta su cui ascoltare (default 5000). """ self.listen_host = host self.listen_port = port self.listener_socket = None # Socket di ascolto del server self.clients = [] # Lista degli oggetti Client connessi self.listening_thread = None # Thread che accetta nuove connessioni self.on_msg_arrived = None # Callback per messaggio ricevuto: func(server, client_address, message) self.on_msg_error = None # Callback per errore: func(server, client_address, error_message) self.on_msg_state = None # Callback per cambio stato connessione: func(server, client_address, state) [CONNECT/DISCONNECT] self.lock = threading.Lock() # Lock per proteggere l'accesso alla lista clients self.close_event = threading.Event() # Evento per segnalare la chiusura a tutti i thread self.is_closing = False # Flag per indicare che il server è in fase di chiusura @property def port(self): """Ottiene la porta di ascolto.""" return self.listen_port @port.setter def port(self, value): """Imposta la porta di ascolto (solo prima di start()).""" if self.listening_thread and self.listening_thread.is_alive(): print("Non è possibile cambiare la porta mentre il server è in esecuzione.", file=sys.stderr) else: self.listen_port = value @property def connected_clients_count(self): """Ottiene il numero di client attualmente connessi.""" with self.lock: return len(self.clients) def start(self): """ Avvia il server e inizia ad ascoltare le connessioni in entrata. """ if self.listening_thread and self.listening_thread.is_alive(): print("Il server è già in esecuzione.") return self.is_closing = False self.close_event.clear() self.clients = [] # Resetta la lista dei client all'avvio try: self.listener_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # Permette il riutilizzo dell'indirizzo subito dopo la chiusura self.listener_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.listener_socket.bind((self.listen_host, self.listen_port)) self.listener_socket.listen() # Imposta un timeout per accept() per permettere al thread di controllare is_closing self.listener_socket.settimeout(self.ACCEPT_TIMEOUT) print(f"Server in ascolto su {self.listen_host}:{self.listen_port}...") self.listening_thread = threading.Thread(target=self._accept_loop, daemon=True) self.listening_thread.start() if self.on_msg_state: # Passa None come client_address per lo stato generale del server self.on_msg_state(self, None, f"SERVER_STARTED on {self.listen_host}:{self.listen_port}") except socket.error as e: self._handle_error(None, f"Errore nell'avvio del listener: {e}") if self.listener_socket: self.listener_socket.close() self.listener_socket = None except Exception as ex: self._handle_error(None, f"Errore generico nell'avvio del server: {ex}") if self.listener_socket: self.listener_socket.close() self.listener_socket = None def close(self): """ Chiude il server, disconnette tutti i client e rilascia le risorse. """ if self.is_closing: # print("Chiusura già in corso...") return print("Avvio procedura di chiusura del server...") self.is_closing = True self.close_event.set() # Segnala a tutti i thread di terminare # 1. Chiudi il socket di ascolto per non accettare nuove connessioni listener = self.listener_socket self.listener_socket = None # Imposta a None prima di chiudere if listener: try: print("Chiusura socket di ascolto...") # Shutdown non necessario/utile su socket non connesso in ascolto listener.close() except Exception as e: self._handle_error(None, f"Errore durante la chiusura del socket di ascolto: {e}") # 2. Attendi che il thread di ascolto termini if self.listening_thread and self.listening_thread.is_alive(): print("In attesa della terminazione del thread di ascolto...") self.listening_thread.join(timeout=self.ACCEPT_TIMEOUT * 2) # Attendi un po' più del timeout di accept if self.listening_thread.is_alive(): print("Attenzione: Il thread di ascolto non è terminato correttamente.", file=sys.stderr) self.listening_thread = None # 3. Disconnetti e chiudi tutti i client with self.lock: clients_to_close = list(self.clients) # Crea una copia per iterare self.clients.clear() # Svuota la lista originale print(f"Chiusura di {len(clients_to_close)} connessioni client...") for client_obj in clients_to_close: client_obj.keep_alive = False # Segnala al thread del client di uscire sock = client_obj.sock thread = client_obj.thread endpoint = client_obj.endpoint if sock: try: # Avvisa il client della chiusura (potrebbe fallire se già disconnesso) sock.shutdown(socket.SHUT_RDWR) sock.close() except (socket.error, OSError) as e: # Ignora errori qui, la connessione potrebbe essere già morta # print(f"Nota: Errore durante shutdown/close del client {endpoint}: {e}") pass # Attendi che il thread del client termini if thread and thread.is_alive(): thread.join(timeout=1.0) # Timeout breve per il join del client if thread.is_alive(): print(f"Attenzione: Thread per client {endpoint} non terminato.", file=sys.stderr) if self.on_msg_state: self.on_msg_state(self, None, "SERVER_STOPPED") print("Server chiuso.") self.is_closing = False # Resetta il flag alla fine def _accept_loop(self): """ Loop eseguito nel listening_thread per accettare connessioni. """ #print("Thread di ascolto avviato.") while not self.is_closing: if not self.listener_socket: # Controlla se il socket è stato chiuso print("Socket di ascolto non valido, uscita dal loop di accettazione.") break try: # accept() bloccherà per ACCEPT_TIMEOUT o finché non arriva una connessione client_socket, client_address = self.listener_socket.accept() if self.is_closing: # Ricontrolla subito dopo accept, potremmo star chiudendo try: client_socket.close(); except: pass break print(f"Nuova connessione accettata da: {client_address}") client_socket.settimeout(10) # Imposta un timeout per le operazioni sul socket client # Avvia un thread dedicato per gestire questo client client_thread = threading.Thread(target=self._service_client, args=(client_socket, client_address), daemon=True) # Crea l'oggetto Client per tracciare la connessione client_obj = Client(f"cl_{client_address[0]}_{client_address[1]}", client_address, client_thread, client_socket) with self.lock: self.clients.append(client_obj) client_thread.start() # Avvia il thread *dopo* aver aggiunto alla lista except socket.timeout: # Il timeout è normale, serve per permettere il controllo di self.is_closing continue except socket.error as e: # Se stiamo chiudendo, gli errori sul listener sono attesi if not self.is_closing: self._handle_error(None, f"Errore socket nel loop di accettazione: {e}") # Esci dal loop in caso di errore non legato alla chiusura break except Exception as ex: if not self.is_closing: self._handle_error(None, f"Errore generico nel loop di accettazione: {ex}") break # Esci per errori generici print("Thread di ascolto terminato.") def _service_client(self, client_socket, client_address): """ Funzione eseguita in un thread per gestire un singolo client connesso. """ client_obj = None # Trova l'oggetto Client corrispondente (dovrebbe essere l'ultimo aggiunto) with self.lock: # Cerca l'oggetto client corrispondente a questo socket/thread # Questo è un po' fragile, sarebbe meglio passare client_obj direttamente se possibile # o usare un dizionario basato su socket o indirizzo. for c in reversed(self.clients): # Cerca dall'ultimo aggiunto if c.sock == client_socket: client_obj = c break if not client_obj: print(f"Errore: Oggetto client non trovato per {client_address}. Chiusura thread.", file=sys.stderr) try: client_socket.close(); except: pass return if self.on_msg_state: self.on_msg_state(self, client_address, "CONNECT") receive_buffer = bytearray() # Buffer per accumulare dati parziali per questo client keep_alive = True try: while keep_alive and not self.close_event.is_set() and client_obj.keep_alive: # Usa select per attendere dati senza bloccare indefinitamente ready_to_read, _, _ = select.select([client_socket], [], [], self.TIMEOUT_MS / 1000.0) if ready_to_read: try: # Leggi dati nel buffer temporaneo chunk = client_socket.recv(4096) if not chunk: # Connessione chiusa dal client print(f"Client {client_address} ha chiuso la connessione.") keep_alive = False break # Esce dal loop while receive_buffer.extend(chunk) # Aggiungi i dati ricevuti al buffer # Elabora il buffer per estrarre messaggi completi while True: delimiter_index = -1 # Cerca il primo delimitatore \n (0x0A) o \f (0x0C) lf_index = receive_buffer.find(b'\x0A') ff_index = receive_buffer.find(b'\x0C') if lf_index != -1 and ff_index != -1: delimiter_index = min(lf_index, ff_index) elif lf_index != -1: delimiter_index = lf_index elif ff_index != -1: delimiter_index = ff_index else: # Nessun delimitatore trovato, attendi altri dati break # Estrai il messaggio (escluso il delimitatore) message_bytes = receive_buffer[:delimiter_index] # Rimuovi il messaggio e il delimitatore dal buffer receive_buffer = receive_buffer[delimiter_index + 1:] # Decodifica e invia al callback try: message = message_bytes.decode('utf-8', errors='ignore').strip() if message and self.on_msg_arrived: self.on_msg_arrived(self, client_address, message) except Exception as decode_ex: self._handle_error(client_address, f"Errore decodifica messaggio: {decode_ex}") except socket.timeout: # Timeout su recv (non dovrebbe accadere con select, ma per sicurezza) continue except socket.error as e: # Gestisci errori specifici if e.errno == socket.errno.ECONNRESET or e.errno == socket.errno.ETIMEDOUT or e.errno == socket.errno.EPIPE: print(f"Connessione persa o resettata da {client_address}: {e}") else: if not self.is_closing: # Non loggare errori se stiamo chiudendo self._handle_error(client_address, f"Errore socket nel servizio client: {e}") keep_alive = False # Interrompi il loop per questo client break except Exception as e: if not self.is_closing: self._handle_error(client_address, f"Errore generico nel servizio client: {e}") keep_alive = False break # else: # Timeout di select, nessun dato ricevuto, il loop continua e ricontrolla i flag # Controlla se il server sta chiudendo o se questo client specifico deve terminare if self.close_event.is_set() or not client_obj.keep_alive: keep_alive = False break except Exception as ex: # Errore imprevisto nel thread del client if not self.is_closing: self._handle_error(client_address, f"Errore critico nel thread client: {ex}") finally: # Pulizia per questo client print(f"Thread di servizio per {client_address} terminato.") if self.on_msg_state: self.on_msg_state(self, client_address, "DISCONNECT") # Chiudi il socket del client try: client_socket.close() except Exception: pass # Rimuovi il client dalla lista dei client attivi with self.lock: # Usa un try-except perché client_obj potrebbe essere già stato rimosso # durante la chiusura generale del server try: self.clients.remove(client_obj) except ValueError: pass # Il client non era (più) nella lista def _send_to_client(self, client_obj, message_bytes): """ Invia un messaggio (bytes) a uno specifico client. Helper interno. Returns: bool: True se l'invio ha avuto successo (o presunto tale), False altrimenti. """ if not client_obj or not client_obj.sock: return False try: client_obj.sock.sendall(message_bytes) return True except socket.timeout: self._handle_error(client_obj.endpoint, "Timeout durante l'invio.") return False except socket.error as e: # Considera la connessione persa in caso di errore di invio self._handle_error(client_obj.endpoint, f"Errore socket durante l'invio: {e}. Disconnessione presunta.") client_obj.keep_alive = False # Segnala al thread di terminare # Non rimuovere dalla lista qui, lo farà il finally del thread _service_client try: client_obj.sock.close(); # Prova a chiudere subito il socket except: pass return False except Exception as e: self._handle_error(client_obj.endpoint, f"Errore generico durante l'invio: {e}") client_obj.keep_alive = False try: client_obj.sock.close(); except: pass return False def send(self, message, client_address=None): """ Invia un messaggio a un client specifico o a tutti i client connessi. Args: message (str): Il messaggio da inviare. client_address (tuple, optional): L'indirizzo (ip, porta) del client a cui inviare. Se None, invia a tutti. Returns: bool: True se l'invio è andato a buon fine per almeno un client (o per il client specifico), False altrimenti o se nessun client era connesso/specificato. """ if self.is_closing or not (self.listening_thread and self.listening_thread.is_alive()): print("Impossibile inviare: il server non è attivo o è in fase di chiusura.", file=sys.stderr) return False if not message: return False try: # Assicurati che il messaggio sia in bytes e termini correttamente (es. \r\n) # L'aggiunta di \r\n dovrebbe avvenire prima di chiamare send() se necessario. message_bytes = message.encode('utf-8') except Exception as e: self._handle_error(None, f"Errore codifica messaggio: {e}") return False if len(message_bytes) > self.MAXLENBUF: self._handle_error(None, f"Messaggio troppo lungo ({len(message_bytes)} bytes), max {self.MAXLENBUF}") return False success = False clients_to_send = [] with self.lock: if client_address: # Trova il client specifico for c in self.clients: if c.endpoint == client_address: clients_to_send.append(c) break if not clients_to_send: self._handle_error(client_address, "Client non trovato per l'invio.") else: # Invia a tutti (broadcast) - crea una copia della lista clients_to_send = list(self.clients) if not clients_to_send: if not client_address: print("Nessun client connesso a cui inviare.") return False # Invia ai client selezionati (fuori dal lock per evitare blocchi prolungati) for client_obj in clients_to_send: #print(f"Invio a {client_obj.endpoint}: {message.strip()}") # Debug if self._send_to_client(client_obj, message_bytes): success = True # Basta un invio riuscito per restituire True return success def _handle_error(self, client_address, message): """Gestore interno per errori, chiama il callback on_msg_error.""" if self.on_msg_error: try: self.on_msg_error(self, client_address, message) except Exception as e: print(f"Errore nell'eseguire il callback on_msg_error: {e}", file=sys.stderr) else: addr_str = f"Client {client_address}: " if client_address else "Server: " print(f"[ERRORE] {addr_str}{message}", file=sys.stderr) # --- Callback Functions (originally from main_servertcp.py) --- def handle_message(server, client_address, message): """Callback per gestire i messaggi ricevuti dai client.""" ip, port = client_address print(f"<< [{ip}:{port}]: {message}") # Esempio: invia un ACK al client specifico # server.send(f"ACK: {message}\r\n", client_address) def handle_error(server, client_address, error_message): """Callback per gestire gli errori relativi a un client o al server.""" addr_str = f"Client {client_address}" if client_address else "Server" print(f"[CALLBACK ERRORE] {addr_str}: {error_message}", file=sys.stderr) def handle_state(server, client_address, state): """Callback per gestire i cambi di stato (connessione/disconnessione/avvio/stop).""" addr_str = f"Client {client_address}" if client_address else "Server" print(f"[CALLBACK STATO] {addr_str}: {state}") # --- Main Execution Block (originally from main_servertcp.py) --- if __name__ == '__main__': SERVER_PORT = 5000 # Porta su cui il server ascolterà print("Avvio Server TCP...") server = ServerTCP(port=SERVER_PORT) # Assegna le funzioni di callback server.on_msg_arrived = handle_message server.on_msg_error = handle_error server.on_msg_state = handle_state # Avvia il server (inizia ad ascoltare in un thread separato) server.start() # Se il server non si è avviato correttamente, esci if not (server.listening_thread and server.listening_thread.is_alive()): print("Avvio del server fallito. Uscita.") sys.exit(1) print("\nServer avviato. Inserisci un messaggio da inviare a tutti i client.") print("Digita 'quit' per fermare il server e uscire.") try: while True: # Loop per l'input da console # L'input() bloccherà questo thread principale finché l'utente non scrive qualcosa command = input("> ") if command.lower() == 'quit': break # Esci dal loop while elif not command: continue # Ignora input vuoto else: # Invia il messaggio a tutti i client connessi # Aggiungi \r\n come terminatore, comune per molti protocolli testuali server.send(command + '\r\n') except KeyboardInterrupt: print("\nInterruzione da tastiera (Ctrl+C) ricevuta. Chiusura server...") except EOFError: # Ctrl+D (Unix) o Ctrl+Z+Enter (Windows) print("\nInput chiuso (EOF). Chiusura server...") finally: # Assicurati che il server venga chiuso correttamente print("Procedura di chiusura avviata dal blocco finally...") server.close() print("Programma terminato.")