多线程TCP服务器互斥锁异常:单客户端断开致全客户端离线问题
多线程TCP服务器单客户端断开导致全客户端离线问题修复
问题描述
我正在开发一个多线程TCP服务器,使用Mutex控制数据发送,当前遇到的问题是当某一个客户端断开连接时,其余所有客户端也会随之断开。需要实现单个客户端断开时不影响其他客户端的正常连接。
服务器代码(原代码)
using Newtonsoft.Json; using Pastel; using System; using System.Collections.Generic; using System.Data.SqlClient; using System.Drawing; using System.IO; using System.Linq; using System.Net; using System.Net.Sockets; using System.Security.AccessControl; using System.Text; using System.Threading; using System.Threading.Tasks; namespace DeepestSwordServer { public class Program { public static TcpListener server; public static TcpClient client = new TcpClient(); public static Thread t; public static Mutex mutex = new Mutex(false, "poligamerMod"); public static List<Connection> list = new List<Connection>(); public static Connection con; static void Main(string[] args) { string ConfigPath = Path.Combine(Environment.CurrentDirectory, "config.json"); if (File.Exists(ConfigPath)) { Console.WriteLine("Starting Server...".Pastel(Color.FromArgb(165, 229, 250))); try { string json = File.ReadAllText(ConfigPath); Config config = JsonConvert.DeserializeObject<Config>(json); Start(config.Ip, config.Port); } catch (Exception e) { Console.WriteLine($"Failed To Start Server Error: {e.Message}".Pastel(Color.FromArgb(255, 0, 0))); } } else { Console.WriteLine("Creating Config File...".Pastel(Color.FromArgb(165, 229, 250))); Config config = new Config(); config.Ip = "0.0.0.0"; config.Port = 3878; string json = JsonConvert.SerializeObject(config); File.Create(ConfigPath).Dispose(); File.WriteAllText(ConfigPath, json); Console.WriteLine("File Created!".Pastel(Color.FromArgb(58, 255, 0))); Console.WriteLine("Please see the config.json where is the ip, port and password if needed of the server, you need to restart the server!".Pastel(Color.FromArgb(165, 229, 250))); Console.ReadLine(); } } public static void Start(string ip, int port) { Console.WriteLine("Server Ready!".Pastel(Color.FromArgb(58, 255, 0))); server = new TcpListener(IPAddress.Parse(ip), port); server.Start(); while (true) { client = server.AcceptTcpClient(); con = new Connection(); con.stream = client.GetStream(); con.streamr = new StreamReader(con.stream); con.streamw = new StreamWriter(con.stream); con.username = con.streamr.ReadLine(); con.id = con.streamr.ReadLine(); Event @event = new Event(); @event.EventType = EventType.Join; @event.Username = con.username; @event.Id = con.id; string join = JsonConvert.SerializeObject(@event); foreach (Connection c in list) { c.streamw.WriteLine(join); @event = new Event(); @event.EventType = EventType.Join; @event.Username = c.username; @event.Id = c.id; string newJoin = JsonConvert.SerializeObject(@event); con.streamw.WriteLine(newJoin); } list.Add(con); @event = new Event(); @event.EventType = EventType.List; foreach (Connection c in list.ToList()) { @event.List.Add(c.username, c.id); } string playerList = JsonConvert.SerializeObject(@event); con.streamw.WriteLine(playerList); Console.WriteLine($"{con.username} with id {con.id} has connected!".Pastel(Color.FromArgb(58, 255, 0))); t = new Thread(HearConnection); t.Start(); } } public static void HearConnection() { Connection hcon = con; do { try { foreach (Connection c in list.ToList()) { mutex.WaitOne(); c.streamw.WriteLine(hcon.streamr.ReadLine()); mutex.ReleaseMutex(); } } catch (Exception e) { list.Remove(hcon); Event @event = new Event(); @event.EventType = EventType.Leave; @event.Username = con.username; @event.Id = con.id; string leave = JsonConvert.SerializeObject(@event); foreach (Connection c in list) { c.streamw.WriteLine(leave); } Console.WriteLine($"{hcon.username} with id {hcon.id} has disconnected!".Pastel(Color.FromArgb(255, 0, 0))); Console.WriteLine(e.ToString()); break; } } while (true); } public struct Connection { public NetworkStream stream; public StreamWriter streamw; public StreamReader streamr; public string username; public string id; } } }
客户端核心代码(原代码)
public void Connect() { MultiPlayerSubMenu.transform.GetChild(2).gameObject.GetComponent<Button>().enabled = false; MultiPlayerSubMenu.transform.GetChild(2).GetChild(0).gameObject.GetComponent<TextMeshProUGUI>().text = "Connecting..."; try { client.Connect(IPAddress.Parse(Ip.GetComponent<InputField>().text), Convert.ToInt32(Port.GetComponent<InputField>().text)); if (client.Connected) { t = new Thread(Listen); stream = client.GetStream(); streamw = new StreamWriter(stream); streamr = new StreamReader(stream); streamw.AutoFlush = true; streamw.WriteLine(Username); streamw.WriteLine(Id); t.IsBackground = true; t.Start(); StartCoroutine(Yes()); } else { MultiPlayerSubMenu.transform.GetChild(2).GetChild(0).gameObject.GetComponent<TextMeshProUGUI>().text = "Cant Connect To Server!"; MultiPlayerSubMenu.transform.GetChild(2).gameObject.GetComponent<Image>().color = Color.red; StartCoroutine(No()); } } catch (Exception e) { Logger.LogInfo(e.Message); MultiPlayerSubMenu.transform.GetChild(2).GetChild(0).gameObject.GetComponent<TextMeshProUGUI>().text = "Cant Connect To Server!"; MultiPlayerSubMenu.transform.GetChild(2).gameObject.GetComponent<Image>().color = Color.red; StartCoroutine(No()); } } public void Listen() { while (client.Connected) { try { string message = streamr.ReadLine(); DoEvent(message); Logger.LogInfo(message); } catch(Exception e) { if (!client.Connected) { LostConnection = true; } else { Logger.LogError(e.ToString()); } } } }
问题根源分析
- 全局静态变量
con的线程冲突:所有客户端连接线程共享全局con,新客户端连接会覆盖该变量,导致监听线程拿到错误的连接实例,断开时错误操作其他客户端连接。 Connection使用struct值类型:存入List时会创建拷贝,后续操作的是副本而非原连接,导致状态不同步。- Mutex使用不当:将锁放在客户端遍历循环内,每次写一个客户端都加锁释放,若某个客户端写入失败,可能导致Mutex未释放,引发死锁。
- 监听逻辑错误:在遍历所有客户端时调用
hcon.streamr.ReadLine(),线程会阻塞直到该客户端发送数据,若客户端断开,异常会直接终止广播逻辑,且错误使用全局con构造离开事件。 - 资源未清理:客户端断开时未关闭TcpClient、Stream等资源,引发资源泄漏和连锁异常。
修复方案及代码修改
1. 将Connection改为class引用类型
public class Connection { public TcpClient Client { get; set; } public NetworkStream Stream { get; set; } public StreamWriter StreamWriter { get; set; } public StreamReader StreamReader { get; set; } public string Username { get; set; } public string Id { get; set; } }
2. 移除全局con,通过线程参数传递连接实例
修改Start方法中的客户端连接处理逻辑:
public static void Start(string ip, int port) { Console.WriteLine("Server Ready!".Pastel(Color.FromArgb(58, 255, 0))); server = new TcpListener(IPAddress.Parse(ip), port); server.Start(); while (true) { var newClient = server.AcceptTcpClient(); var newConnection = new Connection { Client = newClient, Stream = newClient.GetStream(), StreamReader = new StreamReader(newClient.GetStream()), StreamWriter = new StreamWriter(newClient.GetStream()) }; newConnection.Username = newConnection.StreamReader.ReadLine(); newConnection.Id = newConnection.StreamReader.ReadLine(); // 广播新客户端加入事件给现有客户端 var joinEvent = new Event { EventType = EventType.Join, Username = newConnection.Username, Id = newConnection.Id }; string joinJson = JsonConvert.SerializeObject(joinEvent); mutex.WaitOne(); try { foreach (var c in list) { c.StreamWriter.WriteLine(joinJson); c.StreamWriter.Flush(); // 向新客户端广播现有客户端信息 var existingJoinEvent = new Event { EventType = EventType.Join, Username = c.Username, Id = c.Id }; newConnection.StreamWriter.WriteLine(JsonConvert.SerializeObject(existingJoinEvent)); newConnection.StreamWriter.Flush(); } } finally { mutex.ReleaseMutex(); } list.Add(newConnection); // 发送当前玩家列表给新客户端 var listEvent = new Event { EventType = EventType.List }; foreach (var c in list) { listEvent.List.Add(c.Username, c.Id); } string listJson = JsonConvert.SerializeObject(listEvent); newConnection.StreamWriter.WriteLine(listJson); newConnection.StreamWriter.Flush(); Console.WriteLine($"{newConnection.Username} with id {newConnection.Id} has connected!".Pastel(Color.FromArgb(58, 255, 0))); // 传递连接实例给监听线程 var listenThread = new Thread(() => HearConnection(newConnection)); listenThread.Start(); } }
3. 重构HearConnection方法,优化监听与广播逻辑
public static void HearConnection(Connection hcon) { try { string message; // 持续读取客户端消息,直到ReadLine返回null(客户端正常断开) while ((message = hcon.StreamReader.ReadLine()) != null) { mutex.WaitOne(); try { // 遍历所有客户端广播消息,单个客户端发送失败单独处理 foreach (var c in list.ToList()) { if (c == hcon) continue; // 可选:跳过发送给自己 try { c.StreamWriter.WriteLine(message); c.StreamWriter.Flush(); } catch (Exception ex) { Console.WriteLine($"Failed to send message to {c.Username}: {ex.Message}".Pastel(Color.FromArgb(255, 0, 0))); CleanupConnection(c); } } } finally { mutex.ReleaseMutex(); // 确保锁一定会释放 } } } catch (Exception e) { Console.WriteLine($"Error listening to {hcon.Username}: {e.Message}".Pastel(Color.FromArgb(255, 0, 0))); } finally { // 清理当前断开的连接 CleanupConnection(hcon); // 广播客户端离开事件 var leaveEvent = new Event { EventType = EventType.Leave, Username = hcon.Username, Id = hcon.Id }; string leaveJson = JsonConvert.SerializeObject(leaveEvent); mutex.WaitOne(); try { foreach (var c in list.ToList()) { try { c.StreamWriter.WriteLine(leaveJson); c.StreamWriter.Flush(); } catch { CleanupConnection(c); } } } finally { mutex.ReleaseMutex(); } Console.WriteLine($"{hcon.Username} with id {hcon.Id} has disconnected!".Pastel(Color.FromArgb(255, 0, 0))); } } // 新增连接清理方法,确保资源释放 public static void CleanupConnection(Connection conn) { if (list.Remove(conn)) { try { conn.StreamWriter?.Close(); conn.StreamReader?.Close(); conn.Stream?.Close(); conn.Client?.Close(); } catch (Exception ex) { Console.WriteLine($"Error cleaning up connection for {conn.Username}: {ex.Message}".Pastel(Color.FromArgb(255, 0, 0))); } } }
关键修复点总结
- 用class替代struct存储连接信息,避免值拷贝导致的状态不同步
- 移除全局共享的
con变量,通过线程参数传递当前连接实例,解决线程冲突 - 调整Mutex作用范围,确保整个广播过程加锁,并在finally块中释放,防止死锁
- 单独处理每个客户端的发送异常,避免单个客户端故障影响全局广播
- 新增连接清理方法,客户端断开时释放所有相关资源,避免资源泄漏
- 优化消息读取逻辑,直到
ReadLine()返回null再判定客户端断开,提升异常处理准确性
内容的提问来源于stack exchange,提问作者Poligamer YT
相关产品推荐
相关产品推荐

