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,引发文件描述符错误。
解决方案说明
- 给socket设置超时:让
recvfrom每隔1秒自动退出阻塞,检查self.running状态,确保线程能及时响应停止指令。 - 唤醒阻塞的接收线程:在停止时向自身socket发送空数据,强制
recvfrom返回,避免线程长时间阻塞。 - 调整资源释放顺序:先等待线程完全退出,再关闭socket,从根源上避免线程操作已释放的资源。
- 完善主程序中断处理:在KeyboardInterrupt捕获块中调用
stop_server(),触发完整的停止逻辑。 - 增加状态检查:在发送响应、处理用户输入前检查
self.running状态,避免执行无效操作。
内容的提问来源于stack exchange,提问作者Sayed Ahmad
相关产品推荐
相关产品推荐

