# compatibile Windows 11 # compatibile Ubuntu 24.10 # compatibile python 3.12.7 import socket import struct import threading import time import sys import platform # Import kept for consistency, though not strictly needed for input now # --- START OF TCPClient CLASS (from former clienttcp_TLV.py) --- class TCPClient: """ Un client TCP minimale per testare il server TLV. Utilizza TLV per l'invio e la ricezione. La ricezione avviene in un thread separato. """ MAX_FRAME_SIZE = 256 # Deve corrispondere a quella del server def __init__(self, host, port, timeout=5): self.host = host self.port = port self.timeout = timeout self.socket = None self.receive_thread = None self.on_message = None self.on_error = None self.on_connect = None self.on_disconnect = None # Callback per la disconnessione self._stop_receive = threading.Event() self._closing = False # Usiamo bytearray per efficienza nell'aggiungere e rimuovere dati self.buffer = bytearray() self.receive_lock = threading.Lock() # Lock for buffer access if needed later def connect(self): """Tenta di connettersi al server e avvia il thread di ricezione.""" if self.socket: self._handle_error("Client già connesso o in fase di connessione.") return False # Indicate connection attempt failed or redundant try: self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # Impostare un timeout sul socket stesso può aiutare a sbloccare recv # in _receive_loop in caso di problemi, ma il timeout principale # per la connessione iniziale è gestito da settimeout prima di connect. # self.socket.settimeout(self.timeout * 2) # Timeout for operations after connect self.socket.settimeout(self.timeout) # Timeout specifically for the connect call print(f"Tentativo di connessione a {self.host}:{self.port}...") self.socket.connect((self.host, self.port)) self.socket.settimeout(None) # Rimuove il timeout dopo la connessione per recv bloccante self._closing = False self._stop_receive.clear() self._start_receive_thread() if self.on_connect: try: self.on_connect(self) except Exception as cb_ex: print(f"Errore nel callback on_connect: {cb_ex}") print(f"Client connesso al server {self.host}:{self.port}") return True # Indicate successful connection except socket.timeout: self._handle_error(f"Errore di connessione: Timeout ({self.timeout}s)") self._cleanup_socket() # Clean up failed socket attempt return False except ConnectionRefusedError: self._handle_error(f"Errore di connessione: Connessione rifiutata da {self.host}:{self.port}") self._cleanup_socket() return False except OSError as e: self._handle_error(f"Errore di rete durante la connessione: {e}") self._cleanup_socket() return False except Exception as e: self._handle_error(f"Errore imprevisto durante la connessione: {e}") self._cleanup_socket() # Ensure cleanup even on unexpected errors return False def _cleanup_socket(self): """Helper method to close and nullify the socket.""" if self.socket: try: self.socket.close() except Exception: pass # Ignore errors during cleanup close self.socket = None def _start_receive_thread(self): """Avvia il thread dedicato alla ricezione dei messaggi.""" if self.receive_thread and self.receive_thread.is_alive(): # Non dovrebbe succedere se la logica è corretta, ma per sicurezza print("Attenzione: Tentativo di avviare un thread di ricezione già esistente.") return self.receive_thread = threading.Thread(target=self._receive_loop, daemon=True) # daemon=True allows the main program to exit even if this thread is running self.receive_thread.start() def _receive_loop(self): """Loop eseguito nel thread di ricezione per leggere i dati dal socket.""" HEADER_SIZE = 5 # 1 byte Type + 4 bytes Length try: while not self._stop_receive.is_set(): if not self.socket: # Check if socket was closed externally break try: # recv è bloccante (socket.settimeout(None) in connect) # Si sbloccherà quando arrivano dati, la connessione si chiude, # o si verifica un errore. data = self.socket.recv(self.MAX_FRAME_SIZE) if not data: # Connessione chiusa dal server print("Connessione chiusa dal server.") self._handle_disconnect_internal() # Gestisce la disconnessione break # Esce dal loop di ricezione # Processa i dati ricevuti (potrebbero essere frammenti) with self.receive_lock: # Lock if buffer access needs protection self.buffer.extend(data) # Processa il buffer per estrarre messaggi TLV completi self._process_buffer() except ConnectionAbortedError: # Chiusura richiesta dal nostro lato (chiamata a close()) if not self._closing: self._handle_error("Connessione interrotta (abortita).") break # Esce dal loop except ConnectionResetError: self._handle_error("Connessione resettata dal server.") self._handle_disconnect_internal() break # Esce dal loop except OSError as e: # Altri errori di socket (es. socket chiuso mentre recv è bloccato) if self._stop_receive.is_set() or self._closing: # Errore atteso durante la chiusura break else: self._handle_error(f"Errore di rete/socket in ricezione: {e}") self._handle_disconnect_internal() break # Esce dal loop except Exception as ex: # Errori imprevisti nel loop if not self._closing: self._handle_error(f"Errore critico nel thread di ricezione: {ex}") break # Esce dal loop except Exception as thread_ex: # Cattura errori che potrebbero verificarsi fuori dal try/except interno if not self._closing: print(f"Errore imprevisto nel thread di ricezione esterno: {thread_ex}") finally: # Questo blocco viene eseguito quando il loop termina (normalmente o per errore) # Non chiamare close() qui, la chiusura è gestita dal thread principale # Assicurati che lo stato di disconnessione sia gestito correttamente # print("Thread di ricezione terminato.") # Debug pass def _process_buffer(self): """Processa il buffer interno per estrarre e gestire messaggi TLV completi.""" HEADER_SIZE = 5 message_processed = False with self.receive_lock: # Lock if buffer access needs protection while len(self.buffer) >= HEADER_SIZE: try: # Legge l'header (Tipo, Lunghezza) senza rimuoverlo ancora message_type, message_length = struct.unpack('!BI', self.buffer[:HEADER_SIZE]) except struct.error as e: self._handle_error(f"Errore unpacking header TLV: {e}. Buffer corrotto? Buffer: {self.buffer[:20]}...") self.buffer.clear() # Svuota il buffer se l'header non è valido break # Esce dal loop di processamento buffer # Verifica se abbiamo ricevuto l'intero messaggio (header + body) if len(self.buffer) >= HEADER_SIZE + message_length: # Estrae il corpo del messaggio message_data = self.buffer[HEADER_SIZE : HEADER_SIZE + message_length] # Rimuove il messaggio completo (header + body) dal buffer self.buffer = self.buffer[HEADER_SIZE + message_length:] # Processa il messaggio completo in base al tipo self._dispatch_message(message_type, message_data) message_processed = True else: # Non abbiamo ancora il messaggio completo, attendi altri dati break # Esce dal loop di processamento buffer # return message_processed # Could return status if needed def _dispatch_message(self, msg_type, msg_data): """Gestisce un messaggio TLV completo in base al suo tipo.""" if msg_type == 0x01: # Tipo 0x01: Messaggio di testo try: message_text = msg_data.decode('utf-8') if self.on_message: try: self.on_message(self, message_text) except Exception as cb_ex: print(f"Errore nel callback on_message: {cb_ex}") except UnicodeDecodeError: self._handle_error("Errore decodifica UTF-8 dal server") # Aggiungere qui altri gestori per tipi diversi (es. 0x02: Dati binari) # elif msg_type == 0x02: # if self.on_binary_message: # self.on_binary_message(self, msg_data) else: self._handle_error(f"Ricevuto tipo messaggio sconosciuto: {msg_type:#04x}") def send(self, message): """Invia un messaggio di testo al server, formattato come TLV.""" if not self.socket or self._closing: self._handle_error("Client non connesso o in fase di chiusura. Impossibile inviare.") return False try: message_bytes = message.encode('utf-8') msg_type = 0x01 # Tipo messaggio di testo # --- Optional: Check message size limit --- # MAX_SEND_PAYLOAD = 1024 * 10 # Example: 10KB limit for payload # if len(message_bytes) > MAX_SEND_PAYLOAD: # self._handle_error(f"Messaggio troppo grande ({len(message_bytes)} bytes), limite {MAX_SEND_PAYLOAD}.") # return False # --- header = struct.pack('!BI', msg_type, len(message_bytes)) full_message = header + message_bytes # sendall tenta di inviare tutti i dati, bloccando se necessario self.socket.sendall(full_message) return True except UnicodeEncodeError as e: self._handle_error(f"Errore codifica messaggio in UTF-8: {e}") return False except OSError as e: self._handle_error(f"Errore di invio (socket/rete): {e}") # Un errore qui spesso significa che la connessione è persa self._handle_disconnect_internal() # Trigger disconnect logic self.close() # Close our end return False except Exception as e: # Cattura altri errori imprevisti (es. socket non valido) self._handle_error(f"Errore imprevisto durante l'invio: {e}") self._handle_disconnect_internal() self.close() return False def close(self): """Chiude la connessione e termina il thread di ricezione.""" if self._closing or not self.socket: # print("Chiusura già in corso o client non connesso.") # Debug return # Già in chiusura o non connesso print("Avvio chiusura client...") self._closing = True self._stop_receive.set() # Segnala al thread di ricezione di fermarsi # Chiudi il socket. Questo dovrebbe sbloccare recv nel thread di ricezione. if self.socket: sock = self.socket self.socket = None # Imposta a None prima per evitare race condition try: # Shutdown può aiutare a segnalare all'altro capo che stiamo chiudendo # SHUT_RDWR: Non inviare né ricevere più nulla sock.shutdown(socket.SHUT_RDWR) except OSError as e: # Ignora errori comuni come "socket not connected" se già disconnesso if e.errno != 107 and e.errno != 9: # ENOTCONN, EBADF self._handle_error(f"Errore durante socket.shutdown(): {e}") except Exception as e: # Catch other potential errors self._handle_error(f"Errore generico durante socket.shutdown(): {e}") finally: try: # print("Chiusura socket...") # Debug sock.close() except Exception as e: self._handle_error(f"Errore durante socket.close(): {e}") # Attendi che il thread di ricezione termini if self.receive_thread and self.receive_thread.is_alive(): # print(f"Attesa terminazione thread ricezione (timeout 3s)...") # Debug self.receive_thread.join(timeout=3.0) if self.receive_thread.is_alive(): print("Attenzione: Thread di ricezione non terminato nel timeout.") # else: # Debug # print("Thread ricezione terminato correttamente.") # Notifica la disconnessione tramite callback DOPO che tutto è chiuso self._handle_disconnect_internal(invoked_by_close=True) print("Chiusura client completata.") self._closing = False # Resetta il flag alla fine def _handle_error(self, message): """Gestisce gli errori, stampando e chiamando il callback on_error.""" # Stampa sempre l'errore per diagnostica print(f"\n! ERRORE Client: {message}", file=sys.stderr) if self.on_error: try: self.on_error(self, message) except Exception as cb_ex: print(f"Errore nell'eseguire callback on_error: {cb_ex}", file=sys.stderr) def _handle_disconnect_internal(self, invoked_by_close=False): """Logica interna per gestire la disconnessione, chiamata da close() o dal loop di ricezione.""" # Chiamato quando la connessione è persa (dal server) o chiusa (da noi) # Se non siamo già in fase di chiusura manuale, impostiamo lo stato was_already_closing = self._closing if not was_already_closing: self._closing = True # Mark as closing if disconnected unexpectedly self._stop_receive.set() # Signal receive loop just in case it's stuck # Chiamiamo il callback solo una volta e se esiste if self.on_disconnect: try: # Rimuoviamo il callback prima di chiamarlo per evitare chiamate ricorsive # se il callback stesso causa un errore che richiama questa funzione callback = self.on_disconnect # self.on_disconnect = None # Consider removing only if issues arise callback(self) except Exception as cb_ex: print(f"Errore nell'eseguire callback on_disconnect: {cb_ex}", file=sys.stderr) # Se la disconnessione non è stata causata da una chiamata a close(), # potremmo voler chiudere il socket qui per pulizia, ma close() lo fa già. # print(f"Gestita disconnessione interna. Era già in chiusura: {was_already_closing}") # Debug # --- END OF TCPClient CLASS --- # --- START OF MAIN APPLICATION LOGIC (from former main_clienttcp_TLV.py) --- # Callback functions def handle_message(client_instance, message): """Callback per quando arriva un messaggio dal server.""" # Stampa il messaggio ricevuto. Aggiunge newline prima per evitare sovrapposizioni # con il prompt di input(), ma potrebbe non essere perfetto. # \r cancella la linea corrente, utile se l'utente stava scrivendo. sys.stdout.write(f"\rServer: {message}\n> ") sys.stdout.flush() def handle_error(client_instance, message): """Callback per gestire errori riportati dal client.""" # Stampa su stderr. Aggiunge newline prima. sys.stderr.write(f"\r! Errore: {message}\n> ") sys.stderr.flush() def handle_connect(client_instance): """Callback per quando la connessione al server ha successo.""" sys.stdout.write(f"\rConnesso.\n> ") sys.stdout.flush() def handle_disconnect(client_instance): """Callback per quando il client si disconnette.""" # Questo viene chiamato DOPO che la connessione è terminata. sys.stdout.write(f"\rDisconnesso dal server.\n") sys.stdout.flush() # Nota: Non possiamo reinserire il prompt '>' qui perché il loop principale # potrebbe essere già terminato o in fase di terminazione. if __name__ == '__main__': # Configurazione del client SERVER_HOST = '127.0.0.1' SERVER_PORT = 5000 # Crea l'istanza del client client = TCPClient(host=SERVER_HOST, port=SERVER_PORT, timeout=10) # Timeout connessione 10s # Imposta i gestori degli eventi (callback) client.on_message = handle_message client.on_error = handle_error client.on_connect = handle_connect client.on_disconnect = handle_disconnect # Tenta la connessione if not client.connect(): print("Impossibile connettersi al server. Uscita.") sys.exit(1) # Loop principale per l'input utente running = True try: while running: # Controlla se siamo ancora connessi prima di chiedere input if not client.socket or client._closing: print("\nClient non più connesso. Uscita dal loop principale.") running = False break try: # Legge l'input dall'utente in modo bloccante # Il prompt '>' viene stampato dalle callback quando necessario command = input("> ") if not client.socket or client._closing: print("\nDisconnesso durante l'attesa di input. Uscita.") running = False break if command.strip().lower() == 'quit': print("Comando 'quit' ricevuto. Chiusura...") running = False # La chiusura verrà gestita nel blocco finally elif command: # Invia solo se l'input non è vuoto client.send(command) # Se l'input è vuoto, non fare nulla e richiedi di nuovo except EOFError: # L'input è stato chiuso (es. pipe terminata) print("\nInput chiuso (EOF). Uscita...") running = False except KeyboardInterrupt: # Utente ha premuto Ctrl+C print("\nInterruzione da tastiera (Ctrl+C). Uscita...") running = False finally: # Blocco eseguito sempre all'uscita del loop (normale o per eccezione) print("Pulizia finale e chiusura client...") if client: client.close() # Assicura che la connessione sia chiusa correttamente print("Programma client terminato.") # --- END OF MAIN APPLICATION LOGIC ---