From c6266c1cae34b2bb7be23ad317c60e80ac21ab23 Mon Sep 17 00:00:00 2001 From: Kayomn Date: Fri, 1 Oct 2021 00:16:27 +0100 Subject: [PATCH] Add usernames and command messages --- client.py | 25 ++++++++++---- config.py | 2 ++ server.py | 101 ++++++++++++++++++++++++++++++++++++------------------ 3 files changed, 89 insertions(+), 39 deletions(-) diff --git a/client.py b/client.py index d0fa087..25afbf9 100644 --- a/client.py +++ b/client.py @@ -3,11 +3,13 @@ __version__ = "0.0.1" __status__ = "Development" if (__name__ == "__main__"): + import chattle import config import socket import select import sys + username = input("username: ") address = (config.host, config.port) print("Starting connection to", address) @@ -16,20 +18,31 @@ if (__name__ == "__main__"): server_socket.setblocking(False) server_socket.connect_ex(address) + # Input may be written by the user or received from the server. Both cases have to be handled. inputs = [sys.stdin, server_socket] while True: + # Listen for input. readable_io, _, _ = select.select(inputs, [], []) for io in readable_io: if (io == server_socket): - print(io.recv(4096)) + response_data = io.recv(config.message_max) + + if not response_data: + exit(0) + + print(response_data.decode("utf-8")) elif (io == sys.stdin): - message = sys.stdin.readline() + # Messages produced by this client are written to the terminal locally, rather than sending it to + # server to then receive it back. + line = sys.stdin.readline() - server_socket.send(message.encode("utf-8")) - sys.stdout.write("") - sys.stdout.write(message) - sys.stdout.flush() + if not line.startswith("/"): + sys.stdout.write(" ") + sys.stdout.write(line) + sys.stdout.flush() + + server_socket.send(chattle.encode_message(username, line)) diff --git a/config.py b/config.py index 5c8e395..3946ae9 100644 --- a/config.py +++ b/config.py @@ -2,5 +2,7 @@ __author__ = "Kieran Osborne" __version__ = "0.0.1" __status__ = "Development" +server_greeting = "Welcome to Chattle" +message_max = 4096 host = "127.0.0.1" port = 12345 diff --git a/server.py b/server.py index 095d0e6..030b62a 100644 --- a/server.py +++ b/server.py @@ -4,59 +4,94 @@ __status__ = "Development" if (__name__ == "__main__"): import threading + import chattle import config import socket + # Sockets don't automatically close on their own, making a with statement is necessary. with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client_socket: - # Avoid bind() exception: OSError: [Errno 48] Address already in use + # Avoid "bind() exception: OSError: [Errno 48] Address already in use" error client_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) client_socket.bind((config.host, config.port)) - client_socket.listen(100) + client_socket.listen() - clients = [] + clients = set() - def spawn_client(client_connection, client_address): - client_connection.send("Welcome to this chatroom!".encode("utf-8")) + def broadcast(broadcasting_connection, message: str) -> None: + """ + Handles requests from a client to broadcast a message to all other connected clients but the one requesting + the broadcast. + + :param broadcasting_connection: Originating connection that requested the message be broadcast. + :param message: Message requested to be broadcast. + :return: Nothing. + """ + + print(message) + + for client in clients: + # No point broadcasting the message that the client sent the server back to the same client, it already + # has its own local copy since it was the one that wrote it. + if client != broadcasting_connection: + try: + client.send(message.encode("utf-8")) + + except socket.error: + # Remove clients who's connections to the server have been broken + client.close() + clients.discard(client) + + def handle_client(client_connection, client_address) -> None: + """ + Handles a single client connection, awaiting and processing input from it as and when it comes. + + :param client_connection: Client connection. + :param client_address: Client IP and port address tuple. + :return: Nothing. + """ + + client_ip = client_address[0] + + # Roll out the red carpet. + client_connection.send(config.server_greeting.encode("utf-8")) try: - print("Waiting for data") - data = client_connection.recv(4096) + # Handle each incoming message from the connected client. + data = client_connection.recv(config.message_max) while data: - message = f"<{client_address[0]}> {data}" + message = chattle.Message(data) + command = message.as_command() - print(message) - print("Propagating to clients") + if (command): + # It's a command message! + if (command == "quit"): + clients.discard(client_connection) + broadcast(client_connection, client_ip + " disconnected") + client_connection.close() - for client in clients: - if client != client_connection: - try: - client.send(message.encode("utf-8")) + return - except: - client.close() + else: + client_connection.send(f"Unknown command: {command}".encode("utf-8")) - # if the link is broken, we remove the client - if client_connection in clients: - clients.remove(client_connection) + else: + # It's a regular message + broadcast(client_connection, f"<{message.author}@{client_ip}> {message.body}") - print("Finished propagating") + data = client_connection.recv(config.message_max) - print("Waiting for data") - data = client_connection.recv(4096) - - if client_connection in clients: - print("dead") - clients.remove(client_connection) - - except: - return + except socket.timeout: + print(client_ip + " timed out") + client_connection.close() + clients.discard(client_connection) while True: + # Handle each incoming connection as and when they come. connection, address = client_socket.accept() - clients.append(connection) - - # prints the address of the user that just connected + clients.add(connection) print(address[0] + " connected") - threading.Thread(target=spawn_client, args=(connection, address)).start() + # Once a connection has been received it can be sent off to be handled elsewhere and this loop continues to + # handle connecting users. + threading.Thread(target=handle_client, args=(connection, address)).start()