All files

This commit is contained in:
2025-11-04 08:19:56 +01:00
commit 059d668618
9 changed files with 655 additions and 0 deletions

5
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,5 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/

10
.idea/TP1.iml generated Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.13 (TP1)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

7
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.13 (TP1)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13 (TP1)" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/TP1.iml" filepath="$PROJECT_DIR$/.idea/TP1.iml" />
</modules>
</component>
</project>

96
client.py Normal file
View File

@@ -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 <username> <region> <host> <port>")
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)

233
server.py Normal file
View File

@@ -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 <region> <host> <port> <config_file>")
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

138
servers_config.json Normal file
View File

@@ -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
}
]
}
]

152
start-servers.py Normal file
View File

@@ -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 <username> localhost <port>")
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!")