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个,按影响优先级排序:
- 全局共享接收缓冲区导致数据竞争
代码中定义了静态全局的buffer字节数组,所有客户端连接的BeginReceive操作都向这同一块内存写入数据。两个连接并行传输时,两个异步IO操作会互相覆盖缓冲区内容,直接导致解析出的数据乱码、校验失败,触发连接断开逻辑,严重时还会出现内存访问违规,直接导致连接异常终止。 - 非线程安全集合导致的并发异常
存储客户端连接的clientSockets是普通List<Socket>,多线程下同时执行Add、Remove、遍历操作会触发集合修改异常,直接中断连接处理流程。 - 不合理的线程与接收逻辑设计
ReceiveCallback中每次收到数据就额外新开一个线程处理,同时在数据未处理完成时就可能触发下一次BeginReceive,进一步加剧数据竞争问题;另外将二进制文件内容直接按UTF8编码转字符串,会导致二进制数据损坏,触发解析错误。
修复方案
- 为每个连接创建独立的接收上下文
取消全局共享缓冲区,为每个接入的客户端创建独立的状态对象,持有专属缓冲区、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; } } - 替换为线程安全的客户端存储集合
使用ConcurrentDictionary<Socket, ClientState>存储活跃客户端,支持多线程下安全的增删改查操作:public static readonly ConcurrentDictionary<Socket, ClientState> clientSockets = new ConcurrentDictionary<Socket, ClientState>(); - 调整异步接收逻辑
接收操作传入对应连接的独立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); } - 优化文件传输逻辑
不要将二进制文件内容按UTF8转码处理,自定义简单的传输协议:先发8字节标识文件总长度,再发4字节标识文件名长度,接着发文件名的UTF8字节,最后发文件二进制内容,服务端按协议分段解析、写入文件流即可。
内容的提问来源于stack exchange,提问作者Christoffer
相关产品推荐
相关产品推荐

