From 245a96ef79c688a18313141716358be51f2dc1ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= Date: Tue, 11 Mar 2025 14:58:34 +0100 Subject: [PATCH] add client-server architecture with GUI and message handling improvements --- .gitignore | 5 ++- TP10/10.1/client.py | 27 +++++++++--- TP10/10.2/client.py | 54 +++++++++++++++++++++++ TP10/10.2/gui.py | 103 ++++++++++++++++++++++++++++++++++++++++++++ TP10/10.2/server.py | 99 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 282 insertions(+), 6 deletions(-) create mode 100644 TP10/10.2/client.py create mode 100644 TP10/10.2/gui.py create mode 100644 TP10/10.2/server.py diff --git a/.gitignore b/.gitignore index ccaeb42..d3ea9f9 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,7 @@ .venv/* .winvenv -.winvenv/* \ No newline at end of file +.winvenv/* + +.macvenv +.macvenv/* \ No newline at end of file diff --git a/TP10/10.1/client.py b/TP10/10.1/client.py index 23aadf6..8c8d7d9 100644 --- a/TP10/10.1/client.py +++ b/TP10/10.1/client.py @@ -9,6 +9,7 @@ class Client: self.server_port = server_port self.socket = socket.socket() self.running = True + self.callbacks = [] # List of callback functions for received messages try: self.socket.connect((self.server_address, self.server_port)) @@ -24,47 +25,63 @@ class Client: except Exception as e: print(f"Error sending message: {e}") self.running = False + raise e def shutdown(self): """Shutdown the socket communication.""" try: self.running = False - self.socket.shutdown(2) + self.socket.shutdown(socket.SHUT_RDWR) except Exception as e: print(f"Error during shutdown: {e}") def close(self): """Close the connection and wait for listening thread to end.""" - if self.listening_thread: + if hasattr(self, "listening_thread"): self.listening_thread.join() try: self.socket.close() except Exception as e: print(f"Error closing socket: {e}") + def register_callback(self, callback): + """Register a callback function for received messages.""" + self.callbacks.append(callback) + def listen_messages(self): """Listen for incoming messages from the server.""" while self.running: try: msg = self.socket.recv(1024) if msg: - print(f"Message received: {msg.decode()}") + decoded_msg = msg.decode() + # Notify all registered callbacks + for callback in self.callbacks: + callback(f"{decoded_msg}\n") else: break except Exception as e: - print(f"Error receiving message: {e}") + if self.running: # Only print error if not shutting down + print(f"Error receiving message: {e}") break self.running = False def main(): + """Test client in console mode.""" client = Client("localhost", 6060) + def print_message(msg: str): + """Simple callback to print received messages.""" + print(f"Received: {msg}", end="") + + client.register_callback(print_message) + try: while client.running: message = input("Enter message (or Ctrl+C to quit): ") client.send(message) - if message.lower() == "stop": + if message.lower() == "quit": break except KeyboardInterrupt: print("\nClosing connection...") diff --git a/TP10/10.2/client.py b/TP10/10.2/client.py new file mode 100644 index 0000000..700ebe4 --- /dev/null +++ b/TP10/10.2/client.py @@ -0,0 +1,54 @@ +import socket +import threading + + +class Client: + def __init__(self, host, port): + """Initialize client and connect to server.""" + self.host = host + self.port = port + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.socket.connect((host, port)) + self.running = True + self.callback = None + + def register_callback(self, callback): + """Register a callback function to handle received messages. + + Args: + callback (callable): Function to call when messages are received + """ + self.callback = callback + # Start a thread to listen for incoming messages + threading.Thread(target=self.receive_messages, daemon=True).start() + + def receive_messages(self): + """Listen for incoming messages and process them.""" + while self.running: + try: + data = self.socket.recv(1024) + if data: + message = data.decode("utf-8") + if self.callback: + self.callback(message) + else: + # Empty data means server closed connection + if self.callback: + self.callback("Disconnected from server\n") + break + except Exception as e: + if self.running and self.callback: # Only show error if still running + self.callback(f"Error receiving message: {e}\n") + break + + def send(self, message): + """Send a message to the server.""" + self.socket.send(message.encode("utf-8")) + + def shutdown(self): + """Signal the receiving thread to stop.""" + self.running = False + + def close(self): + """Close the socket connection.""" + self.socket.close() diff --git a/TP10/10.2/gui.py b/TP10/10.2/gui.py new file mode 100644 index 0000000..b187ef7 --- /dev/null +++ b/TP10/10.2/gui.py @@ -0,0 +1,103 @@ +import tkinter as tk +from tkinter import ttk +from client import Client +import datetime + + +class ClientWindow: + def __init__(self, root: tk.Tk): + """Initialize the chat window.""" + self.root = root + self.root.title("Chat Client") + self.root.geometry("600x400") + + self.client = None + + # Server connection fields + self.address = tk.StringVar(value="localhost") + self.port = tk.StringVar(value="6060") + + # Address field + address_label = ttk.Label(root, text="Address:") + address_label.grid(column=0, row=0, padx=5, pady=5) + address_entry = ttk.Entry(root, textvariable=self.address) + address_entry.grid(column=1, row=0, padx=5, pady=5) + + # Port field + port_label = ttk.Label(root, text="Port:") + port_label.grid(column=2, row=0, padx=5, pady=5) + port_entry = ttk.Entry(root, textvariable=self.port) + port_entry.grid(column=3, row=0, padx=5, pady=5) + + # Connect button + self.connect_button = ttk.Button(root, text="Connect", command=self.connect) + self.connect_button.grid(column=4, row=0, padx=5, pady=5) + + # Messages display + self.text_area = tk.Text(root, wrap=tk.WORD, width=50, height=20) + self.text_area.grid(column=0, row=1, columnspan=5, padx=5, pady=5) + self.text_area.config(state="disabled") + + # Message input + self.message = tk.StringVar() + self.msg_entry = ttk.Entry(root, textvariable=self.message) + self.msg_entry.grid(column=0, row=2, columnspan=5, padx=5, pady=5, sticky="ew") + self.msg_entry.bind("", self.send) + + # Configure grid weights + root.grid_rowconfigure(1, weight=1) + root.grid_columnconfigure(0, weight=1) + + def connect(self): + """Connect to the server.""" + try: + self.client = Client(self.address.get(), int(self.port.get())) + if self.client: # Only register callback if client is created successfully + self.client.register_callback(self.append_text) + self.connect_button["text"] = "Disconnect" + self.connect_button["command"] = self.disconnect + self.append_text("Connected to server\n") + except Exception as e: + self.append_text(f"Connection error: {e}\n") + self.client = None # Reset client on failed connection + + def disconnect(self): + """Disconnect from the server.""" + if self.client: + self.client.shutdown() + self.client.close() + self.client = None + self.connect_button["text"] = "Connect" + self.connect_button["command"] = self.connect + self.append_text("Disconnected from server\n") + + def send(self, event=None): + """Send message to server.""" + if self.client and self.message.get(): + try: + self.client.send(self.message.get()) + self.message.set("") # Clear input field + except Exception as e: + self.append_text(f"Error sending message: {e}\n") + + def append_text(self, message: str): + """Append text to message display area with timestamp.""" + self.text_area.config(state="normal") + timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + # Ensure the message ends with a newline + if not message.endswith("\n"): + message += "\n" + + # Add timestamp and ensure double line break between messages + formatted_message = f"[{timestamp}] {message}\n" + + self.text_area.insert("end", formatted_message) + self.text_area.see("end") + self.text_area.config(state="disabled") + + +if __name__ == "__main__": + root = tk.Tk() + ClientWindow(root) + root.mainloop() diff --git a/TP10/10.2/server.py b/TP10/10.2/server.py new file mode 100644 index 0000000..36aff2c --- /dev/null +++ b/TP10/10.2/server.py @@ -0,0 +1,99 @@ +import socket +import threading + + +class ClientThread(threading.Thread): + def __init__(self, server, client_socket: socket.socket): + """Initialize client thread with server reference and socket.""" + super().__init__() + self.server = server + self.socket = client_socket + + def send(self, msg: str): + """Send message to the client.""" + try: + self.socket.send(msg.encode()) + except Exception as e: + print(f"Error sending message to client: {e}") + self.server.remove_client(self) + + def shutdown(self): + """Shutdown the client socket.""" + try: + self.socket.shutdown(socket.SHUT_RDWR) + except Exception as e: + print(f"Error shutting down client socket: {e}") + + def close(self): + """Close the client socket.""" + try: + self.socket.close() + except Exception as e: + print(f"Error closing client socket: {e}") + + def run(self): + """Read messages from client continuously.""" + try: + while True: + msg = self.socket.recv(1024) + if msg: + self.server.send_to_all(msg) + else: + break + except Exception as e: + print(f"Error receiving message: {e}") + finally: + self.server.remove_client(self) + self.close() + + +class Server: + def __init__(self, port: int = 12345): + """Initialize server with port.""" + self.port = port + self.socket = socket.socket() + self.clients = [] + + self.socket.bind(("", self.port)) + self.socket.listen() + + def run(self): + """Accept client connections continuously.""" + print("Server is listening for connections...") + try: + while True: + client_socket, addr = self.socket.accept() + print(f"Connection from {addr}") + client_thread = ClientThread(self, client_socket) + self.clients.append(client_thread) + client_thread.start() + except KeyboardInterrupt: + print("\nServer stopped") + finally: + for client in self.clients[:]: + client.shutdown() + client.close() + self.socket.close() + print("Server closed") + + def send_to_all(self, message: bytes): + """Send message to all connected clients.""" + disconnected_clients = [] + for client in self.clients: + try: + client.send(message.decode()) + except Exception: + disconnected_clients.append(client) + + for client in disconnected_clients: + self.remove_client(client) + + def remove_client(self, client: ClientThread): + """Remove client from clients list.""" + if client in self.clients: + self.clients.remove(client) + + +if __name__ == "__main__": + s = Server(6060) + s.run()