Unity中TCP/IP数据传输时程序卡顿的解决方案咨询
解决Unity TCP服务器卡顿与BeginRead异常问题
你的核心问题在于服务器端阻塞式读取导致Unity主线程卡顿,以及异步读取时资源过早释放引发的异常。我们一步步拆解问题并给出针对性优化方案:
一、问题根源分析
服务器端卡顿原因
- 超大缓冲区浪费资源:
bytes = new Byte[Int32.MaxValue];创建了接近2GB的数组,不仅占用巨量内存,还会大幅降低读取效率。 - 异步读取被强行同步:
result.AsyncWaitHandle.WaitOne()把BeginRead的异步逻辑硬改成同步等待,和直接用stream.Read没有区别,完全失去了异步的意义。 - 资源过早释放:
using (stream = connectedTcpClient.GetStream())会在代码块结束时自动释放stream,但此时异步读取操作可能还未完成,直接导致客户端出现“Stream/socket已释放仍被访问”的异常。
客户端潜在问题
- 废弃API使用:
WWW类已被Unity标记为废弃,同步读取大文件会阻塞主线程。 - 发送逻辑不完整:缺少发送后的资源清理与状态确认,容易引发连接异常。
二、服务器端优化方案
我们重构读取逻辑,正确使用异步IO,避免阻塞,同时合理管理资源:
1. 核心优化代码
private TcpListener tcpListener; private const int BUFFER_SIZE = 4096; // 用合理的缓冲大小替代超大数组 private string savePath; private int fileCount = 0; public void StartServer() { Thread listenerThread = new Thread(ListenForClients); listenerThread.IsBackground = true; listenerThread.Start(); startServerButton.interactable = false; savePath = UnityEditor.EditorUtility.SaveFolderPanel("选择文件保存路径", "", ""); } private void ListenForClients() { try { tcpListener = new TcpListener(IPAddress.Parse("127.0.0.1"), 8052); tcpListener.Start(); Debug.Log("服务器已启动,等待客户端连接..."); while (true) { // 阻塞等待客户端连接(在后台线程,不影响Unity主线程) TcpClient client = tcpListener.AcceptTcpClient(); Debug.Log("新客户端已连接"); // 为每个客户端创建独立的接收状态,避免上下文丢失 FileReceiveState state = new FileReceiveState { Client = client, Stream = client.GetStream(), Buffer = new byte[BUFFER_SIZE], ReceivedData = new MemoryStream(), SaveRootPath = savePath, FileIndex = ++fileCount }; // 启动异步读取,无需同步等待 state.Stream.BeginRead(state.Buffer, 0, state.Buffer.Length, OnDataReceived, state); } } catch (SocketException ex) { Debug.Log($"Socket异常: {ex.Message}"); } } // 保存异步读取的上下文状态 private class FileReceiveState { public TcpClient Client { get; set; } public NetworkStream Stream { get; set; } public byte[] Buffer { get; set; } public MemoryStream ReceivedData { get; set; } public string SaveRootPath { get; set; } public int FileIndex { get; set; } public int? FileNameLength { get; set; } // 标记是否已读取文件名长度 } private void OnDataReceived(IAsyncResult ar) { FileReceiveState state = (FileReceiveState)ar.AsyncState; try { int bytesRead = state.Stream.EndRead(ar); if (bytesRead == 0) { // 客户端关闭连接,处理已接收的完整文件 ProcessReceivedFile(state); CleanupResources(state); return; } // 将读取到的数据写入内存流暂存 state.ReceivedData.Write(state.Buffer, 0, bytesRead); // 检查是否已获取文件名长度(前4字节) if (!state.FileNameLength.HasValue && state.ReceivedData.Length >= 4) { byte[] lenBytes = new byte[4]; state.ReceivedData.Position = 0; state.ReceivedData.Read(lenBytes, 0, 4); state.FileNameLength = BitConverter.ToInt32(lenBytes, 0); state.ReceivedData.Position = state.ReceivedData.Length; // 回到流末尾继续读取 } // 继续异步读取下一批数据 state.Stream.BeginRead(state.Buffer, 0, state.Buffer.Length, OnDataReceived, state); } catch (Exception ex) { Debug.Log($"数据读取异常: {ex.Message}"); CleanupResources(state); } } private void ProcessReceivedFile(FileReceiveState state) { if (!state.FileNameLength.HasValue) { Debug.Log("未接收到完整的文件名长度信息"); return; } try { // 读取文件名 state.ReceivedData.Position = 4; // 跳过前4字节的长度标识 byte[] fileNameBytes = new byte[state.FileNameLength.Value]; state.ReceivedData.Read(fileNameBytes, 0, fileNameBytes.Length); string fileName = Encoding.ASCII.GetString(fileNameBytes); string fileExt = Path.GetExtension(fileName); // 读取文件内容 byte[] fileData = new byte[state.ReceivedData.Length - 4 - state.FileNameLength.Value]; state.ReceivedData.Read(fileData, 0, fileData.Length); // 保存文件 string finalPath = Path.Combine(state.SaveRootPath, $"temp{state.FileIndex}{fileExt}"); using (BinaryWriter writer = new BinaryWriter(File.Open(finalPath, FileMode.Create))) { writer.Write(fileData); } Debug.Log($"文件保存成功: {finalPath}"); } catch (Exception ex) { Debug.Log($"文件处理异常: {ex.Message}"); } } private void CleanupResources(FileReceiveState state) { state.Stream?.Close(); state.Client?.Close(); state.ReceivedData?.Dispose(); }
三、客户端代码优化
替换废弃API,改用异步读取与发送,避免主线程卡顿:
private TcpClient client; private NetworkStream stream; public async void SendFileToServer() { string filePath = UnityEditor.EditorUtility.OpenFilePanel("选择要发送的文件", "", ""); if (string.IsNullOrEmpty(filePath)) return; try { // 异步读取文件,不阻塞主线程 byte[] fileData = await File.ReadAllBytesAsync(filePath); byte[] fileNameBytes = Encoding.ASCII.GetBytes(filePath); byte[] fileNameLength = BitConverter.GetBytes(fileNameBytes.Length); // 组装发送数据包 byte[] sendData = new byte[4 + fileNameBytes.Length + fileData.Length]; fileNameLength.CopyTo(sendData, 0); fileNameBytes.CopyTo(sendData, 4); fileData.CopyTo(sendData, 4 + fileNameBytes.Length); // 连接服务器并异步发送 client = new TcpClient("127.0.0.1", 8052); stream = client.GetStream(); await stream.WriteAsync(sendData, 0, sendData.Length); await stream.FlushAsync(); Debug.Log($"文件已发送至服务器: {filePath}"); // 发送完成后清理资源 stream.Close(); client.Close(); } catch (Exception ex) { Debug.Log($"文件发送异常: {ex.Message}"); stream?.Close(); client?.Close(); } }
四、额外注意事项
- Unity线程安全:如果需要在异步回调中操作UI或场景对象,必须通过
UnityMainThreadDispatcher或者Invoke回到主线程执行,Unity API大多不支持跨线程调用。 - 异常边界处理:增加对网络断开、文件读写失败等场景的捕获逻辑,提升程序稳定性。
- 缓冲区调整:根据你传输的文件平均大小,可适当调整
BUFFER_SIZE(比如8192字节),平衡内存占用与读取效率。
内容的提问来源于stack exchange,提问作者Siva




