python-3.xsocketspython-sockets

Python UDP server continues processing messages after KeyboardInterrupt, leading to "Bad file descriptor" error


I have implemented a UDP server in Python using threading to handle message reception and processing. The server is intended to stop gracefully when a KeyboardInterrupt (Ctrl+C) is caught. However, even after catching the KeyboardInterrupt, the server continues to process messages, leading to errors like "Socket error in send_response_to_client: [Errno 9] Bad file descriptor".

I have tried modifying the server threads to exit gracefully upon KeyboardInterrupt, but the issue persists. Can anyone suggest what might be causing this behavior and how to ensure that the server stops processing messages immediately after the KeyboardInterrupt is caught?

Here is a simplified version of my server code:

import socket
import threading
import sys
import time


class UDPServer:
    """UDP server class for receiving and responding to messages from clients."""

    def __init__(self):
        """Initialize the server."""
        self.last_client_address = None
        self.lock = threading.Lock()
        self.server_socket = None
        self.receiver_thread = None
        self.user_thread = None
        self.running = False

    def receive_messages_from_clients(self):
        """Receive messages from clients and respond to them."""
        while self.running:
            try:
                message, client_address = self.server_socket.recvfrom(1024)
                print(f"Received message from {client_address}: {message.decode()}")
                with self.lock:
                    self.last_client_address = client_address
                self.send_response_to_client(message, client_address)
            except socket.error as socket_error:
                print(f"Socket error in receive_messages_from_clients: {socket_error}")
            except Exception as general_error:
                print(f"Error in receive_messages_from_clients: {general_error}")

    def send_response_to_client(self, message, client_address):
        """Send response to the client."""
        try:
            self.server_socket.sendto(
                f"Echo: {message.decode()}".encode(), client_address
            )
        except socket.error as socket_error:
            print(f"Socket error in send_response_to_client: {socket_error}")
        except Exception as general_error:
            print(f"Error in send_response_to_client: {general_error}")

    def send_user_message_to_last_client(self):
        """Send user input messages to the last client."""
        while self.running:
            if self.last_client_address is not None:
                breakpoint()
                message = input(
                    "Type your message to the client or q to quite the server: "
                )
                if message.lower() == "q":
                    print("KeyboardInterrupt: Server shutting down...")
                    server.stop_server()
                    sys.exit(0)
                try:
                    self.server_socket.sendto(
                        f"Server user message: {message}".encode(),
                        self.last_client_address,
                    )
                except socket.error as socket_error:
                    print(
                        f"Socket error in send_user_message_to_last_client: {socket_error}"
                    )
                except Exception as general_error:
                    print(f"Error in send_user_message_to_last_client: {general_error}")

    def run_server(self, host, port):
        """Start the server and listen for incoming messages."""
        try:
            self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            self.server_socket.bind((host, port))
            self.running = True

            print(f"Server listening on {host}:{port}")

            self.receiver_thread = threading.Thread(
                target=self.receive_messages_from_clients
            )
            self.receiver_thread.start()

            self.user_thread = threading.Thread(
                target=self.send_user_message_to_last_client
            )
            self.user_thread.start()

            # Wait for KeyboardInterrupt to stop the server
            while self.running:
                time.sleep(1)

        except socket.error as socket_error:
            print(f"Socket error in run_server: {socket_error}")
            self.stop_server()
        except Exception as general_error:
            print(f"General error in run_server: {general_error}")
            self.stop_server()
        finally:
            self.stop_server()

    def stop_server(self):
        """Stop the server and perform cleanup."""
        if self.running:
            print("Server stopping...")
            self.running = False
            if self.server_socket:
                self.server_socket.close()
            if threading.current_thread() != self.receiver_thread:
                self.receiver_thread.join()
            if threading.current_thread() != self.user_thread:
                self.user_thread.join()
            print("Server stopped.")


  if __name__ == "__main__":
        server = UDPServer()
        try:
            server.run_server("localhost", 12345)
        except KeyboardInterrupt:
            print("\nKeyboardInterrupt: Server shutting down...")

Solution

    1. Don't close the server socket before joining the threads in stop_server.
    2. recvfrom hangs until a message is received, so self.running won't be checked until a message is received. Add self.server_socket.settimeout(1) (whatever time you feel appropriate) to run_server. Add except socket.error: pass to receive_messages_from_clients to ignore the timeout. That way self.running will get polled. That should help things shut down cleanly.