All files
This commit is contained in:
5
.idea/.gitignore
generated
vendored
Normal file
5
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
10
.idea/TP1.iml
generated
Normal file
10
.idea/TP1.iml
generated
Normal 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>
|
||||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal 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
7
.idea/misc.xml
generated
Normal 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
8
.idea/modules.xml
generated
Normal 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
96
client.py
Normal 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
233
server.py
Normal 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
138
servers_config.json
Normal 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
152
start-servers.py
Normal 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!")
|
||||||
Reference in New Issue
Block a user