qt程序使用tcp连接,客户端退出后,再次重连服务器,一直出现close_wait状态,且无法再连接上

百度了tcp的各个状态,看到了 CLOSE_WAIT状态的原因与解决方法(转载留自己看)_编程小生的专栏-优快云博客_close_wait

里面说的close_wait的现象,主要原因是某种情况下对方关闭了socket链接,但是我方忙与读或者写,没有关闭连接。

觉得是自己的代码在读取数据时没有判断状态,且程序重新发起重连前没有关闭掉socket。

修改代码 ,后加的代码颜色标注了

void TcpClient::OnReadyRead()
{
    if(client_ && client_->state() == QAbstractSocket::ConnectedState)
    {

        QByteArray data = client_->readAll();

        ParseData(data.data(), data.size());
    }
}

查看代码发现

QBoundTcpSocket *client_;//是TcpClient的成员变量,在重连失败后,释放了new的QBoundTcpSocket ,但没有写析构函数,执行关闭连接的操作。

加上了析构函数

QBoundTcpSocket::~QBoundTcpSocket()
{
    Close();
}

bool QBoundTcpSocket::Close()
{
    disconnectFromHost();
    if( sockfd != -1 )
    {
        applog::LOGGER->Log(applog::kLogError, "QBoundTcpSocket::Close - 1111111111111111111111");
        ::close(sockfd);
    }
    applog::LOGGER->Log(applog::kLogError, "QBoundTcpSocket::Close - 2222222222222222222222");
    abort();//这里用abort才能把端口释放掉,使用close()是不行的
    sockfd = -1;
    return true;
}

<think> 我们正在处理一个使用PyQt6编写的TCP服务端程序,该程序采用多线程(通过`moveToThread`方式)实现,并通过按钮控制服务端的开启和关闭。问题在于关闭服务端后,客户端频繁尝试连接导致服务端程序崩溃。 根据引用中提供的信息,我们可以分析可能导致崩溃的原因: 1. 引用[2]提到线程终止的情况:包括线程函数结束、调用退出函数、被取消或进程终止。在关闭服务端时,我们需要确保线程正确终止,避免线程未正常退出而引发问题。 2. 引用[3]和[4]提到了TCP连接数过多以及网络相关的问题,特别是引用[4]提到一个服务端处理3~4万连接时出现问题。虽然我们的连接数可能没那么多,但频繁连接尝试也可能导致资源耗尽或程序崩溃。 在PyQt6的多线程模型中,我们通常将工作对象移动到新线程,并通过信号槽与主线程通信。在关闭服务端时,我们需要: - 停止监听新的连接。 - 关闭所有已建立的连接。 - 安全地终止工作线程。 可能导致崩溃的原因分析: 1. 在关闭服务端时,可能没有正确关闭已建立的连接,导致资源泄露(如套接字未关闭)。 2. 当客户端频繁连接时,服务端虽然已经关闭了监听套接字,但是可能在关闭过程中,由于客户端频繁连接尝试,导致系统资源(如文件描述符)耗尽,或者触发操作系统的限制。 3. 线程终止时,如果仍有客户端连接尝试,而服务端线程正在退出,可能导致访问已经释放的资源,从而崩溃。 4. 在关闭服务端时,如果工作线程还在处理连接请求(比如正在接受连接或读写数据),强制终止线程可能导致未定义行为。 解决方案: 我们需要在关闭服务端时,按以下步骤操作: 1. 停止监听:调用监听套接字的`close()`方法停止接受新的连接。 2. 断开所有现有连接:我们需要维护一个客户端连接列表,在关闭服务端时,遍历这个列表,关闭每个连接并移除。同时,确保在关闭连接时,这些连接对应的资源(如套接字)被正确释放。 3. 终止工作线程:通过信号通知工作线程退出,然后等待线程结束(使用`QThread.quit()`和`QThread.wait()`)。 4. 在关闭过程中,避免客户端频繁连导致的问题,可以设置一个标志位,表示服务端正在关闭,忽略新的连接请求。 具体实现步骤: 步骤1:在服务端类中,维护一个客户端套接字列表,并设置一个标志位表示服务端是否开启。 步骤2:在关闭服务端时,先将标志位置为关闭状态,然后关闭监听套接字。 步骤3:遍历客户端套接字列表,逐个关闭套接字,并从列表中移除。 步骤4:发送信号给工作线程,要求线程退出,并等待线程结束。 代码结构示例: 首先,定义一个工作类(ServerWorker),它将包含服务端的逻辑,并运行在工作线程中。 ```python import socket from PyQt6.QtCore import QObject, pyqtSignal, QThread class ServerWorker(QObject): new_client_signal = pyqtSignal(socket.socket) error_signal = pyqtSignal(str) finished = pyqtSignal() def __init__(self, host, port): super().__init__() self.host = host self.port = port self.running = False self.clients = [] # 存储客户端套接字 self.server_socket = None def start_server(self): try: self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.server_socket.bind((self.host, self.port)) self.server_socket.listen(5) self.running = True while self.running: try: client_socket, addr = self.server_socket.accept() # 将新连接客户端加入到列表 self.clients.append(client_socket) self.new_client_signal.emit(client_socket) except socket.error as e: self.error_signal.emit(f"Accept error: {e}") except Exception as e: self.error_signal.emit(f"Server start failed: {e}") finally: # 清理工作 self.stop_server() self.finished.emit() def stop_server(self): self.running = False # 关闭监听套接字 if self.server_socket: try: self.server_socket.close() except: pass self.server_socket = None # 关闭所有客户端套接字 for client in self.clients: try: client.close() except: pass self.clients.clear() ``` 在主窗口中,我们创建线程和worker,并通过按钮控制开启和关闭。 ```python from PyQt6.QtWidgets import QMainWindow, QPushButton, QApplication from PyQt6.QtCore import QThread class MainWindow(QMainWindow): def __init__(self): super().__init__() self.worker = None self.thread = None self.start_btn = QPushButton("Start Server", self) self.stop_btn = QPushButton("Stop Server", self) # ... 布局代码省略 ... self.start_btn.clicked.connect(self.start_server) self.stop_btn.clicked.connect(self.stop_server) def start_server(self): self.thread = QThread() self.worker = ServerWorker("0.0.0.0", 12345) self.worker.moveToThread(self.thread) self.thread.started.connect(self.worker.start_server) self.worker.finished.connect(self.thread.quit) self.worker.finished.connect(self.worker.deleteLater) self.thread.finished.connect(self.thread.deleteLater) # 连接错误信号和客户端连接信号 self.worker.error_signal.connect(self.handle_error) self.worker.new_client_signal.connect(self.handle_new_client) self.thread.start() def stop_server(self): if self.worker: # 调用worker的停止方法,这会触发worker的清理并退出循环 self.worker.stop_server() def handle_new_client(self, client_socket): # 处理新客户端 pass def handle_error(self, error_msg): print(error_msg) ``` 注意: 1. 在`stop_server`方法中,我们直接调用了`worker.stop_server()`。但是,由于worker运行在另一个线程,我们需要确保线程安全。我们可以通过信号槽来调用stop_server,因为信号槽是线程安全的。 因此,修改`MainWindow`的`stop_server`方法,通过信号触发worker的停止: ```python def stop_server(self): if self.worker: # 通过信号触发停止,确保线程安全 self.worker.stop_signal.emit() # 我们需要在worker中定义一个stop_signal信号 ``` 并在`ServerWorker`中增加一个信号`stop_signal`,并连接到一个停止方法: ```python class ServerWorker(QObject): # ... 其他信号 ... stop_signal = pyqtSignal() def __init__(self, host, port): super().__init__() # ... 初始化 ... self.stop_signal.connect(self.stop_server) # 连接信号到停止方法 ``` 这样,当点击停止按钮时,会发出`stop_signal`信号,该信号被连接到`worker.stop_server`,由于信号槽的机制,这个调用会在worker所在的线程中执行,因此是线程安全的。 2. 在`stop_server`方法中,我们关闭了服务器套接字和所有客户端套接字,并清除了客户端列表。同时,将`running`标志设为False,这使得`start_server`中的循环退出,然后发出`finished`信号,触发线程退出。 3. 关于客户端频繁连接的问题:当服务端关闭监听套接字后,客户端连接尝试会被拒绝(产生连接错误),但不会导致服务端崩溃。因为服务端已经不再接受连接。我们的处理是关闭所有现有连接,并退出监听循环。 4. 另外,在关闭客户端套接字时,可能客户端正在发送数据,因此可能会产生异常,我们在关闭时忽略异常。 5. 还有一个可能的崩溃点:当关闭服务端时,如果工作线程还在处理某个客户端的请求(比如在读取数据),直接关闭套接字可能会导致工作线程中产生异常。因此,我们最好在关闭套接字之前,先停止对该套接字的读写操作。但是,由于我们使用的是阻塞式套接字,在`accept`等待时,我们通过关闭监听套接字来中断等待(会产生一个错误),然后关闭所有客户端套接字。如果工作线程中除了`accept`之外没有阻塞操作,那么问题不大。如果有其他阻塞操作(如`recv`),那么我们需要在关闭客户端套接字时,中断这些阻塞操作。这可以通过设置超时或者使用非阻塞套接字来实现,但会增加复杂度。 为了避免阻塞操作导致无法及时响应停止请求,我们可以使用非阻塞套接字或设置超时,但这里为了简单,我们采用关闭套接字来中断阻塞操作(因为关闭套接字会使正在阻塞的操作立即返回错误)。 改进:在`stop_server`中关闭客户端套接字时,由于这些套接字可能正在被其他函数(如读取数据的函数)使用,所以关闭套接字会导致这些函数立即返回错误。我们需要确保这些错误被捕获,并不会导致程序崩溃。 在`ServerWorker`中,我们处理了`accept`的错误,但是没有处理客户端套接字操作中的错误。如果我们有单独的数据接收处理,那么也需要在这些地方捕获异常。 综上所述,采用上述步骤应该可以避免关闭服务端后客户端频繁连接导致的崩溃问题。 此外,引用[3]提到关闭网卡的TOE功能解决了类似问题,但我们的问题可能不需要,因为我们的连接数并不高。引用[4]提到select引起的崩溃,但我们的代码中没有使用select,所以可以排除。 因此,关键点在于正确关闭套接字和线程。 相关问题: 1. 在PyQt6中使用多线程处理TCP连接时,如何安全地停止线程? 2. 如何防止客户端频繁连接导致服务端资源耗尽? 3. 在服务端关闭时,如何处理尚未关闭的客户端连接? 4. 如何确保在关闭套接字时不会导致正在阻塞的线程操作崩溃? 根据上述分析,我们给出了一个解决方案。但需要注意的是,实际应用中可能还需要考虑更多细节,例如数据的收发处理、异常处理等。 </think> 针对PyQt6 TCP服务端在多线程(`moveToThread`)模式下,通过按键关闭服务后因客户端频繁连导致程序崩溃的问题,结合站内引用和TCP服务端开发经验,提出以下解决方案: ### 根本原因分析 1. **线程终止问题**:关闭服务时线程未安全终止,客户端连触发资源竞争(引用[2]) 2. **资源泄漏**:套接字和线程资源未彻底释放,客户端频繁连接耗尽系统资源 3. **事件循环冲突**:客户端连接请求与Qt事件循环交互导致崩溃(引用[4]) 4. **系统限制**:Windows家庭版Antimalware服务可能加剧资源争用(引用[1]) ### 解决方案代码实现 ```python from PyQt6.QtCore import QObject, pyqtSignal, QThread, QMutex from PyQt6.QtNetwork import QTcpServer, QTcpSocket import socket class TcpServerWorker(QObject): finished = pyqtSignal() error_occurred = pyqtSignal(str) def __init__(self): super().__init__() self.server = QTcpServer() self.active_sockets = [] self.running = False self.mutex = QMutex() # 线程安全锁 def start_server(self, port): try: if not self.server.listen(port=port): raise RuntimeError(self.server.errorString()) self.server.newConnection.connect(self.handle_connection) self.running = True except Exception as e: self.error_occurred.emit(f"启动失败: {str(e)}") def handle_connection(self): client_socket = self.server.nextPendingConnection() with self.mutex: # 线程安全操作 self.active_sockets.append(client_socket) client_socket.disconnected.connect( lambda: self.remove_socket(client_socket)) def remove_socket(self, socket): with self.mutex: if socket in self.active_sockets: socket.deleteLater() self.active_sockets.remove(socket) def safe_shutdown(self): """安全关闭服务端""" if not self.running: return self.running = False # 1. 停止接受新连接 self.server.close() # 2. 关闭所有客户端连接 with self.mutex: for socket in self.active_sockets[:]: # 复制列表避免迭代修改 try: socket.disconnectFromHost() if socket.state() == QTcpSocket.SocketState.ConnectedState: socket.waitForDisconnected(1000) socket.deleteLater() except: pass self.active_sockets.clear() # 3. 确保资源释放 self.server.deleteLater() self.finished.emit() class MainWindow(QMainWindow): def __init__(self): # ... 界面初始化 ... self.worker = TcpServerWorker() self.thread = QThread() self.worker.moveToThread(self.thread) # 连接信号 self.thread.started.connect(lambda: self.worker.start_server(8080)) self.worker.finished.connect(self.thread.quit) self.worker.finished.connect(self.worker.deleteLater) self.thread.finished.connect(self.thread.deleteLater) def start_server(self): self.thread.start() # 更新UI状态... def stop_server(self): # 通过信号触发安全关闭 self.worker.safe_shutdown.emit() # 更新UI状态... ``` ### 关键改进措施 1. **双阶段关闭协议**(解决资源泄漏) - 第一阶段:立即关闭监听套接字,拒绝新连接 - 第二阶段:逐一断开现有客户端连接(带超时机制) 2. **线程安全防护**(解决竞争条件) - 使用`QMutex`保护共享资源(`active_sockets`) - 通过`deleteLater()`确保Qt对象在主线程销毁 3. **防御性编程**(应对频繁连) ```python # 在handle_connection中添加连接数限制 MAX_CONNECTIONS = 1000 def handle_connection(self): if len(self.active_sockets) >= MAX_CONNECTIONS: client_socket = self.server.nextPendingConnection() client_socket.disconnectFromHost() return # ...正常处理... ``` 4. **系统优化建议**(引用[1][3]) - 禁用Windows Antimalware实时扫描(添加程序例外) - 调整TCP/IP参数(注册表): ```regedit [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters] "MaxUserPort"=dword:0000fffe ; 最大临时端口数(65534) "TcpTimedWaitDelay"=dword:0000001e ; TIME_WAIT时间(30秒) ``` ### 预防措施 1. **心跳检测机制**:实现空闲连接超时断开(减少僵尸连接) 2. **连接冷却策略**:对频繁连的客户端实施临时封禁 3. **资源监控**:实时显示连接数和内存使用(预防资源耗尽) 4. **日志增强**:记录连接/断开事件(便于追踪崩溃原因) > **注意**:关闭服务后客户端仍会收到`ConnectionRefusedError`,这是正常行为。可通过客户端实现指数退避连策略减轻服务端压力[^1][^3]。 --- **相关问题**: 1. PyQt6中如何实现TCP客户端指数退避连算法? 2. 多线程TCP服务端资源监控有哪些最佳实践? 3. Windows网络服务如何优化应对高并发连接? 4. Qt网络编程中常见的内存泄漏点有哪些?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值