From d157cf8189445843eb5c5c3bc9a59e425703e764 Mon Sep 17 00:00:00 2001 From: cdez Date: Mon, 21 Apr 2025 19:14:17 -0700 Subject: [PATCH] Continue making HW5 work... fix the client issue hopefully? --- chatserver/client.py | 4 + chatserver/server.py | 330 +++++++++++++++++-------------------------- 2 files changed, 131 insertions(+), 203 deletions(-) diff --git a/chatserver/client.py b/chatserver/client.py index 00e4254..431e3a7 100644 --- a/chatserver/client.py +++ b/chatserver/client.py @@ -32,6 +32,10 @@ def main(): client_socket.connect((SERVER_HOST, SERVER_PORT)) print(f"Connected to chat server at {SERVER_HOST}:{SERVER_PORT}") + # Prompt user for a username and send it to the server + username = input("Enter your username: ").strip() + client_socket.send(username.encode('utf-8')) + # Send the local path to the server client_socket.send(f"SET_PATH {local_path}".encode('utf-8')) diff --git a/chatserver/server.py b/chatserver/server.py index 59ffe71..ccd3f97 100644 --- a/chatserver/server.py +++ b/chatserver/server.py @@ -1,6 +1,7 @@ import os import socket import select +import threading CHATPORT = 8181 BUFFER_SIZE = 1024 @@ -18,213 +19,136 @@ def format_file_info(file_path): file_type = "Directory" if os.path.isdir(file_path) else "File" return f"{os.path.basename(file_path)} - {file_type} - {size_str}" -def main(): - # set the flag variable to true for infinite loop - quitFlag = True - # create a socket object - server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - # set the socket options, SO_REUSEADDR allows to reuse the same port - # SO_KEEPALIVE enables the socket to send keepalive messages - server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) - # bind the socket to the address and port - server_socket.bind(('', CHATPORT)) - # listen for incoming connections - server_socket.listen(MAX_CONNECT_QUEUE) - # set the server socket to non-blocking - server_socket.setblocking(False) +def handle_client(sock, client_address, socket_list, usernames, chat_rooms, client_states, user_paths): + """Handle communication with a single client.""" + try: + while True: + message = sock.recv(BUFFER_SIZE).decode('utf-8') + if not message: + break - # create a list to store the sockets, server and client - socket_list = [server_socket] - # since a socket is associated with a username and a chatroom, - # and each one can only be associated one way at once, use dictionaries to - # associate with each other. - usernames = {} - chat_rooms = {} - # keep track of the states of each client connected, and if they are - # technically in an 'unsafe' state like in change username or chatroom mode. - client_states = {} - # Add a dictionary to store user-specific paths - user_paths = {} - # let the user know the server is listening on port CHATPORT. + state = client_states.get(sock, "NORMAL") + if state == "WAITING_FOR_USERNAME": + usernames[sock] = message.strip() + chat_rooms[sock] = "default" + sock.send(b"\nUsername accepted. You are now in the 'default' chatroom.") + client_states[sock] = "NORMAL" + user_paths[sock] = None # Initialize path for the user + elif state == "NORMAL": + if message.startswith("@") and "-ls" in message: + # Handle directory listing request + parts = message.split() + if len(parts) >= 2 and "-ls" in parts[1]: + usernames_part = parts[0] + if usernames_part.count("@") == 2: + receiving_username, sending_username = usernames_part[1:].split("@") + receiving_sock = next((s for s, u in usernames.items() if u == receiving_username), None) + if receiving_sock: + user_path = user_paths.get(receiving_sock) + if user_path and os.path.isdir(user_path): + files = os.listdir(user_path) + if files: + file_list = "\n".join( + [format_file_info(os.path.join(user_path, f)) for f in files] + ) + sock.send(f"Files in {receiving_username}'s directory:\n{file_list}".encode('utf-8')) + else: + sock.send(f"No files in {receiving_username}'s directory.".encode('utf-8')) + else: + sock.send(f"Error: {receiving_username} has no valid directory set.".encode('utf-8')) + else: + sock.send(b"Error: Receiving user not found.") + else: + sock.send(b"Error: Invalid message format.") + elif message.startswith("@") and "-f" in message: + # Handle file request or transfer + parts = message.split() + if len(parts) >= 3 and "-f" in parts[1]: + usernames_part = parts[0] + if usernames_part.count("@") == 2: + receiving_username, sending_username = usernames_part[1:].split("@") + receiving_sock = next((s for s, u in usernames.items() if u == receiving_username), None) + if receiving_sock: + if len(parts) == 3: + # File request + filename = parts[2] + receiving_sock.send(f"FILE_REQUEST {sending_username} {filename}".encode('utf-8')) + elif len(parts) > 5 and parts[-1] == "ENDFILE": + # File transfer + filename = parts[2] + filetype = parts[3] + filelength = int(parts[4]) + filebytes = " ".join(parts[5:-1]).encode('utf-8') + receiving_sock.send( + f"FILE_TRANSFER {sending_username} {filename} {filetype} {filelength}".encode('utf-8') + ) + receiving_sock.send(filebytes) + receiving_sock.send(b"ENDFILE") + else: + sock.send(b"Error: Receiving user not found.") + else: + sock.send(b"Error: Invalid message format.") + elif message.strip() == "%!": + break + else: + username = usernames[sock] + chat_room = chat_rooms[sock] + send_message = f"{username} >> {message}".encode('utf-8') + for client_sock in socket_list: + if client_sock != sock and chat_rooms.get(client_sock) == chat_room: + client_sock.send(send_message) + except Exception as e: + print(f"Error with client {client_address}: {e}") + finally: + # Clean up on client disconnect + sock.close() + socket_list.remove(sock) + if sock in usernames: + del usernames[sock] + if sock in chat_rooms: + del chat_rooms[sock] + if sock in client_states: + del client_states[sock] + if sock in user_paths: + del user_paths[sock] + print(f"Client {client_address} disconnected.") + +def main(): + # Initialize server + server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server_socket.bind(('', CHATPORT)) + server_socket.listen(MAX_CONNECT_QUEUE) print(f"Chatserver listening on port: {CHATPORT}") - # infinite loop for the program - while quitFlag: - # use the select function to monitor the sockets - read_sockets, _, _ = select.select(socket_list, [], []) - # loop through the sockets - for sock in read_sockets: - # if the socket is the server socket, accept the connection - if sock == server_socket: - client_socket, client_address = server_socket.accept() - client_socket.setblocking(False) - socket_list.append(client_socket) - print("\nAccepted connection from new client.") - client_socket.send(b"\nWelcome to the chat server!\n") - client_socket.send(b"\nUse '%!' to exit, or '%?' for help.\n") - client_socket.send(b"\nType in a username: ") - client_states[client_socket] = "WAITING_FOR_USERNAME" - user_paths[client_socket] = None # Initialize path for new client - # otherwise the socket is a client socket. - else: - # try to receive the message from the client - try: - message = sock.recv(BUFFER_SIZE).decode('utf-8') - if message: - state = client_states.get(sock, "NORMAL") - if state == "WAITING_FOR_USERNAME": - usernames[sock] = message.strip() - chat_rooms[sock] = "default" - sock.send(b"\nUsername accepted. You are now in the 'default' chatroom.") - client_states[sock] = "NORMAL" - user_paths[sock] = None # Initialize path for the user - elif state == "WAITING_FOR_NEW_USERNAME": - usernames[sock] = message.strip() - sock.send(b"\nUsername changed successfully.") - print(f"\nClient switched username to {usernames[sock]}.") - client_states[sock] = "NORMAL" - elif state == "WAITING_FOR_NEW_CHATROOM": - old_chatroom = chat_rooms[sock] - chat_rooms[sock] = message.strip() - sock.send(f"\nSwitched to chatroom '{chat_rooms[sock]}.'".encode('utf-8')) - print(f"\nClient switched to chatroom '{chat_rooms[sock]}.'") - if old_chatroom != "default" and old_chatroom not in chat_rooms.values(): - print(f"\nChatroom '{old_chatroom}' is empty and removed.") - client_states[sock] = "NORMAL" - elif state == "NORMAL": - if message.startswith("@") and "-ls" in message: - # Parse the message format: @RECEIVING_USERNAME@SENDING_USERNAME -ls - parts = message.split() - if len(parts) >= 2 and "-ls" in parts[1]: - usernames_part = parts[0] - if usernames_part.count("@") == 2: - receiving_username, sending_username = usernames_part[1:].split("@") - # Find the receiving user's socket - receiving_sock = next( - (s for s, u in usernames.items() if u == receiving_username), None - ) - sending_sock = next( - (s for s, u in usernames.items() if u == sending_username), None - ) - if receiving_sock and sending_sock: - user_path = user_paths.get(receiving_sock) - if user_path and os.path.isdir(user_path): - # List files in the receiving user's directory - files = os.listdir(user_path) - if files: - file_list = "\n".join( - [format_file_info(os.path.join(user_path, f)) for f in files] - ) - sending_sock.send( - f"Files in {receiving_username}'s directory:\n{file_list}".encode('utf-8') - ) - else: - sending_sock.send( - f"No files in {receiving_username}'s directory.".encode('utf-8') - ) - else: - sending_sock.send( - f"Error: {receiving_username} has no valid directory set.".encode('utf-8') - ) - else: - sock.send(b"Error: One or both users not found.") - else: - sock.send(b"Error: Invalid message format.") - elif message.startswith("@") and "-f" in message: - # Parse the message format: @RECEIVING_USERNAME@SENDING_USERNAME -f FILENAME - parts = message.split() - if len(parts) >= 3 and "-f" in parts[1]: - usernames_part = parts[0] - if usernames_part.count("@") == 2: - receiving_username, sending_username = usernames_part[1:].split("@") - # Find the receiving user's socket - receiving_sock = next( - (s for s, u in usernames.items() if u == receiving_username), None - ) - sending_sock = next( - (s for s, u in usernames.items() if u == sending_username), None - ) - if receiving_sock and sending_sock: - if len(parts) == 3: - # File request: @RECEIVING_USERNAME@SENDING_USERNAME -f FILENAME - filename = parts[2] - receiving_sock.send( - f"FILE_REQUEST {sending_username} {filename}".encode('utf-8') - ) - elif len(parts) > 5 and parts[-1] == "ENDFILE": - # File transfer: @RECEIVING_USERNAME@SENDING_USERNAME -f FILENAME FILETYPE FILELENGTH FILEBYTES ENDFILE - filename = parts[2] - filetype = parts[3] - filelength = int(parts[4]) - filebytes = " ".join(parts[5:-1]).encode('utf-8') - receiving_sock.send( - f"FILE_TRANSFER {sending_username} {filename} {filetype} {filelength}".encode('utf-8') - ) - receiving_sock.send(filebytes) - receiving_sock.send(b"ENDFILE") - else: - sock.send(b"Error: One or both users not found.") - else: - sock.send(b"Error: Invalid message format.") - elif message.strip() == "%!": - sock.close() - socket_list.remove(sock) - del usernames[sock] - del chat_rooms[sock] - del client_states[sock] - print("\nClient has closed connection.") - elif message.strip() == "%?": - print("\nClient requested help.") - sock.send(b"Help: (%?)\n") - sock.send(b"Send messages like normal. Use enter to send a message.\n") - sock.send(b"Available Commands:\n") - sock.send(b"\t%? : Show this help prompt.\n") - sock.send(b"\t%! : Exit from the Chat Server.\n") - sock.send(b"\t%* : Switch to another chatroom.\n") - sock.send(b"\t%$ : Change Username.\n") - elif message.strip() == "%*": - sock.send(b"\nEnter chatroom name: ") - client_states[sock] = "WAITING_FOR_NEW_CHATROOM" - elif message.strip() == "%$": - sock.send(b"\nEnter new username: ") - client_states[sock] = "WAITING_FOR_NEW_USERNAME" - else: - username = usernames[sock] - chat_room = chat_rooms[sock] - send_message = f"{username} >> {message}".encode('utf-8') - for client_sock in socket_list: - if client_sock != server_socket and chat_rooms.get(client_sock) == chat_room: - client_sock.send(send_message) - else: - sock.close() - socket_list.remove(sock) - if sock in usernames: - del usernames[sock] - if sock in chat_rooms: - old_chatroom = chat_rooms[sock] - del chat_rooms[sock] - if old_chatroom != "default" and old_chatroom not in chat_rooms.values(): - print(f"\nChatroom '{old_chatroom}' is empty and has been removed.") - if sock in client_states: - del client_states[sock] - print("\nClient has closed connection.") - except Exception as err: - print(f"Error: {err}") - sock.close() - socket_list.remove(sock) - if sock in usernames: - del usernames[sock] - if sock in chat_rooms: - old_chatroom = chat_rooms[sock] - del chat_rooms[sock] - if old_chatroom != "default" and old_chatroom not in chat_rooms.values(): - print(f"\nChatroom '{old_chatroom}' is empty and has been removed.") - if sock in client_states: - del client_states[sock] - print("\nClient has closed connection.") + # Shared data structures + socket_list = [] + usernames = {} + chat_rooms = {} + client_states = {} + user_paths = {} - server_socket.close() + try: + while True: + client_socket, client_address = server_socket.accept() + print(f"Accepted connection from {client_address}") + socket_list.append(client_socket) + + # Initialize client state + client_states[client_socket] = "WAITING_FOR_USERNAME" + user_paths[client_socket] = None + + # Start a new thread for the client + client_thread = threading.Thread( + target=handle_client, + args=(client_socket, client_address, socket_list, usernames, chat_rooms, client_states, user_paths), + daemon=True + ) + client_thread.start() + except KeyboardInterrupt: + print("\nShutting down server...") + finally: + server_socket.close() if __name__ == "__main__": main() \ No newline at end of file