Unity游戏C# TCP服务端大批量消息丢失问题排查与解决咨询
Unity TCP客户端消息丢失问题修复方案
问题定位
本地测试正常,但部署到远程服务器(AWS EC2/家用服务器)后,发送100+条消息时客户端无法接收全部内容,且出现packetLength=16777216(2^24)的异常值。已排除防火墙问题,问题根源在客户端数据包解析逻辑,同时服务端发送逻辑也存在优化空间。
核心问题分析
- 粘包导致解析错位:TCP是流协议,多次发送的数据包会被合并传输。当前客户端缓存处理逻辑错误,未正确保留未处理的剩余数据,导致长度字段读取错位,解析出异常长度值。
- 缓存重置逻辑错误:
ReceiveCallback中调用receivedData.Reset(HandleData(_data)),若HandleData返回false(存在未处理数据),Reset会清空缓存,直接丢失剩余数据。 - 字节序不匹配:服务端与客户端的
ReadInt()/WriteInt()字节序不一致,小端数据被当成大端读取时,会出现0x00000010变成0x10000000(即16777216)的异常。 - 服务端无发送确认:使用
BeginWrite无回调,无法确认数据是否发送成功,快速发送大量消息时可能导致缓冲区溢出。
修复方案
一、修正客户端数据包解析逻辑
1. 重构缓存与接收回调逻辑
取消错误的Reset操作,改为将新数据追加到缓存,保留未处理的剩余数据:
private void ReceiveCallback(IAsyncResult _result){ try { int _byteLength = stream.EndRead(_result); if (_byteLength <= 0){ client.Disconnect(); return; } // 将新接收的数据追加到缓存,而非覆盖重置 receivedData.AppendBytes(receiveBuffer, 0, _byteLength); // 处理所有能解析的完整数据包 HandleCompletePackets(); stream.BeginRead(receiveBuffer, 0, dataBufferSize, ReceiveCallback, null); } catch(Exception _ex) { Disconnect(); Debug.Log(_ex); } }
2. 优化数据包解析循环
使用预览长度替代直接读取,避免因数据不足导致的解析错位,同时增加非法长度校验:
private void HandleCompletePackets(){ const int MAX_PACKET_SIZE = 1024 * 1024; // 限制最大包大小,防止恶意数据 while (receivedData.UnreadLength() >= 4){ // 先预览长度,不移动读取指针 int _packetLength = receivedData.PeekInt(); if (_packetLength <= 0 || _packetLength > MAX_PACKET_SIZE){ Debug.LogError($"非法数据包长度: {_packetLength},断开连接"); client.Disconnect(); return; } // 确认缓存中有足够数据后,再正式读取 if (receivedData.UnreadLength() >= 4 + _packetLength){ receivedData.ReadInt(); // 跳过预览的长度字段 byte[] _packetBytes = receivedData.ReadBytes(_packetLength); ThreadManager.ExecuteOnMainThread(() => { using (Packet _packet = new Packet(_packetBytes)){ int _packetId = _packet.ReadInt(); if (client.packetHandlers.TryGetValue(_packetId, out var handler)){ handler(_packet); } else { Debug.LogError($"未找到包ID {_packetId}的处理器"); } } }); } else { // 数据不足,等待下一次接收 break; } } }
注:需要给receivedData类添加PeekInt()方法(只读不移动指针)和AppendBytes()方法(追加数据到缓存)。
3. 统一字节序
确保服务端和客户端的整数读写使用相同字节序,比如统一用小端:
- 服务端写入整数时:
BitConverter.GetBytes(_value)(若系统是小端),或强制用BitConverter.GetBytes(_value).Reverse()转为大端(网络字节序) - 客户端读取时对应处理,避免解析错误。
二、优化服务端发送逻辑
添加发送回调确认发送完成,避免缓冲区溢出,同时实现发送队列控制速率:
private Queue<Packet> sendQueue = new Queue<Packet>(); private bool isSending = false; public void SendData(Packet _packet) { if (socket == null) return; lock (sendQueue) { sendQueue.Enqueue(_packet); } if (!isSending) { ProcessSendQueue(); } } private void ProcessSendQueue() { lock (sendQueue) { if (sendQueue.Count == 0) { isSending = false; return; } isSending = true; Packet packet = sendQueue.Dequeue(); stream.BeginWrite(packet.ToArray(), 0, packet.Length(), OnSendCompleted, null); } } private void OnSendCompleted(IAsyncResult ar) { try { stream.EndWrite(ar); } catch (Exception ex) { Console.WriteLine($"发送数据失败: {ex}"); } // 继续发送队列中的下一个包 ProcessSendQueue(); }
三、实现消息确认与未接收回调
通过**请求-响应(ACK)**机制处理消息丢失,触发未接收回调:
客户端ACK逻辑
处理完消息后回复确认包:
// 示例:注册消息处理器时添加ACK回复 client.packetHandlers[(int)PacketIDs.GameMessage] = (packet) => { int msgId = packet.ReadInt(); // 处理业务逻辑... // 发送ACK确认 using (Packet ackPacket = new Packet((int)PacketIDs.ACK)){ ackPacket.Write(msgId); client.SendTCPData(ackPacket); } };
服务端超时与重传逻辑
维护未确认消息列表,超时触发重传或未接收回调:
private Dictionary<int, (Packet packet, DateTime sendTime, int retryCount)> unAckedMessages = new Dictionary<int, (Packet, DateTime, int)>(); private int nextMsgId = 0; private const int MAX_RETRY = 3; private const int TIMEOUT_SECONDS = 5; // 发送可靠消息(需要确认) public void SendReliableData(Packet _packet){ int msgId = nextMsgId++; // 在数据包头部加入唯一消息ID Packet reliablePacket = new Packet(_packet); reliablePacket.InsertInt(0, msgId); // 插入到包开头 SendData(reliablePacket); unAckedMessages[msgId] = (reliablePacket, DateTime.Now, 0); } // 定时检查超时(建议用定时器每秒执行一次) private void CheckMessageTimeout(){ List<int> failedMsgs = new List<int>(); foreach(var kvp in unAckedMessages){ if ((DateTime.Now - kvp.Value.sendTime).TotalSeconds > TIMEOUT_SECONDS){ if (kvp.Value.retryCount < MAX_RETRY){ // 重传消息 SendData(kvp.Value.packet); unAckedMessages[kvp.Key] = (kvp.Value.packet, DateTime.Now, kvp.Value.retryCount + 1); } else { // 重传失败,触发未接收回调 OnMessageUnreceived(kvp.Key, kvp.Value.packet); failedMsgs.Add(kvp.Key); } } } // 清理失败的消息 foreach(int msgId in failedMsgs){ unAckedMessages.Remove(msgId); } } // 处理客户端ACK private void HandleACK(Packet packet){ int msgId = packet.ReadInt(); if (unAckedMessages.ContainsKey(msgId)){ unAckedMessages.Remove(msgId); } } // 自定义未接收回调 private void OnMessageUnreceived(int msgId, Packet packet){ Console.WriteLine($"消息ID {msgId} 重传{MAX_RETRY}次后仍未被客户端接收"); // 这里可添加业务逻辑,比如记录日志、通知玩家等 }
内容的提问来源于stack exchange,提问作者Rick Vink
相关产品推荐
相关产品推荐

