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

C# TCP Socket服务端多连接传文件时首个连接异常断开问题

问题描述
  • 基于Socket实现文件传输功能时,同时建立两个Socket连接并行传输两个文件,第一个Socket连接会异常中断失效。
  • 参考过多个开发者的Socket服务端实现,但不同版本逻辑差异较大,无法定位自身代码问题,需要错误排查方向与解决思路。

问题复现代码

public static List<FileUppload> uploadlist = new List<FileUppload>();

private static readonly Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
public static readonly List<Socket> clientSockets = new List<Socket>();
private const int BUFFER_SIZE = 10485760; //2048 2097152
private const int PORT = 100;
private static readonly byte[] buffer = new byte[BUFFER_SIZE];

static void Main(string[] args)
{
    Console.Title = "Server";
    SetupServer();
    Console.ReadLine();
    CloseAllSockets();
}

private static void SetupServer()
{
    Console.WriteLine("File server");
    serverSocket.Bind(new IPEndPoint(IPAddress.Any, PORT));
    serverSocket.Listen(0);
    serverSocket.BeginAccept(AcceptCallback, null);
    Console.WriteLine("File server har startat");
}

private static void AcceptCallback(IAsyncResult AR)
{
    Socket socket;

    try
    {
        socket = serverSocket.EndAccept(AR);
    }
    catch (ObjectDisposedException)
    {
        return;
    }

    clientSockets.Add(socket);
    Console.Title = "Server Anslugningar: " + clientSockets.Count;
    socket.BeginReceive(buffer, 0, BUFFER_SIZE, SocketFlags.None, ReceiveCallback, socket);
    Console.WriteLine("Client connected, waiting for request...");
    serverSocket.BeginAccept(AcceptCallback, null);
}

private static void ReceiveCallback(IAsyncResult AR)
{
    ThreadStart ts = delegate
    {
            #region encoding
            Socket current = (Socket)AR.AsyncState;
        int received;

        try
        {
            received = current.EndReceive(AR);
        }
        catch (SocketException)
        {
            Console.WriteLine("negerkuk");
                clientKick(current);
        return;
        }

        byte[] recBuf = new byte[received];
        Array.Copy(buffer, recBuf, received);
        string text = Encoding.UTF8.GetString(recBuf);

        try
        {
            if (!Receive.GetData(text, current)) 
            { 
                Console.WriteLine("negerfitta"); 
                return; 
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
                clientKick(current);
        return;
        }
            #endregion

            current.BeginReceive(buffer, 0, BUFFER_SIZE, SocketFlags.None, ReceiveCallback, current);
    };
    new Thread(ts).Start();
}

#region function
private static void CloseAllSockets()
{
    foreach (Socket socket in clientSockets)
    {
        socket.Shutdown(SocketShutdown.Both);
        socket.Close();
    }

    serverSocket.Close();
}
public static void clientSend(Socket socket, string text)
{
    byte[] data = Encoding.UTF8.GetBytes(text);
    socket.Send(data);
}
private static void clientKick(Socket socket)
{
    Console.WriteLine("Client forcefully disconnected");
    socket.Close();
    clientSockets.Remove(socket);
    Console.Title = "Server Anslugningar: " + clientSockets.Count;
}
private static void clientSendAll(string text)
{
    foreach (Socket socket in clientSockets)
    {
        clientSend(socket, text);
    }
}

public static string read(string path)
{
    try
    {
        if (!File.Exists(path)) return string.Empty;

        using (FileStream file = File.OpenRead(path))
        {
            using (SHA1Managed checksum = new SHA1Managed())
            {
                return BitConverter.ToString(checksum.ComputeHash(file))
                    .Replace("-", string.Empty).ToLower();
            }
        }
    }
    catch { }

    return string.Empty;
}
#endregion
根因分析

并行传输时连接中断的核心原因有3个,按影响优先级排序:

  1. 全局共享接收缓冲区导致数据竞争
    代码中定义了静态全局的buffer字节数组,所有客户端连接的BeginReceive操作都向这同一块内存写入数据。两个连接并行传输时,两个异步IO操作会互相覆盖缓冲区内容,直接导致解析出的数据乱码、校验失败,触发连接断开逻辑,严重时还会出现内存访问违规,直接导致连接异常终止。
  2. 非线程安全集合导致的并发异常
    存储客户端连接的clientSockets是普通List<Socket>,多线程下同时执行Add、Remove、遍历操作会触发集合修改异常,直接中断连接处理流程。
  3. 不合理的线程与接收逻辑设计
    ReceiveCallback中每次收到数据就额外新开一个线程处理,同时在数据未处理完成时就可能触发下一次BeginReceive,进一步加剧数据竞争问题;另外将二进制文件内容直接按UTF8编码转字符串,会导致二进制数据损坏,触发解析错误。
修复方案
  1. 为每个连接创建独立的接收上下文
    取消全局共享缓冲区,为每个接入的客户端创建独立的状态对象,持有专属缓冲区、Socket实例、文件传输状态,从根本上避免跨连接的数据覆盖:
    public class ClientState
    {
        public Socket ClientSocket { get; set; }
        // 单连接分配1MB缓冲区即可,无需10MB的全局大缓冲区
        public byte[] Buffer { get; set; } = new byte[1024 * 1024];
        // 文件传输专属状态
        public FileStream CurrentFileStream { get; set; }
        public long TotalFileSize { get; set; }
        public long ReceivedSize { get; set; }
        public string FileName { get; set; }
    }
    
  2. 替换为线程安全的客户端存储集合
    使用ConcurrentDictionary<Socket, ClientState>存储活跃客户端,支持多线程下安全的增删改查操作:
    public static readonly ConcurrentDictionary<Socket, ClientState> clientSockets = new ConcurrentDictionary<Socket, ClientState>();
    
  3. 调整异步接收逻辑
    接收操作传入对应连接的独立ClientState对象,数据从连接专属缓冲区读取,处理完当前批次数据后再启动下一次接收,移除不必要的手动开线程逻辑(异步回调本身运行在线程池线程,无需额外开线程)。关键修改示例:
    private static void AcceptCallback(IAsyncResult AR)
    {
        Socket socket;
        try
        {
            socket = serverSocket.EndAccept(AR);
        }
        catch (ObjectDisposedException)
        {
            return;
        }
        // 为新连接创建独立状态
        var state = new ClientState { ClientSocket = socket };
        clientSockets.TryAdd(socket, state);
        Console.Title = $"Server 连接数: {clientSockets.Count}";
        // 用连接专属缓冲区启动接收
        socket.BeginReceive(state.Buffer, 0, state.Buffer.Length, SocketFlags.None, ReceiveCallback, state);
        Console.WriteLine("客户端已连接,等待请求...");
        serverSocket.BeginAccept(AcceptCallback, null);
    }
    
    private static void ReceiveCallback(IAsyncResult AR)
    {
        var state = (ClientState)AR.AsyncState;
        var currentSocket = state.ClientSocket;
        int receivedLen;
        try
        {
            receivedLen = currentSocket.EndReceive(AR);
        }
        catch (SocketException)
        {
            clientKick(currentSocket);
            return;
        }
        // 直接从连接专属缓冲区拷贝数据,不会被其他连接覆盖
        var recBuf = new byte[receivedLen];
        Array.Copy(state.Buffer, recBuf, receivedLen);
        
        try
        {
            // 文件传输直接处理字节数据,不要转UTF8字符串,先实现固定格式的传输协议(文件长度->文件名->文件内容)
            if (!Receive.GetData(recBuf, state))
            {
                return;
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
            clientKick(currentSocket);
            return;
        }
        // 处理完当前数据再启动下一次接收
        currentSocket.BeginReceive(state.Buffer, 0, state.Buffer.Length, SocketFlags.None, ReceiveCallback, state);
    }
    
  4. 优化文件传输逻辑
    不要将二进制文件内容按UTF8转码处理,自定义简单的传输协议:先发8字节标识文件总长度,再发4字节标识文件名长度,接着发文件名的UTF8字节,最后发文件二进制内容,服务端按协议分段解析、写入文件流即可。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.01 00:01:36