You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Unity中TCP/IP数据传输时程序卡顿的解决方案咨询

解决Unity TCP服务器卡顿与BeginRead异常问题

你的核心问题在于服务器端阻塞式读取导致Unity主线程卡顿,以及异步读取时资源过早释放引发的异常。我们一步步拆解问题并给出针对性优化方案:

一、问题根源分析

服务器端卡顿原因

  1. 超大缓冲区浪费资源bytes = new Byte[Int32.MaxValue]; 创建了接近2GB的数组,不仅占用巨量内存,还会大幅降低读取效率。
  2. 异步读取被强行同步result.AsyncWaitHandle.WaitOne()BeginRead的异步逻辑硬改成同步等待,和直接用stream.Read没有区别,完全失去了异步的意义。
  3. 资源过早释放using (stream = connectedTcpClient.GetStream()) 会在代码块结束时自动释放stream,但此时异步读取操作可能还未完成,直接导致客户端出现“Stream/socket已释放仍被访问”的异常。

客户端潜在问题

  1. 废弃API使用WWW类已被Unity标记为废弃,同步读取大文件会阻塞主线程。
  2. 发送逻辑不完整:缺少发送后的资源清理与状态确认,容易引发连接异常。

二、服务器端优化方案

我们重构读取逻辑,正确使用异步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

火山引擎 最新活动