1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-12-24 22:14:36 +02:00

Update proxy server

This commit is contained in:
nordsoft 2022-11-09 20:25:06 +04:00
parent f83e4f4ce4
commit 45129c5cda

View File

@ -11,14 +11,7 @@ PROTOCOL_VERSION_MAX = 1
SERVER_HOST = "0.0.0.0" SERVER_HOST = "0.0.0.0"
SERVER_PORT = 5002 # port we want to use SERVER_PORT = 5002 # port we want to use
def send_msg(sock, msg, doPack): def receive_packed(sock):
# For 1 byte (bool) send just that
# Prefix each message with a 4-byte length (network byte order)
if doPack and len(msg) > 1:
msg = struct.pack('<I', len(msg)) + msg
sock.sendall(msg)
def recv_msg(sock):
# Read message length and unpack it into an integer # Read message length and unpack it into an integer
raw_msglen = recvall(sock, 4) raw_msglen = recvall(sock, 4)
if not raw_msglen: if not raw_msglen:
@ -37,9 +30,6 @@ def recvall(sock, n):
data.extend(packet) data.extend(packet)
return data return data
# initialize list/set of all connected client's sockets
client_sockets = dict()
class GameConnection: class GameConnection:
server: socket # socket to vcmiserver server: socket # socket to vcmiserver
@ -52,17 +42,16 @@ class GameConnection:
self.client = None self.client = None
pass pass
class Session:
class Room:
total = 1 # total amount of players total = 1 # total amount of players
joined = 0 # amount of players joined to the session joined = 0 # amount of players joined to the session
password = "" # password to connect password = "" # password to connect
protected = False # if True, password is required to join to the session protected = False # if True, password is required to join to the session
name: str # name of session name: str # name of session
host: socket # player socket who created the session (lobby mode) host: socket # player socket who created the room
token: str # uuid of vcmiserver for hosting player
players = [] # list of sockets of players, joined to the session players = [] # list of sockets of players, joined to the session
connections = [] # list of GameConnections for vcmiclient (game mode) started = False
started = False # True - game mode, False - lobby mode
def __init__(self, host: socket, name: str) -> None: def __init__(self, host: socket, name: str) -> None:
self.name = name self.name = name
@ -85,6 +74,22 @@ class Session:
self.players.remove(player) self.players.remove(player)
self.joined -= 1 self.joined -= 1
class Session:
name: str # name of session
host_uuid: str # uuid of vcmiserver for hosting player
clients_uuid: list # list od vcmiclients uuid
players: list # list of sockets of players, joined to the session
connections: list # list of GameConnections for vcmiclient/vcmiserver (game mode)
def __init__(self) -> None:
self.name = ""
self.host_uuid = ""
self.clients_uuid = []
self.players = []
self.connections = []
pass
def addConnection(self, conn: socket, isServer: bool): def addConnection(self, conn: socket, isServer: bool):
#find uninitialized server connection #find uninitialized server connection
for gc in self.connections: for gc in self.connections:
@ -119,6 +124,62 @@ class Session:
return gc.server return gc.server
class Client:
auth: bool
def __init__(self) -> None:
self.auth = False
class ClientLobby(Client):
joined: bool
username: str
room: Room
protocolVersion: int
encoding: str
ready: bool
vcmiversion: str #TODO: check version compatibility
def __init__(self) -> None:
super().__init__()
self.room = None
self.joined = False
self.username = ""
self.protocolVersion = 0
self.encoding = 'utf8'
self.ready = False
self.vcmiversion = ""
class ClientPipe(Client):
apptype: str #client/server
prevmessages: list
session: Session
uuid: str
def __init__(self) -> None:
super().__init__()
self.prevmessages = []
self.session = None
self.apptype = ""
self.uuid = ""
class Sender:
address: str #full client address
client: Client
def __init__(self) -> None:
self.client = None
pass
def isLobby(self) -> bool:
return isinstance(self.client, ClientLobby)
def isPipe(self) -> bool:
return isinstance(self.client, ClientPipe)
# create a TCP socket # create a TCP socket
s = socket.socket() s = socket.socket()
# make the port as reusable port # make the port as reusable port
@ -129,45 +190,57 @@ s.bind((SERVER_HOST, SERVER_PORT))
s.listen(10) s.listen(10)
print(f"[*] Listening as {SERVER_HOST}:{SERVER_PORT}") print(f"[*] Listening as {SERVER_HOST}:{SERVER_PORT}")
# active rooms
rooms = {}
# list of active sessions # list of active sessions
sessions = dict() sessions = []
# initialize list/set of all connected client's sockets
client_sockets = {}
def handleDisconnection(client: socket): def handleDisconnection(client: socket):
if not client in client_sockets:
return
sender = client_sockets[client] sender = client_sockets[client]
if sender["joined"]: if sender.isLobby() and sender.client.joined:
if not sender["session"].started: if not sender.client.room.started:
if sender["session"].host == client: if sender.client.room.host == client:
#destroy the session, sending messages inside the function #destroy the session, sending messages inside the function
deleteSession(sender["session"]) deleteRoom(sender.client.room)
else: else:
sender["session"].leave(client) sender.client.room.leave(client)
sender["joined"] = False sender.client.joined = False
message = f":>>KICK:{sender['session'].name}:{sender['username']}" message = f":>>KICK:{sender.client.room.name}:{sender.client.username}"
for client_socket in sender["session"].players: broadcast(sender.client.room.players, message.encode())
client_socket.send(message.encode()) updateStatus(sender.client.room)
updateStatus(sender["session"]) updateRooms()
updateSessions()
if sender.isPipe():
pass
client.close() client.close()
sender["valid"] = False client_sockets.pop(client)
def send(client: socket, message: str): def send(client: socket, message: str):
if client in client_sockets.keys():
sender = client_sockets[client] sender = client_sockets[client]
if "valid" not in sender or sender["valid"]:
client.send(message.encode(errors='replace')) client.send(message.encode(errors='replace'))
def broadcast(clients: list, message: str): def broadcast(clients: list, message: str):
for c in clients: for c in clients:
if client_sockets[c].isLobby() and client_sockets[c].client.auth:
send(c, message) send(c, message)
def sendSessions(client: socket): def sendRooms(client: socket):
msg2 = "" msg2 = ""
counter = 0 counter = 0
for s in sessions.values(): for s in rooms.values():
if not s.started: if not s.started:
msg2 += f":{s.name}:{s.joined}:{s.total}:{s.protected}" msg2 += f":{s.name}:{s.joined}:{s.total}:{s.protected}"
counter += 1 counter += 1
@ -176,158 +249,166 @@ def sendSessions(client: socket):
send(client, msg) send(client, msg)
def updateSessions(): def updateRooms():
for s in client_sockets.keys(): for s in client_sockets.keys():
sendSessions(s) sendRooms(s)
def deleteSession(session: Session): def deleteRoom(room: Room):
msg = f":>>KICK:{session.name}" msg = f":>>KICK:{room.name}"
for player in session.players: for player in room.players:
client_sockets[player]["joined"] = False client_sockets[player].client.joined = False
msg2 = msg + f":{client_sockets[player]['username']}" msg2 = msg + f":{client_sockets[player].client.username}"
send(player, msg2) send(player, msg2)
sessions.pop(session.name) rooms.pop(room.name)
def updateStatus(session: Session): def updateStatus(room: Room):
msg = f":>>STATUS:{session.joined}" msg = f":>>STATUS:{room.joined}"
for player in session.players: for player in room.players:
msg += f":{client_sockets[player]['username']}:{client_sockets[player]['ready']}" msg += f":{client_sockets[player].client.username}:{client_sockets[player].client.ready}"
broadcast(session.players, msg) broadcast(room.players, msg)
def startSession(session: Session): def startRoom(room: Room):
session.started = True room.started = True
session = Session()
session.name = room.name
sessions.append(session)
session.host_uuid = str(uuid.uuid4()) session.host_uuid = str(uuid.uuid4())
hostMessage = f":>>HOST:{session.host_uuid}:{session.joined - 1}" #one client will be connected locally hostMessage = f":>>HOST:{session.host_uuid}:{room.joined - 1}" #one client will be connected locally
#host message must be before start message #host message must be before start message
send(session.host, hostMessage) send(room.host, hostMessage)
for player in session.players: for player in room.players:
client_sockets[player]['uuid'] = str(uuid.uuid4()) _uuid = str(uuid.uuid4())
msg = f":>>START:{client_sockets[player]['uuid']}" session.clients_uuid.append(_uuid)
msg = f":>>START:{_uuid}"
send(player, msg) send(player, msg)
#remove this connection
player.close
client_sockets.pop(player)
#this room shall not exist anymore
rooms.pop(room.name)
def dispatch(client: socket, sender: dict, arr: bytes): def dispatch(cs: socket, sender: Sender, arr: bytes):
if arr == None or len(arr) == 0: if arr == None or len(arr) == 0:
return return
print(f"[{sender['address']}] dispatching message")
#check for game mode connection #check for game mode connection
msg = str(arr) msg = str(arr)
if msg.find("Aiya!") != -1: if msg.find("Aiya!") != -1:
sender["pipe"] = True #switch to pipe mode sender.client = ClientPipe()
print(" vcmi recognized") print(" vcmi recognized")
if sender["pipe"]: if sender.isPipe():
if sender["game"]: #if already playing - sending raw bytes as is if sender.client.auth: #if already playing - sending raw bytes as is
sender["prevmessages"].append(arr) sender.client.prevmessages.append(arr)
print(" storing message") print(" storing message")
else: else:
sender["prevmessages"].append(struct.pack('<I', len(arr)) + arr) #pack message sender.client.prevmessages.append(struct.pack('<I', len(arr)) + arr) #pack message
print(" packing message") print(" packing message")
#search fo application type in the message #search fo application type in the message
match = re.search(r"\((\w+)\)", msg) match = re.search(r"\((\w+)\)", msg)
_appType = '' _appType = ''
if match != None: if match != None:
_appType = match.group(1) _appType = match.group(1)
sender["apptype"] = _appType sender.client.apptype = _appType
#extract uuid from message #extract uuid from message
_uuid = arr.decode() _uuid = arr.decode()
print(f" decoding {_uuid}") print(f" decoding {_uuid}")
if not _uuid == '' and not sender["apptype"] == '': if not _uuid == '' and not sender.client.apptype == '':
#search for uuid #search for uuid
for session in sessions.values(): for session in sessions:
if session.started:
#verify uuid of connected application #verify uuid of connected application
if _uuid.find(session.host_uuid) != -1 and sender["apptype"] == "server": if _uuid.find(session.host_uuid) != -1 and sender.client.apptype == "server":
print(f" apptype {sender['apptype']} uuid {_uuid}") print(f" apptype {sender.client.apptype} uuid {_uuid}")
session.addConnection(client, True) session.addConnection(cs, True)
sender["session"] = session sender.client.session = session
sender["game"] = True sender.client.auth = True
#read boolean flag for the endian #read boolean flag for the endian
# this is workaround to send only one remaining byte # this is workaround to send only one remaining byte
# WARNING: reversed byte order is not supported # WARNING: reversed byte order is not supported
sender["prevmessages"].append(client.recv(1)) sender.client.prevmessages.append(cs.recv(1))
print(f" binding server connection to session {session.name}") print(f" binding server connection to session {session.name}")
return return
if sender["apptype"] == "client": if sender.client.apptype == "client":
for p in session.players: for p in session.clients_uuid:
if _uuid.find(client_sockets[p]["uuid"]) != -1: if _uuid.find(p) != -1:
print(f" apptype {sender['apptype']} uuid {_uuid}") print(f" apptype {sender.client.apptype} uuid {_uuid}")
#client connection #client connection
session.addConnection(client, False) session.addConnection(cs, False)
sender["session"] = session sender.client.session = session
sender["game"] = True sender.client.auth = True
#read boolean flag for the endian #read boolean flag for the endian
# this is workaround to send only one remaining byte # this is workaround to send only one remaining byte
# WARNING: reversed byte order is not supported # WARNING: reversed byte order is not supported
sender["prevmessages"].append(client.recv(1)) sender.client.prevmessages.append(cs.recv(1))
print(f" binding client connection to session {session.name}") print(f" binding client connection to session {session.name}")
break break
#game mode #game mode
if sender["pipe"] and sender["game"] and sender["session"].validPipe(client): if sender.isPipe() and sender.client.auth and sender.client.session.validPipe(cs):
print(f" pipe for {sender['session'].name}")
#send messages from queue #send messages from queue
opposite = sender["session"].getPipe(client) opposite = sender.client.session.getPipe(cs)
for x in client_sockets[opposite]["prevmessages"]: for x in client_sockets[opposite].client.prevmessages:
client.sendall(x) cs.sendall(x)
client_sockets[opposite]["prevmessages"].clear() client_sockets[opposite].client.prevmessages.clear()
try: try:
for x in sender["prevmessages"]: for x in sender.client.prevmessages:
opposite.sendall(x) opposite.sendall(x)
except Exception as e: except Exception as e:
print(f"[!] Error: {e}") print(f"[!] Error: {e}")
#TODO: handle disconnection #TODO: handle disconnection
sender["prevmessages"].clear() sender.client.prevmessages.clear()
return return
#we are in pipe mode but game still not started - waiting other clients to connect #we are in pipe mode but game still not started - waiting other clients to connect
if sender["pipe"]: if sender.isPipe():
print(f" waiting other clients") print(f" waiting other clients")
return return
#lobby mode #intialize lobby mode
if not sender["auth"]: if not sender.isLobby():
if len(arr) < 2: if len(arr) < 2:
print("[!] Error: unknown client tries to connect") print("[!] Error: unknown client tries to connect")
#TODO: block address? close the socket? #TODO: block address? close the socket?
return return
sender.client = ClientLobby()
# first byte is protocol version # first byte is protocol version
sender["protocol_version"] = arr[0] sender.client.protocolVersion = arr[0]
if arr[0] < PROTOCOL_VERSION_MIN or arr[0] > PROTOCOL_VERSION_MAX: if arr[0] < PROTOCOL_VERSION_MIN or arr[0] > PROTOCOL_VERSION_MAX:
print(f"[!] Error: client has incompatbile protocol version {arr[0]}") print(f"[!] Error: client has incompatbile protocol version {arr[0]}")
send(client, ":>>ERROR:Cannot connect to remote server due to protocol incompatibility") send(cs, ":>>ERROR:Cannot connect to remote server due to protocol incompatibility")
return return
# second byte is a encoding str size # second byte is an encoding str size
if arr[1] == 0: if arr[1] == 0:
sender["encoding"] = "utf8" sender.client.encoding = "utf8"
else: else:
if len(arr) < arr[1] + 2: if len(arr) < arr[1] + 2:
send(client, ":>>ERROR:Protocol error") send(cs, ":>>ERROR:Protocol error")
return return
# read encoding string
sender["encoding"] = arr[2:(arr[1] + 2)].decode(errors='ignore') sender.client.encoding = arr[2:(arr[1] + 2)].decode(errors='ignore')
arr = arr[(arr[1] + 2):] arr = arr[(arr[1] + 2):]
msg = str(arr) msg = str(arr)
msg = arr.decode(encoding=sender["encoding"], errors='replace') msg = arr.decode(encoding=sender.client.encoding, errors='replace')
_open = msg.partition('<') _open = msg.partition('<')
_close = _open[2].partition('>') _close = _open[2].partition('>')
if _open[0] != '' or _open[1] == '' or _open[2] == '' or _close[0] == '' or _close[1] == '': if _open[0] != '' or _open[1] == '' or _open[2] == '' or _close[0] == '' or _close[1] == '':
print(f"[!] Incorrect message from {sender['address']}: {msg}") print(f"[!] Incorrect message from {sender.address}: {msg}")
return return
_nextTag = _close[2].partition('<') _nextTag = _close[2].partition('<')
@ -336,127 +417,128 @@ def dispatch(client: socket, sender: dict, arr: bytes):
#greetings to the server #greetings to the server
if tag == "GREETINGS": if tag == "GREETINGS":
if sender["auth"]: if sender.client.auth:
print(f"[!] Greetings from authorized user {sender['username']} {sender['address']}") print(f"[!] Greetings from authorized user {sender.client.username} {sender.address}")
send(client, ":>>ERROR:User already authorized") send(cs, ":>>ERROR:User already authorized")
return return
if len(tag_value) < 3: if len(tag_value) < 3:
send(client, f":>>ERROR:Too short username {tag_value}") send(cs, f":>>ERROR:Too short username {tag_value}")
return return
for user in client_sockets.values(): for user in client_sockets.values():
if user['username'] == tag_value: if user.isLobby() and user.client.username == tag_value:
send(client, f":>>ERROR:Can't connect with the name {tag_value}. This login is already occpupied") send(cs, f":>>ERROR:Can't connect with the name {tag_value}. This login is already occpupied")
return return
print(f"[*] User {sender['address']} autorized as {tag_value}") print(f"[*] {sender.address} autorized as {tag_value}")
sender["username"] = tag_value sender.client.username = tag_value
sender["auth"] = True sender.client.auth = True
sender["joined"] = False sendRooms(cs)
sendSessions(client)
#VCMI version received #VCMI version received
if tag == "VER" and sender["auth"]: if tag == "VER" and sender.client.auth:
print(f"[*] User {sender['username']} has version {tag_value}") print(f"[*] User {sender.client.username} has version {tag_value}")
sender.client.vcmiversion = tag_value
#message received #message received
if tag == "MSG" and sender["auth"]: if tag == "MSG" and sender.client.auth:
message = f":>>MSG:{sender['username']}:{tag_value}" message = f":>>MSG:{sender.client.username}:{tag_value}"
if sender["joined"]: if sender.client.joined:
broadcast(sender["session"].players, message) broadcast(sender.client.room.players, message) #send message only to players in the room
else: else:
broadcast(client_sockets.keys(), message) targetClients = [i for i in client_sockets.keys() if not client_sockets[i].client.joined]
broadcast(targetClients, message)
#new session #new room
if tag == "NEW" and sender["auth"] and not sender["joined"]: if tag == "NEW" and sender.client.auth and not sender.client.joined:
if tag_value in sessions: if tag_value in rooms:
#refuse creating game #refuse creating game
message = f":>>ERROR:Cannot create session with name {tag_value}, session with this name already exists" message = f":>>ERROR:Cannot create session with name {tag_value}, session with this name already exists"
send(client, message) send(cs, message)
return return
sessions[tag_value] = Session(client, tag_value) rooms[tag_value] = Room(cs, tag_value)
sender["joined"] = True sender.client.joined = True
sender["ready"] = False sender.client.ready = False
sender["session"] = sessions[tag_value] sender.client.room = rooms[tag_value]
#set password for the session #set password for the session
if tag == "PSWD" and sender["auth"] and sender["joined"] and sender["session"].host == client: if tag == "PSWD" and sender.client.auth and sender.client.joined and sender.client.room.host == cs:
sender["session"].password = tag_value sender.client.room.password = tag_value
sender["session"].protected = tag_value != "" sender.client.room.protected = bool(tag_value != "")
#set amount of players to the new session #set amount of players to the new room
if tag == "COUNT" and sender["auth"] and sender["joined"] and sender["session"].host == client: if tag == "COUNT" and sender.client.auth and sender.client.joined and sender.client.room.host == cs:
if sender["session"].total != 1: if sender.client.room.total != 1:
#refuse changing amount of players #refuse changing amount of players
message = f":>>ERROR:Changing amount of players is not possible for existing session" message = f":>>ERROR:Changing amount of players is not possible for existing session"
send(client, message) send(cs, message)
return return
sender["session"].total = int(tag_value) sender.client.room.total = int(tag_value)
message = f":>>CREATED:{sender['session'].name}" message = f":>>CREATED:{sender.client.room.name}"
send(client, message) send(cs, message)
#now session is ready to be broadcasted #now room is ready to be broadcasted
message = f":>>JOIN:{sender['session'].name}:{sender['username']}" message = f":>>JOIN:{sender.client.room.name}:{sender.client.username}"
send(client, message) send(cs, message)
updateStatus(sender["session"]) updateStatus(sender.client.room)
updateSessions() updateRooms()
#join session #join session
if tag == "JOIN" and sender["auth"] and not sender["joined"]: if tag == "JOIN" and sender.client.auth and not sender.client.joined:
if tag_value not in sessions: if tag_value not in rooms:
message = f":>>ERROR:Session with name {tag_value} doesn't exist" message = f":>>ERROR:Room with name {tag_value} doesn't exist"
send(client, message) send(cs, message)
return return
if sessions[tag_value].joined >= sessions[tag_value].total: if rooms[tag_value].joined >= rooms[tag_value].total:
message = f":>>ERROR:Session {tag_value} is full" message = f":>>ERROR:Room {tag_value} is full"
send(client, message) send(cs, message)
return return
if sessions[tag_value].started: if rooms[tag_value].started:
message = f":>>ERROR:Session {tag_value} is started" message = f":>>ERROR:Session {tag_value} is started"
send(client, message) send(cs, message)
return return
sender["joined"] = True sender.client.joined = True
sender["ready"] = False sender.client.ready = False
sender["session"] = sessions[tag_value] sender.client.room = rooms[tag_value]
if tag == "PSWD" and sender["auth"] and sender["joined"] and sender["session"].host != client: if tag == "PSWD" and sender.client.auth and sender.client.joined and sender.client.room.host != cs:
if not sender["session"].protected or sender["session"].password == tag_value: if not sender.client.room.protected or sender.client.room.password == tag_value:
sender["session"].join(client) sender.client.room.join(cs)
message = f":>>JOIN:{sender['session'].name}:{sender['username']}" message = f":>>JOIN:{sender.client.room.name}:{sender.client.username}"
broadcast(sender["session"].players, message) broadcast(sender.client.room.players, message)
updateStatus(sender["session"]) updateStatus(sender.client.room)
updateSessions() updateRooms()
else: else:
sender["joined"] = False sender.client.joined = False
message = f":>>ERROR:Incorrect password" message = f":>>ERROR:Incorrect password"
send(client, message) send(cs, message)
return return
#leaving session #leaving session
if tag == "LEAVE" and sender["auth"] and sender["joined"] and sender["session"].name == tag_value: if tag == "LEAVE" and sender.client.auth and sender.client.joined and sender.client.room.name == tag_value:
if sender["session"].host == client: if sender.client.room.host == cs:
#destroy the session, sending messages inside the function #destroy the session, sending messages inside the function
deleteSession(sender["session"]) deleteRoom(sender.client.room)
else: else:
message = f":>>KICK:{sender['session'].name}:{sender['username']}" message = f":>>KICK:{sender.client.room.name}:{sender.client.username}"
broadcast(sender["session"].players, message) broadcast(sender.client.room.players, message)
sender["session"].leave(client) sender.client.room.leave(cs)
sender["joined"] = False sender.client.joined = False
updateStatus(sender["session"]) updateStatus(sender.client.room)
updateSessions() updateRooms()
if tag == "READY" and sender["auth"] and sender["joined"] and sender["session"].name == tag_value: if tag == "READY" and sender.client.auth and sender.client.joined and sender.client.room.name == tag_value:
if sender["session"].joined > 0 and sender["session"].host == client: if sender.client.room.joined > 0 and sender.client.room.host == cs:
startSession(sender["session"]) startRoom(sender.client.room)
updateSessions() updateRooms()
dispatch(client, sender, (_nextTag[1] + _nextTag[2]).encode()) dispatch(cs, sender, (_nextTag[1] + _nextTag[2]).encode())
def listen_for_client(cs): def listen_for_client(cs):
@ -467,25 +549,32 @@ def listen_for_client(cs):
while True: while True:
try: try:
# keep listening for a message from `cs` socket # keep listening for a message from `cs` socket
if client_sockets[cs]["game"]: if client_sockets[cs].isPipe() and client_sockets[cs].client.auth:
msg = cs.recv(4096) msg = cs.recv(4096)
else: else:
msg = recv_msg(cs) msg = receive_packed(cs)
if msg == None or msg == b'':
print(f"[!] Disconnecting client {cs}")
handleDisconnection(cs)
return
dispatch(cs, client_sockets[cs], msg)
except Exception as e: except Exception as e:
# client no longer connected # client no longer connected
print(f"[!] Error: {e}") print(f"[!] Error: {e}")
handleDisconnection(cs) handleDisconnection(cs)
return return
dispatch(cs, client_sockets[cs], msg)
while True: while True:
# we keep listening for new connections all the time # we keep listening for new connections all the time
client_socket, client_address = s.accept() client_socket, client_address = s.accept()
print(f"[+] {client_address} connected.") print(f"[+] {client_address} connected.")
# add the new connected client to connected sockets # add the new connected client to connected sockets
client_sockets[client_socket] = {"address": client_address, "auth": False, "username": "", "joined": False, "game": False, "pipe": False, "apptype": "", "prevmessages": []} client_sockets[client_socket] = Sender()
client_sockets[client_socket].address = client_address
# start a new thread that listens for each client's messages # start a new thread that listens for each client's messages
t = Thread(target=listen_for_client, args=(client_socket,)) t = Thread(target=listen_for_client, args=(client_socket,))
# make the thread daemon so it ends whenever the main thread ends # make the thread daemon so it ends whenever the main thread ends