You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Python UDP服务器捕获KeyboardInterrupt后报Bad file descriptor的解决咨询

Python UDP多线程服务器捕获KeyboardInterrupt后无法优雅停止的问题解决

问题描述

采用多线程实现Python UDP服务器,用于接收并处理客户端消息,预期捕获KeyboardInterrupt(Ctrl+C)后优雅停止服务。但实际捕获该中断后,服务器仍继续处理消息,进而触发“Socket error in send_response_to_client: [Errno 9] Bad file descriptor”类错误。尝试修改服务器线程使其在捕获KeyboardInterrupt后优雅退出,但问题仍未解决。

服务器简化版代码:

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.timeout:
                # 超时仅退出阻塞,继续检查running状态
                continue
            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."""
        if not self.running:
            return
        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:
                try:
                    message = input(
                        "Type your message to the client or q to quit the server: "
                    )
                except EOFError:
                    # 处理输入被中断的情况
                    self.stop_server()
                    return
                if message.lower() == "q":
                    print("Server shutting down...")
                    self.stop_server()
                    return
                if not self.running:
                    break
                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}")
            time.sleep(0.1)  # 避免空循环占用CPU

    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.server_socket.settimeout(1.0)  # 设置超时,避免recvfrom永久阻塞
            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()

            # 等待停止信号
            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 not self.running:
            return
        print("Server stopping...")
        self.running = False
        
        # 唤醒阻塞的recvfrom,让线程退出循环
        if self.server_socket:
            try:
                self.server_socket.sendto(b'', ('localhost', 12345))
            except:
                pass
        
        # 先等待线程退出,再关闭socket
        if threading.current_thread() != self.receiver_thread and self.receiver_thread.is_alive():
            self.receiver_thread.join()
        if threading.current_thread() != self.user_thread and self.user_thread.is_alive():
            self.user_thread.join()
        
        if self.server_socket:
            self.server_socket.close()
        print("Server stopped.")


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

问题成因

  • 阻塞调用无法及时响应状态变更:recvfrom是阻塞式系统调用,当设置self.running=False后,线程仍会卡在recvfrom上,直到有外部消息触发返回,此时socket已经被关闭,后续操作必然报错。
  • KeyboardInterrupt处理缺失核心逻辑:主程序捕获到中断后仅打印提示,未调用stop_server(),导致服务器的停止流程未被触发,线程持续运行。
  • 资源释放顺序错误:原代码中先关闭socket,再等待线程退出,线程在退出前仍会尝试使用已关闭的socket,引发文件描述符错误。

解决方案说明

  1. 给socket设置超时:让recvfrom每隔1秒自动退出阻塞,检查self.running状态,确保线程能及时响应停止指令。
  2. 唤醒阻塞的接收线程:在停止时向自身socket发送空数据,强制recvfrom返回,避免线程长时间阻塞。
  3. 调整资源释放顺序:先等待线程完全退出,再关闭socket,从根源上避免线程操作已释放的资源。
  4. 完善主程序中断处理:在KeyboardInterrupt捕获块中调用stop_server(),触发完整的停止逻辑。
  5. 增加状态检查:在发送响应、处理用户输入前检查self.running状态,避免执行无效操作。

内容的提问来源于stack exchange,提问作者Sayed Ahmad

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 06:00:16