Keyboard Interrupt无法正常退出:多线程Socket服务器优雅关闭求助
解决Socket服务器需两次Ctrl+C才能退出的问题
我来帮你搞定这个困扰,咱们先拆解问题根源,再给出可行的修复方案。
问题根源分析
你遇到的核心问题来自两个关键点:
- 非守护线程的默认行为:Python创建的线程默认是非守护线程,主程序退出时会等待所有非守护线程执行完毕才会彻底终止。
- 阻塞的
recv()调用:子线程里的conn_sock.recv(1024)是阻塞式调用,当客户端没有发送数据时,线程会一直卡在这个位置,不会主动退出。 - 另外,子线程里用
sys.exit()只能退出当前线程,无法触发整个程序的退出,这不是正确的线程终止方式。
解决方案:两种修复思路
方案1:快速修复——使用守护线程+关闭监听Socket
这个方案简单直接,适合快速解决问题:
- 将客户端线程设置为守护线程,这样主线程退出时,守护线程会被自动终止。
- 在
KeyboardInterrupt处理块中关闭监听Socket,让主循环里的sock.accept()抛出异常,直接退出主循环。
修改后的完整代码:
import socket import sys from threading import Thread try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) except socket.error as e: print('Error occured while creating the socket {}'.format(e)) sys.exit(1) server_address = ('localhost', 50000) sock.bind(server_address) print('**** Server started on {}:{} ****'.format(*server_address)) sock.listen(5) def client_thread(conn_sock, client_add): try: while True: client_msg = conn_sock.recv(1024).decode() if not client_msg: # 处理客户端异常断开的情况 break if client_msg.lower() != 'exit': print('[{{0}}:{{1}}] {{2}}'.format(*client_add, client_msg)) serv_reply = 'Okay ' + client_msg.upper() conn_sock.send(bytes(serv_reply, 'utf-8')) else: print('{} exitted !!'.format(client_add[0])) break except Exception as e: print(f"Thread error for {client_add}: {e}") finally: conn_sock.close() # 确保关闭客户端Socket try: while True: try: conn_sock, client_add = sock.accept() print('Recieved connection from {}:{}'.format(*client_add)) conn_sock.send(bytes('***** Welcome to {} *****'.format(server_address[0]), 'utf-8')) # 设置daemon=True,将线程设为守护线程 Thread(target=client_thread, args=(conn_sock, client_add), daemon=True).start() except OSError: # 监听Socket被关闭时,accept()会抛出OSError,退出主循环 break except KeyboardInterrupt: print('Program execution cancelled by user') finally: sock.close() print("Server socket closed")
关键改动说明:
- 创建线程时添加
daemon=True,标记为守护线程。 - 在
client_thread中添加finally块,确保客户端Socket被正确关闭。 - 处理
accept()抛出的OSError,当监听Socket关闭时退出主循环。
方案2:优雅退出——使用全局标志+Socket超时
如果希望线程能主动退出而不是被强制终止,可以用全局运行标志+Socket超时的方式,更优雅:
- 定义全局
running标志,控制主循环和子线程的执行。 - 给客户端Socket设置超时,让
recv()不会一直阻塞,定期检查running标志。 - 收到
KeyboardInterrupt时,设置running=False,关闭监听Socket,等待线程主动退出。
修改后的完整代码:
import socket import sys from threading import Thread import time running = True # 全局运行标志 try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) except socket.error as e: print('Error occured while creating the socket {}'.format(e)) sys.exit(1) server_address = ('localhost', 50000) sock.bind(server_address) print('**** Server started on {}:{} ****'.format(*server_address)) sock.listen(5) def client_thread(conn_sock, client_add): conn_sock.settimeout(1) # 设置1秒超时,定期检查running标志 try: while running: try: client_msg = conn_sock.recv(1024).decode() if not client_msg: break if client_msg.lower() != 'exit': print('[{{0}}:{{1}}] {{2}}'.format(*client_add, client_msg)) serv_reply = 'Okay ' + client_msg.upper() conn_sock.send(bytes(serv_reply, 'utf-8')) else: print('{} exitted !!'.format(client_add[0])) break except socket.timeout: continue # 超时后继续检查running标志 except Exception as e: print(f"Thread error for {client_add}: {e}") finally: conn_sock.close() print(f"Closed connection with {client_add}") try: while running: try: sock.settimeout(1) # 给监听Socket也设置超时,定期检查running conn_sock, client_add = sock.accept() print('Recieved connection from {}:{}'.format(*client_add)) conn_sock.send(bytes('***** Welcome to {} *****'.format(server_address[0]), 'utf-8')) Thread(target=client_thread, args=(conn_sock, client_add)).start() except socket.timeout: continue except KeyboardInterrupt: print('Program execution cancelled by user') running = False # 设置退出标志 finally: sock.close() print("Server socket closed") # 给线程足够时间清理资源 time.sleep(1.5)
关键改动说明:
- 添加全局
running标志,控制所有循环的执行。 - 给客户端Socket和监听Socket设置超时,避免阻塞调用一直挂起。
- 收到中断时设置
running=False,让所有线程主动退出循环,清理资源。
测试验证
现在按一次Ctrl+C,主程序会:
- 捕获
KeyboardInterrupt,设置退出标志(方案2)或直接关闭监听Socket(方案1)。 - 主线程退出,守护线程被终止(方案1)或线程主动检查到标志后退出(方案2)。
- 所有Socket资源被正确关闭,程序彻底退出,不需要第二次Ctrl+C。
内容的提问来源于stack exchange,提问作者Rohit
相关产品推荐
相关产品推荐

