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)
需解决:
- 问题根源是什么?(原AS3客户端正常,排除服务器问题)
- 是否有更优的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
相关产品推荐
相关产品推荐

