diff --git a/src/client/client.py b/src/client/client.py new file mode 100644 index 0000000..ff8fc4c --- /dev/null +++ b/src/client/client.py @@ -0,0 +1,70 @@ +import socket +import threading +import sys + +# Wait for incoming data from server +# .decode is used to turn the message in bytes to a string +def receive(socket, stop_event): + while not stop_event.is_set(): + try: + data = b'' + while True: + chunk = socket.recv(4096) + data += chunk + if len(chunk) < 4096: + break + if data and len(data) > 0: + print(str(data.decode('utf-8'))) + except (socket.error, ConnectionResetError) as e: + print("You have been disconnected from the server. Error: " + str(e)) + break + +# Get host and port +host = input("Host: ") +port = int(input("Port: ")) +name = input("Enter your name: ") + +# Attempt connection to server +try: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.connect((host, port)) + sock.sendall(str.encode(name)) +except (socket.error, ConnectionRefusedError) as e: + # Create new thread to wait for data + stop_event = threading.Event() + receiveThread = threading.Thread(target=receive, args=(sock, stop_event)) + receiveThread.start() + # Send data to server + try: + while True: + message = input() + if not message: # Allow clean exit on empty input + break + sock.sendall(str.encode(message)) + except (socket.error, ConnectionResetError) as e: + print(f"Connection error: {e}") + finally: + print("Closing connection...") + stop_event.set() # Signal receive thread to stop + receiveThread.join() +except (socket.error, ConnectionRefusedError) as e: + print("Could not make a connection to the server. Error: " + str(e)) + input("Press enter to quit") + sys.exit(0) + +# Send data to server +# str.encode is used to turn the string message into bytes so it can be sent across the network +# Setup clean exit +try: + while True: + message = input() + if not message: # Allow clean exit on empty input + break + sock.sendall(str.encode(message)) +except (socket.error, ConnectionResetError) as e: + print(f"Connection error: {e}") +finally: + print("Closing connection...") + stop_event.set() # Signal receive thread to stop + sock.close() + receiveThread.join() diff --git a/src/server/server.py b/src/server/server.py new file mode 100644 index 0000000..b7a9365 --- /dev/null +++ b/src/server/server.py @@ -0,0 +1,108 @@ +import socket +import sys +import threading + +# Variables for holding information about connections +connections = [] +total_connections = 0 +_connections_lock = threading.Lock() + +# Client class, new instance created for each connected client +# Each instance has the socket and address that is associated with items +# Along with an assigned ID and a name chosen by the client +class Client(threading.Thread): + def __init__(self, socket, address, id, name, signal): + threading.Thread.__init__(self) + self.socket = socket + self.address = address + self.id = id + self.name = name + self.signal = signal + + def __str__(self): + return str(self.id) + " " + str(self.address) + " " + self.name + + # Attempt to get data from client + # If unable to, assume client has disconnected and remove him from server data + # If able to and we get data back, print it in the server and send it back to every + # client aside from the client that has sent it + # .decode is used to convert the byte data into a printable string + def run(self): + while self.signal: + try: + data = b'' + while True: + chunk = self.socket.recv(4096) + data += chunk + if len(chunk) < 4096: + break + except (socket.error, ConnectionResetError) as e: + print("Client " + str(self.address) + " has disconnected") + self.signal = False + with _connections_lock: + connections.remove(self) + break + if data != b"": + print("ID " + str(self.id) + ": " + str(data.decode('utf-8'))) + with _connections_lock: + for client in connections: + if client.id != self.id: + client.socket.sendall(data) + +# Wait for new connections +def newConnections(socket): + global total_connections + while True: + try: + sock, address = socket.accept() + name = sock.recv(1024).decode('utf-8') # Receive the name from the client + client = Client(sock, address, total_connections, name, True) + with _connections_lock: + connections.append(client) + total_connections += 1 + client.start() + print("New connection at ID " + str(client)) + except (socket.error, ConnectionError) as e: + print(f"Error accepting connection: {e}") + continue + +def main(): + # Get host and port + host = input("Host: ") + if not host: + host = "localhost" + try: + port = int(input("Port: ")) + if not (1024 <= port <= 65535): + raise ValueError("Port must be between 1024 and 65535") + except ValueError as e: + print(f"Invalid port: {e}") + sys.exit(1) + + # Create new server socket + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + sock.bind((host, port)) + sock.listen(5) + except socket.error as e: + print(f"Failed to bind socket: {e}") + sys.exit(1) + + # Create new thread to wait for connections + newConnectionsThread = threading.Thread(target=newConnections, args=(sock,)) + newConnectionsThread.start() + try: + stop_event = threading.Event() + stop_event.wait() # Wait indefinitely until KeyboardInterrupt + except KeyboardInterrupt: + print("\nShutting down server...") + # Signal all clients to stop + with _connections_lock: + for client in connections: + client.signal = False + client.socket.close() + client.join() + sock.close() + +if __name__ == "__main__": + main() \ No newline at end of file