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

Unity TCP客户端接收Netty服务器JSON消息乱序及解析异常排查

问题

现有一台以空字符(\0)作为分隔符发送JSON消息的Netty游戏服务器,原AS3客户端可正常通信,但新开发的Unity客户端出现通信异常:

  • 消息接收顺序错乱,例如服务器发送Hello\0Socket\0World,客户端按Socket→Hello→World的顺序接收
  • JSON解析频繁报错,长消息问题尤为显著
  • 调整缓冲区大小后问题仍未解决

Unity客户端TCP接收代码:

try {
    _tcpClient = new TcpClient();
    await _tcpClient.ConnectAsync(_host, _port);
    _isListening = true;

    Debug.Log("Connected...");
    UnityMainThreadDispatcher.Instance().Enqueue(DispatchConnected());

    Byte[] bytes = new Byte[BufferSize];
    StringBuilder partialMessage = new();
    while (_isListening && !_stopRequested) {
        if (_tcpClient != null && _tcpClient.Connected) {
            using (NetworkStream stream = _tcpClient.GetStream()) {
                if (stream.CanRead) {
                    try {
                        int bytesRead;
                        while ((bytesRead = stream.Read(bytes, 0, BufferSize)) > 0) {
                            string bufferMessage = Encoding.UTF8.GetString(bytes, 0, bytesRead);

                            // Append the buffer to the existing partial message
                            partialMessage.Append(bufferMessage);

                            // Check if the partial message contains the termination character
                            int terminateIndex;
                            while ((terminateIndex = partialMessage.ToString().IndexOf(TerminateCharacter)) != -1) {
                                string completeMessage = partialMessage.ToString(0, terminateIndex);
                                Debug.Log("R: " + completeMessage);
                                UnityMainThreadDispatcher.Instance().Enqueue(DispatchServerMessage(completeMessage, true)); // <-- This is where I convert to JSON

                                // Remove the processed portion from the partial message
                                partialMessage.Remove(0, terminateIndex + 1);
                            }  
                        }
                    }
                    catch (IOException ioException) {
                        Debug.LogError($"IOException: {ioException.Message}");
                    }
                    catch (Exception exception) {
                        Debug.LogError(exception);
                    }
                }
            }
        }
        else {
            Debug.Log("TCP Client is not connected!");
            ClientDisconnected();
            break; // Break out of the loop when the client is not connected
        }
    }

    // Process any remaining partial message after the loop
    if (partialMessage.Length > 0) {
        UnityMainThreadDispatcher.Instance().Enqueue(DispatchServerMessage(partialMessage.ToString(), true));
        partialMessage.Clear();
    }
}
catch (SocketException socketException) {
    if (socketException.ErrorCode == 10061) {
        // Debug.LogError("Connection refused!!!");
        UnityMainThreadDispatcher.Instance().Enqueue(DispatchConnectionRefused());
    }
    else {
        UnityMainThreadDispatcher.Instance().Enqueue(DispatchConnectionInterrupted());
    }
}
catch (IOException ioException) {
    UnityMainThreadDispatcher.Instance().Enqueue(DispatchConnectionInterrupted());
    // UnityMainThreadDispatcher.Instance().Enqueue(DispatchConnectionRefused());
}
finally
{
    _stopRequested = true; // Ensure the thread stops even if an exception occurs
    _tcpClient?.Close();
    _clientReceiveThread = null;
}

Netty服务器初始化代码:

ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast("timeout", new IdleStateHandler(ServerSettings.MAX_IDLE_TIME_IN_SECONDS, 0, ServerSettings.MAX_IDLE_TIME_IN_SECONDS));
pipeline.addLast(new DelimiterBasedFrameDecoder(1024 * 1024, Delimiters.nulDelimiter()));
pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));// (2)
pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8)); // (1)
pipeline.addLast(new SimpleTCPHandler()); // (3)

需解决:

  1. 问题根源是什么?(原AS3客户端正常,排除服务器问题)
  2. 是否有更优的JSON消息处理方式?

问题根源分析

1. 核心问题:主线程异步调度导致顺序错乱

Unity客户端中,UnityMainThreadDispatcher.Instance().Enqueue()是异步入队操作,无法保证入队顺序与主线程执行顺序完全一致。当短时间内有多条消息入队时,主线程的任务调度可能打乱接收顺序——比如先入队的Hello可能因主线程已有待执行任务,被后入队的Socket抢先执行,最终出现顺序颠倒的现象。

2. 次要问题:字符串处理的低效与编码风险

  • 每次调用partialMessage.ToString().IndexOf(TerminateCharacter)都会生成新字符串副本,长消息场景下频繁触发GC,同时可能因字符串拼接的中间状态引发解析错误。
  • 未考虑UTF-8多字节特性:若\0分隔符被拆分为两次stream.Read读取,虽当前逻辑能拼接处理,但如果缓冲区末尾是不完整的UTF-8字符,转换为字符串时会出现乱码,直接导致JSON解析失败。

优化方案与更优JSON处理方式

1. 修复消息顺序问题

改用线程安全的有序队列+主线程批量处理的模式,先在接收线程按顺序存储消息,再让主线程批量取出并执行,避免异步入队的调度混乱:

// 定义线程安全队列存储消息
private ConcurrentQueue<string> _messageQueue = new ConcurrentQueue<string>();
private bool _isProcessingQueue = false;

// 接收线程中替换原Enqueue逻辑:
_messageQueue.Enqueue(completeMessage);
// 触发主线程处理(避免重复触发)
if (!_isProcessingQueue)
{
    UnityMainThreadDispatcher.Instance().Enqueue(ProcessMessageQueue);
}

// 主线程批量处理队列的方法
private void ProcessMessageQueue()
{
    _isProcessingQueue = true;
    while (_messageQueue.TryDequeue(out string msg))
    {
        DispatchServerMessage(msg, true);
    }
    _isProcessingQueue = false;
}

2. 优化TCP粘包/拆包处理(解决长消息解析错误)

放弃字符串层面的拼接与查找,改用字节数组缓冲区直接在字节层面定位\0分隔符,避免UTF-8编码的中间错误:

private List<byte> _partialBytes = new List<byte>();

// 替换原StringBuilder相关逻辑:
while ((bytesRead = stream.Read(bytes, 0, BufferSize)) > 0)
{
    _partialBytes.AddRange(bytes.Take(bytesRead));
    
    // 查找空字符字节(0x00)
    int nullIndex;
    while ((nullIndex = _partialBytes.IndexOf(0x00)) != -1)
    {
        byte[] messageBytes = _partialBytes.Take(nullIndex).ToArray();
        string completeMessage = Encoding.UTF8.GetString(messageBytes);
        
        // 入队处理
        _messageQueue.Enqueue(completeMessage);
        
        // 移除已处理的字节(包括分隔符)
        _partialBytes.RemoveRange(0, nullIndex + 1);
    }
}

3. 更优的JSON处理方式

  • 前置校验:在解析前先验证消息完整性,比如检查JSON首尾是否为{}或[],过滤空消息或非法格式,避免无效解析报错。
  • 选用高性能库:Unity中推荐使用适配版Newtonsoft.Json或System.Text.Json,相比原生JsonUtility,它们对复杂JSON结构和错误处理的支持更完善,解析效率更高。
  • 批量解析:主线程批量取出队列消息后统一解析,减少重复的库初始化开销,提升处理效率。

验证建议

  • 在接收线程中打印消息,确认接收顺序是否与服务器发送顺序一致:若接收线程内顺序正确,可确定是主线程调度问题;若接收线程内顺序就错乱,再排查TCP接收逻辑。
  • 测试长消息时,对比服务器发送的JSON字符串与客户端接收线程转换后的字符串,排查是否存在字节拼接错误。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 17:57:03