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

Keyboard Interrupt无法正常退出:多线程Socket服务器优雅关闭求助

解决Socket服务器需两次Ctrl+C才能退出的问题

我来帮你搞定这个困扰,咱们先拆解问题根源,再给出可行的修复方案。

问题根源分析

你遇到的核心问题来自两个关键点:

  • 非守护线程的默认行为:Python创建的线程默认是非守护线程,主程序退出时会等待所有非守护线程执行完毕才会彻底终止。
  • 阻塞的recv()调用:子线程里的conn_sock.recv(1024)是阻塞式调用,当客户端没有发送数据时,线程会一直卡在这个位置,不会主动退出。
  • 另外,子线程里用sys.exit()只能退出当前线程,无法触发整个程序的退出,这不是正确的线程终止方式。

解决方案:两种修复思路

方案1:快速修复——使用守护线程+关闭监听Socket

这个方案简单直接,适合快速解决问题:

  1. 将客户端线程设置为守护线程,这样主线程退出时,守护线程会被自动终止。
  2. 在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超时的方式,更优雅:

  1. 定义全局running标志,控制主循环和子线程的执行。
  2. 给客户端Socket设置超时,让recv()不会一直阻塞,定期检查running标志。
  3. 收到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,主程序会:

  1. 捕获KeyboardInterrupt,设置退出标志(方案2)或直接关闭监听Socket(方案1)。
  2. 主线程退出,守护线程被终止(方案1)或线程主动检查到标志后退出(方案2)。
  3. 所有Socket资源被正确关闭,程序彻底退出,不需要第二次Ctrl+C。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 07:30:26