def run(self, port): global SHUTDOWN sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) binding_host = "" sock.bind((binding_host, port)) sock.setblocking(0) sock.listen(1000) logger.info("Listening on port % s", port) while True: try: timeout = 0.1 # Check for a new client every 10th of a second readable, _, _ = select.select([sock], [], [], timeout) if len(readable) > 0: client_socket, client_address = sock.accept() client_socket = Socket(client_socket) client_socket.set_bandwidth(self.bandwidth, self.bandwidth) connection = Connection(self, client_socket, client_address) connection.latency = self.latency with self._mutex: self._connections[connection.unique_id] = connection connection.start() logger.info(f"New connection from {client_address}") self.broadcast_client_update(connection, connection.client_attributes()) except KeyboardInterrupt: break logger.info("Shutting down server") SHUTDOWN = True sock.close()
def connect(self): if self.is_connected(): raise RuntimeError("Client.connect : already connected") try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.socket = Socket(sock) self.socket.connect((self.host, self.port)) local_address = self.socket.getsockname() logger.info( "Connecting from local %s:%s to %s:%s", local_address[0], local_address[1], self.host, self.port, ) self.send_command(common.Command(common.MessageType.CLIENT_ID)) self.send_command(common.Command(common.MessageType.LIST_CLIENTS)) self.send_command(common.Command(common.MessageType.LIST_ROOMS)) except ConnectionRefusedError: self.socket = None except common.ClientDisconnectedException: self.handle_connection_lost() except Exception as e: logger.error("Connection error %s", e, exc_info=True) self.socket = None raise
def recv(socket: Socket, size: int): """ Try to read size bytes from the socket. Raise ClientDisconnectedException if the socket is disconnected. """ result = b"" while size != 0: r, _, _ = select.select([socket], [], [], 0.1) if len(r) > 0: try: tmp = socket.recv(size) except (ConnectionAbortedError, ConnectionResetError) as e: logger.warning(e) raise ClientDisconnectedException() if len(tmp) == 0: raise ClientDisconnectedException() result += tmp size -= len(tmp) return result
class Client: """ The client class is responsible for: - handling the connection with the server - receiving packet of bytes and convert them to commands - send commands - maintain an updated view of clients and room states from server's inputs """ def __init__(self, host: str = common.DEFAULT_HOST, port: int = common.DEFAULT_PORT): self.host = host self.port = port self.pending_commands: List[common.Command] = [] self.socket: Socket = None self.client_id: Optional[str] = None # Will be filled with a unique string identifying this client self.current_custom_attributes: Dict[str, Any] = {} self.clients_attributes: Dict[str, Dict[str, Any]] = {} self.rooms_attributes: Dict[str, Dict[str, Any]] = {} self.current_room: Optional[str] = None def __del__(self): if self.socket is not None: self.disconnect() def __enter__(self): self.connect() return self def __exit__(self, *args): if self.is_connected(): self.disconnect() def connect(self): if self.is_connected(): raise RuntimeError("Client.connect : already connected") try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.socket = Socket(sock) self.socket.connect((self.host, self.port)) local_address = self.socket.getsockname() logger.info( "Connecting from local %s:%s to %s:%s", local_address[0], local_address[1], self.host, self.port, ) self.send_command(common.Command(common.MessageType.CLIENT_ID)) self.send_command(common.Command(common.MessageType.LIST_CLIENTS)) self.send_command(common.Command(common.MessageType.LIST_ROOMS)) except ConnectionRefusedError: self.socket = None except common.ClientDisconnectedException: self.handle_connection_lost() except Exception as e: logger.error("Connection error %s", e, exc_info=True) self.socket = None raise def disconnect(self): if self.socket: self.socket.shutdown(socket.SHUT_RDWR) self.socket.close() self.socket = None def is_connected(self): return self.socket is not None def add_command(self, command: common.Command): self.pending_commands.append(command) def handle_connection_lost(self): logger.info("Connection lost for %s:%s", self.host, self.port) # Set socket to None before putting CONNECTION_LIST message to avoid sending/reading new messages self.socket = None def wait(self, message_type: MessageType) -> bool: """ Wait for a command of a given message type, the remaining commands are ignored. Usually message_type is LEAVING_ROOM. """ while self.is_connected(): try: received_commands = self.fetch_incoming_commands() except common.ClientDisconnectedException: self.handle_connection_lost() break for command in received_commands: if command.type == message_type: return True return False def send_command(self, command: common.Command): try: common.write_message(self.socket, command) return True except common.ClientDisconnectedException: self.handle_connection_lost() return False def join_room(self, room_name: str): return self.send_command(common.Command(common.MessageType.JOIN_ROOM, room_name.encode("utf8"), 0)) def leave_room(self, room_name: str): self.current_room = None return self.send_command(common.Command(common.MessageType.LEAVE_ROOM, room_name.encode("utf8"), 0)) def delete_room(self, room_name: str): return self.send_command(common.Command(common.MessageType.DELETE_ROOM, room_name.encode("utf8"), 0)) def set_client_attributes(self, attributes: dict): diff = update_attributes_and_get_diff(self.current_custom_attributes, attributes) if diff == {}: return True return self.send_command( common.Command(common.MessageType.SET_CLIENT_CUSTOM_ATTRIBUTES, common.encode_json(diff), 0) ) def set_room_attributes(self, room_name: str, attributes: dict): return self.send_command(common.make_set_room_attributes_command(room_name, attributes)) def send_list_rooms(self): return self.send_command(common.Command(common.MessageType.LIST_ROOMS)) def set_room_keep_open(self, room_name: str, value: bool): return self.send_command( common.Command( common.MessageType.SET_ROOM_KEEP_OPEN, common.encode_string(room_name) + common.encode_bool(value), 0 ) ) def _handle_list_client(self, command: common.Command): clients_attributes, _ = common.decode_json(command.data, 0) update_named_attributes(self.clients_attributes, clients_attributes) def _handle_list_rooms(self, command: common.Command): rooms_attributes, _ = common.decode_json(command.data, 0) update_named_attributes(self.rooms_attributes, rooms_attributes) def _handle_client_id(self, command: common.Command): self.client_id = command.data.decode() def _handle_room_update(self, command: common.Command): rooms_attributes_update, _ = common.decode_json(command.data, 0) update_named_attributes(self.rooms_attributes, rooms_attributes_update) def _handle_room_deleted(self, command: common.Command): room_name, _ = common.decode_string(command.data, 0) if room_name not in self.rooms_attributes: logger.warning("Room %s deleted but no attributes in internal view.", room_name) return del self.rooms_attributes[room_name] def _handle_client_update(self, command: common.Command): clients_attributes_update, _ = common.decode_json(command.data, 0) update_named_attributes(self.clients_attributes, clients_attributes_update) def _handle_client_disconnected(self, command: common.Command): client_id, _ = common.decode_string(command.data, 0) if client_id not in self.clients_attributes: logger.warning("Client %s disconnected but no attributes in internal view.", client_id) return del self.clients_attributes[client_id] def _handle_join_room(self, command: common.Command): room_name, _ = common.decode_string(command.data, 0) logger.info("Join room %s confirmed by server", room_name) self.current_room = room_name def _handle_send_error(self, command: common.Command): error_message, _ = common.decode_string(command.data, 0) logger.error("Received error message : %s", error_message) _default_command_handlers: Mapping[MessageType, Callable[[common.Command], None]] = { MessageType.LIST_CLIENTS: _handle_list_client, MessageType.LIST_ROOMS: _handle_list_rooms, MessageType.CLIENT_ID: _handle_client_id, MessageType.ROOM_UPDATE: _handle_room_update, MessageType.ROOM_DELETED: _handle_room_deleted, MessageType.CLIENT_UPDATE: _handle_client_update, MessageType.CLIENT_DISCONNECTED: _handle_client_disconnected, MessageType.JOIN_ROOM: _handle_join_room, MessageType.SEND_ERROR: _handle_send_error, } def has_default_handler(self, message_type: MessageType): return message_type in self._default_command_handlers def fetch_incoming_commands(self) -> List[common.Command]: """ Gather incoming commands from the socket and return them as a list. Process those that have a default handler with the one registered. """ try: received_commands = common.read_all_messages(self.socket) except common.ClientDisconnectedException: self.handle_connection_lost() raise count = len(received_commands) if count > 0: logger.debug("Received %d commands", len(received_commands)) for command in received_commands: logger.debug("Received %s", command.type) if command.type in self._default_command_handlers: self._default_command_handlers[command.type](self, command) return received_commands def fetch_outgoing_commands(self, commands_send_interval=0): """ Send commands in pending_commands queue to the server. """ for idx, command in enumerate(self.pending_commands): logger.debug("Send %s (%d / %d)", command.type, idx + 1, len(self.pending_commands)) if not self.send_command(command): break if commands_send_interval > 0: time.sleep(commands_send_interval) self.pending_commands = [] def fetch_commands(self, commands_send_interval=0) -> List[common.Command]: self.fetch_outgoing_commands(commands_send_interval) return self.fetch_incoming_commands()