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

多线程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());
            }
        }
    }
}

问题根源分析

  1. 全局静态变量con的线程冲突:所有客户端连接线程共享全局con,新客户端连接会覆盖该变量,导致监听线程拿到错误的连接实例,断开时错误操作其他客户端连接。
  2. Connection使用struct值类型:存入List时会创建拷贝,后续操作的是副本而非原连接,导致状态不同步。
  3. Mutex使用不当:将锁放在客户端遍历循环内,每次写一个客户端都加锁释放,若某个客户端写入失败,可能导致Mutex未释放,引发死锁。
  4. 监听逻辑错误:在遍历所有客户端时调用hcon.streamr.ReadLine(),线程会阻塞直到该客户端发送数据,若客户端断开,异常会直接终止广播逻辑,且错误使用全局con构造离开事件。
  5. 资源未清理:客户端断开时未关闭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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.09 07:15:35