commit 059d668618de19be72f1b53dea75378c374f95d3 Author: LiRuZ Date: Tue Nov 4 08:19:56 2025 +0100 All files diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..b58b603 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,5 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/TP1.iml b/.idea/TP1.iml new file mode 100644 index 0000000..8519b69 --- /dev/null +++ b/.idea/TP1.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..16b71d4 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..0cc9f92 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/client.py b/client.py new file mode 100644 index 0000000..8f31f14 --- /dev/null +++ b/client.py @@ -0,0 +1,96 @@ +import socket +import sys +import threading +import json +from datetime import datetime, timezone + +def listen_to_server(sock): + buffer = "" + try: + while True: + data = sock.recv(4096) + if not data: + print("🔌 Connection closed by server.") + break + buffer += data.decode() + while '\n' in buffer: + line, buffer = buffer.split('\n', 1) + try: + msg = json.loads(line) + ts = msg.get("timestamp", "?") + sender = msg.get("sender", "?") + content = msg.get("content", "") + level = msg.get("level", "local") + print(f"[Received @ {ts}] {sender} ({level}): {content}") + except json.JSONDecodeError: + print("⚠Received invalid JSON.") + except Exception as e: + print("⚠Error receiving from server:", e) + finally: + sock.close() + print("Disconnected.") + sys.exit(0) + +if __name__ == "__main__": + if len(sys.argv) != 5: + print("Usage: python client.py ") + sys.exit(1) + + username = sys.argv[1] + region = sys.argv[2] + host = sys.argv[3] + port = int(sys.argv[4]) + + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect((host, port)) + except ConnectionRefusedError: + print(f"Connection refused to {host}:{port}. Is the server running?") + sys.exit(1) + except Exception as e: + print(f"Connection error: {e}") + sys.exit(1) + + print(f"Connected to server {host}:{port} as {username}") + print("Type messages. Use `::level=local|national|global` (default: local)") + + threading.Thread(target=listen_to_server, args=(sock,), daemon=True).start() + + try: + while True: + line = input() + if "::level=" in line: + parts = line.split("::level=") + content = parts[0].strip() + level = parts[1].strip().lower() + else: + content = line.strip() + level = "local" + + if not content: + continue + + timestamp = datetime.now(timezone.utc).isoformat() + message = { + "id": f"{username}-{timestamp}", + "sender": username, + "content": content, + "level": level, + "timestamp": timestamp, + "region": region + } + + try: + sock.sendall((json.dumps(message) + "\n").encode()) + except Exception: + print("Failed to send message. Disconnected?") + break + except KeyboardInterrupt: + print("\nClient closed.") + finally: + sock.close() + sys.exit(0) + + + + diff --git a/server.py b/server.py new file mode 100644 index 0000000..c3a3528 --- /dev/null +++ b/server.py @@ -0,0 +1,233 @@ +import socket +import threading +import json +import sys +import time +from datetime import datetime, timezone + + +class Server: + def __init__(self, region, host, port, config_file): + self.region = region + self.host = host + self.port = int(port) + self.config_file = config_file + + self.is_running = True + self.clients = [] + self.lock = threading.Lock() + self.neighbors = [] # neighbors: list of (region, host, port) + self.region_peers = [] # servers in the same region (host, port) + self.nation = None + self.seen_messages = set() + + self.load_config() + + def load_config(self): + with open(self.config_file, 'r') as f: + all_servers = json.load(f) + + # Find this region info + for region_info in all_servers: + if region_info["region"] == self.region: + self.nation = region_info.get("nation", None) + + # Collect region peers (all servers in this region) + for server in region_info.get("servers", []): + self.region_peers.append((server["host"], server["port"])) + + # Collect neighbors as (region, host, port) + for neighbor in region_info.get("neighbors", []): + h, p = neighbor.split(":") + # We need to find region of this neighbor host:port from config + neighbor_region = None + for r in all_servers: + for srv in r.get("servers", []): + if srv["host"] == h and int(p) == srv["port"]: + neighbor_region = r["region"] + break + if neighbor_region: + break + self.neighbors.append((neighbor_region, h, int(p))) + + break + + def start(self): + print(f"[{self.region}] Server started on port {self.port}") + threading.Thread(target=self.accept_clients, daemon=True).start() + threading.Thread(target=self.listen_for_servers, daemon=True).start() + + def accept_clients(self): + server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_socket.bind((self.host, self.port)) + server_socket.listen() + + while self.is_running: + try: + client_sock, _ = server_socket.accept() + with self.lock: + self.clients.append(client_sock) + threading.Thread(target=self.handle_client, args=(client_sock,), daemon=True).start() + except Exception as e: + print(f"[{self.region}] Accept client error: {e}") + + def handle_client(self, client_sock): + buffer = "" + while True: + try: + data = client_sock.recv(4096) + if not data: + break + buffer += data.decode() + while '\n' in buffer: + line, buffer = buffer.split('\n', 1) + message = json.loads(line) + self.process_message(message, from_client=True, client_sock=client_sock) + except Exception: + break + with self.lock: + if client_sock in self.clients: + self.clients.remove(client_sock) + client_sock.close() + + def listen_for_servers(self): + # Listen on port+1000 for server-server communication + server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_socket.bind((self.host, self.port + 1000)) + server_socket.listen() + while self.is_running: + try: + conn, _ = server_socket.accept() + threading.Thread(target=self.handle_server_connection, args=(conn,), daemon=True).start() + except Exception as e: + print(f"[{self.region}] ⚠️ Accept server error: {e}") + + def handle_server_connection(self, conn): + buffer = "" + while True: + try: + data = conn.recv(4096) + if not data: + break + buffer += data.decode() + while '\n' in buffer: + line, buffer = buffer.split('\n', 1) + message = json.loads(line) + self.process_message(message, from_client=False) + except Exception: + break + conn.close() + + def process_message(self, message, from_client=False, client_sock=None): + # Deduplicate messages + msg_id = message.get("id") + if msg_id in self.seen_messages: + return + self.seen_messages.add(msg_id) + + level = message.get("level", "local").lower() + sender_region = message.get("region") + timestamp = message.get("timestamp") + sender = message.get("sender") + content = message.get("content") + + print(f"[{self.region}] 📩 {level.upper()} message from {sender} @ {timestamp}: {content}") + + # Send to local clients except sender (if from client) + self._send_to_local_clients(message, exclude_sock=client_sock if from_client else None) + + # Propagate based on level + if level == "local": + # Forward to all region peers except self + for host, port in self.region_peers: + if host == self.host and port == self.port: + continue + self._send_to_server(host, port + 1000, message) + + elif level == "national": + if self.nation is None or sender_region is None: + return + + # Determine sender's nation from config + sender_nation = None + with open(self.config_file, 'r') as f: + all_servers = json.load(f) + for s in all_servers: + if s["region"] == sender_region: + sender_nation = s.get("nation", None) + break + if sender_nation != self.nation: + return + + # Forward to region peers + for host, port in self.region_peers: + if host == self.host and port == self.port: + continue + self._send_to_server(host, port + 1000, message) + + # Forward to neighbors with the same nation + for region, host, port in self.neighbors: + neighbor_nation = None + with open(self.config_file, 'r') as f: + all_servers = json.load(f) + for s in all_servers: + if s["region"] == region: + neighbor_nation = s.get("nation", None) + break + if neighbor_nation == self.nation: + self._send_to_server(host, port + 1000, message) + + elif level == "global": + # Forward to all region peers except self + for host, port in self.region_peers: + if host == self.host and port == self.port: + continue + self._send_to_server(host, port + 1000, message) + + # Forward to all neighbors + for region, host, port in self.neighbors: + self._send_to_server(host, port + 1000, message) + + def _send_to_local_clients(self, message, exclude_sock=None): + with self.lock: + to_remove = [] + for c in self.clients: + if c == exclude_sock: + continue + try: + c.sendall((json.dumps(message) + "\n").encode()) + except Exception: + to_remove.append(c) + for c in to_remove: + self.clients.remove(c) + c.close() + + def _send_to_server(self, host, port, message): + try: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.settimeout(2) + s.connect((host, port)) + s.sendall((json.dumps(message) + "\n").encode()) + except Exception: + pass + + +if __name__ == "__main__": + if len(sys.argv) != 5: + print("Usage: python server.py ") + sys.exit(1) + + region = sys.argv[1] + host = sys.argv[2] + port = sys.argv[3] + config_file = sys.argv[4] + + server = Server(region, host, port, config_file) + server.start() + + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + print(f"\n[{region}] Shutting down server...") + server.is_running = False \ No newline at end of file diff --git a/servers_config.json b/servers_config.json new file mode 100644 index 0000000..238672d --- /dev/null +++ b/servers_config.json @@ -0,0 +1,138 @@ +[ + { + "region": "World", + "nation": "World", + "host": "localhost", + "port": 9000, + "neighbors": [ + "localhost:8001", + "localhost:8004" + ], + "servers": [ + { + "host": "localhost", + "port": 9000 + } + ] + }, + { + "region": "FRA", + "nation": "France", + "host": "localhost", + "port": 8001, + "neighbors": [ + "localhost:9000", + "localhost:8002", + "localhost:8003", + "localhost:8101" + ], + "servers": [ + { + "host": "localhost", + "port": 8001 + }, + { + "host": "localhost", + "port": 8101 + } + ] + }, + { + "region": "Paris", + "nation": "France", + "host": "localhost", + "port": 8002, + "neighbors": [ + "localhost:8001", + "localhost:8101" + ], + "servers": [ + { + "host": "localhost", + "port": 8002 + }, + { + "host": "localhost", + "port": 8102 + } + ] + }, + { + "region": "Marseille", + "nation": "France", + "host": "localhost", + "port": 8003, + "neighbors": [ + "localhost:8001" + ], + "servers": [ + { + "host": "localhost", + "port": 8003 + }, + { + "host": "localhost", + "port": 8103 + } + ] + }, + { + "region": "USA", + "nation": "USA", + "host": "localhost", + "port": 8004, + "neighbors": [ + "localhost:9000", + "localhost:8005", + "localhost:8006" + ], + "servers": [ + { + "host": "localhost", + "port": 8004 + }, + { + "host": "localhost", + "port": 8104 + } + ] + }, + { + "region": "NYC", + "nation": "USA", + "host": "localhost", + "port": 8005, + "neighbors": [ + "localhost:8004" + ], + "servers": [ + { + "host": "localhost", + "port": 8005 + }, + { + "host": "localhost", + "port": 8105 + } + ] + }, + { + "region": "LA", + "nation": "USA", + "host": "localhost", + "port": 8006, + "neighbors": [ + "localhost:8004" + ], + "servers": [ + { + "host": "localhost", + "port": 8006 + }, + { + "host": "localhost", + "port": 8106 + } + ] + } +] \ No newline at end of file diff --git a/start-servers.py b/start-servers.py new file mode 100644 index 0000000..88c7311 --- /dev/null +++ b/start-servers.py @@ -0,0 +1,152 @@ +import subprocess +import time +import json +import sys +import os +import select + +servers_config = [ + { + "region": "World", + "nation": "World", + "host": "localhost", + "port": 9000, + "neighbors": ["localhost:8001", "localhost:8004"], + "servers": [ + {"host": "localhost", "port": 9000}, + ], + }, + { + "region": "FRA", + "nation": "France", + "host": "localhost", + "port": 8001, + "neighbors": ["localhost:9000", "localhost:8002", "localhost:8003", "localhost:8101"], + "servers": [ + {"host": "localhost", "port": 8001}, + {"host": "localhost", "port": 8101}, + ], + }, + { + "region": "Paris", + "nation": "France", + "host": "localhost", + "port": 8002, + "neighbors": ["localhost:8001", "localhost:8101"], + "servers": [ + {"host": "localhost", "port": 8002}, + {"host": "localhost", "port": 8102}, + ], + }, + { + "region": "Marseille", + "nation": "France", + "host": "localhost", + "port": 8003, + "neighbors": ["localhost:8001"], + "servers": [ + {"host": "localhost", "port": 8003}, + {"host": "localhost", "port": 8103}, + ], + }, + { + "region": "USA", + "nation": "USA", + "host": "localhost", + "port": 8004, + "neighbors": ["localhost:9000", "localhost:8005", "localhost:8006"], + "servers": [ + {"host": "localhost", "port": 8004}, + {"host": "localhost", "port": 8104}, + ], + }, + { + "region": "NYC", + "nation": "USA", + "host": "localhost", + "port": 8005, + "neighbors": ["localhost:8004"], + "servers": [ + {"host": "localhost", "port": 8005}, + {"host": "localhost", "port": 8105}, + ], + }, + { + "region": "LA", + "nation": "USA", + "host": "localhost", + "port": 8006, + "neighbors": ["localhost:8004"], + "servers": [ + {"host": "localhost", "port": 8006}, + {"host": "localhost", "port": 8106}, + ], + } +] + +config_filename = "servers_config.json" +with open(config_filename, "w") as f: + json.dump(servers_config, f, indent=2) + +processes = [] +server_script = os.path.abspath("server.py") + +for s in servers_config: + region = s["region"] + host = s["host"] + + for srv in s["servers"]: + port = srv["port"] + print(f"[{region}] Starting server on port {port} ...") + p = subprocess.Popen( + [sys.executable, server_script, region, host, str(port), config_filename], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + bufsize=1, + ) + processes.append((region, port, p)) + time.sleep(0.3) + +print("\nAll servers launched.") +print("Use: python client.py localhost ") +print("Regions:", ", ".join([s["region"] for s in servers_config])) + +try: + while True: + for region, port, p in list(processes): + while True: + ready, _, _ = select.select([p.stdout], [], [], 0) + if p.stdout in ready: + line = p.stdout.readline() + if not line: + break + print(f"[{region}:{port}] {line.strip()}") + else: + break + + while True: + ready, _, _ = select.select([p.stderr], [], [], 0) + if p.stderr in ready: + line = p.stderr.readline() + if not line: + break + print(f"[{region}:{port}][ERR] {line.strip()}") + else: + break + + if p.poll() is not None: + print(f"\n[{region}:{port}] Server exited.") + processes.remove((region, port, p)) + + if not processes: + print("All servers stopped.") + break + + time.sleep(0.1) + +except KeyboardInterrupt: + print("\nShutting down all servers...") + for _, _, p in processes: + p.terminate() + print("Bye!")