C#多线程TcpClient场景调用Application.Exit()无法正常关闭连接问题
问题根因
你碰到的System.Net.Sockets.SocketException (0x80004005): A blocking operation was interrupted by a call to WSACancelBlockingCall不是代码bug,是TcpListener.Stop()的预期行为:AcceptTcpClient()是阻塞方法,当监听Socket被主动关闭时,阻塞在这个调用上的线程会被系统强制唤醒,抛出该异常。你之前没有对这个场景做特殊处理,才会导致退出流程卡住。
另外原代码没有统一管理所有已接入的TcpClient实例,停止监听时没有主动断开现有设备连接,加上手动启动的工作线程默认是前台线程(只要有前台线程未终止,进程就不会退出),才会出现窗体关闭后进程在后台残留的问题。你之前写的CancellationToken逻辑完全无效——StartServer()一旦进入AcceptTcpClient()阻塞状态,根本走不到外层的IsCancellationRequested判断,等于无效代码。
修复步骤
- 新增线程安全的集合,统一存储所有已接入的客户端连接、对应的处理线程,不要只存最后一个启动的线程(原代码里的
t字段会不断被新线程覆盖,之前接入的连接完全脱离管控)。 - 针对性处理
AcceptTcpClient()抛出的SocketException:如果是服务主动停止触发的异常,直接正常退出循环即可,不要当成运行错误处理。 - 停止服务时按顺序执行:先标记停止状态、再关闭监听器阻止新连接接入、再遍历所有活跃客户端挨个Close唤醒阻塞在读写的线程、最后等待所有客户端处理线程退出后再走完停止流程。
- 所有手动启动的工作线程全部设置
IsBackground = true,极端场景下就算停止逻辑没走完,CLR也会在进程退出时自动终止这些线程,不会留残留进程。 - 删除原来冗余的Task.Run+CancellationToken包装逻辑,把监听逻辑放到TCPService内部的独立后台线程跑,不要阻塞调用方。
修正后的代码
TCPService类
public class TCPService { private const int port = 8045; private bool running; private TcpListener server = null; // 线程安全集合存储所有活跃客户端连接 private readonly List<TcpClient> _connectedClients = new List<TcpClient>(); // 存储所有客户端处理线程,停止时等待退出 private readonly List<Thread> _clientThreads = new List<Thread>(); public ammendDeviceCountDelegate handleDeviceCount; public void StartServer() { string hostName = Dns.GetHostName(); IPAddress myIP = GetIPAddress(hostName); server = new TcpListener(myIP, port); server.Start(); running = true; // 监听逻辑跑在独立后台线程,不阻塞调用方 Thread listenerThread = new Thread(StartListener); listenerThread.IsBackground = true; listenerThread.Start(); } public void StopServer() { running = false; // 先停监听器,阻止新连接接入 try { server?.Stop(); } catch { } // 关闭所有已连接客户端,唤醒阻塞在读写的线程 lock (_connectedClients) { foreach (var client in _connectedClients) { try { client?.Close(); } catch { } } _connectedClients.Clear(); } // 等待客户端线程退出,最多等1秒防止卡退出 foreach (var t in _clientThreads) { t.Join(1000); } _clientThreads.Clear(); } private void StartListener() { while (running) { try { TcpClient client = server.AcceptTcpClient(); Console.WriteLine("新设备接入"); // 新客户端加入统一管理集合 lock (_connectedClients) { _connectedClients.Add(client); } // 启动客户端处理线程 Thread clientThread = new Thread(HandleDevice); clientThread.IsBackground = true; lock (_clientThreads) { _clientThreads.Add(clientThread); } clientThread.Start(client); } catch (SocketException) { // 主动停止触发的异常直接忽略,退出循环 break; } } } private void HandleDevice(Object obj) { TcpClient client = (TcpClient)obj; handleDeviceCount(false); try { using (client) using (var stream = client.GetStream()) { byte[] bytes = new Byte[256]; int i; // 连接断开或被主动关闭时,Read会返回0或抛异常,直接退出循环 while ((i = stream.Read(bytes, 0, bytes.Length)) != 0) { string data = Encoding.ASCII.GetString(bytes, 0, i); Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: 收到数据: {data}"); string replyStr = "handshaky"; Byte[] reply = Encoding.ASCII.GetBytes(replyStr); stream.Write(reply, 0, reply.Length); Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: 发送响应: {replyStr}"); } } } catch (Exception e) { // 非主动停止场景下的异常才打印日志 if (running) { Console.WriteLine($"客户端连接异常: {e.Message}"); } } finally { // 从管理集合移除已断开的客户端 lock (_connectedClients) { _connectedClients.Remove(client); } handleDeviceCount(true); } } private static IPAddress GetIPAddress(string hostname) { IPHostEntry host = Dns.GetHostEntry(hostname); foreach (IPAddress ip in host.AddressList) { if (ip.AddressFamily == AddressFamily.InterNetwork) { return ip; } } return IPAddress.None; } }
退出调用逻辑
直接在主窗体的FormClosing事件里调用停止方法即可,不需要原来冗余的Task包装:
private void MainForm_FormClosing(object sender, FormClosingEventArgs e) { tcp.StopServer(); }
如果是在Program类里处理退出逻辑,就在Application.Run()返回之后调用tcp.StopServer()就行。
内容的提问来源于stack exchange,提问作者832049823
相关产品推荐
相关产品推荐

