Unity C# Socket多次调用BeginSend数据未全到Java服务器如何解决
问题根因分析
1. TCP协议本身特性
TCP是面向字节流的传输协议,本身没有数据包边界,你在客户端调用4次Send,底层TCP协议栈可能将4份数据合并为一个TCP段发送,也可能拆分为多个段发送。你当前的前后端逻辑都没有处理粘包、拆包场景,就会出现服务端一次拿到所有4份数据但只处理1次的情况,误以为只收到1份数据。
2. 客户端方法2的错误
- 错误使用
Stack<byte[]>存储待发送数据:栈是后进先出结构,会导致先调用SendRequest的数据后发送,发送顺序完全错乱,发送队列应该使用先进先出的Queue<byte[]>实现。 - 未处理半包发送:
BeginSend/EndSend不保证一次能把传入的所有字节全部发送完成,EndSend的返回值是实际成功发送的字节数,你当前代码直接忽略这个值,要是一次没发完,剩余字节会直接丢失。 - 初始状态异常:
IAsyncResult res没有初始化,第一次进入Update判断res.IsComplete会触发空引用异常,可能导致后续发送逻辑直接中断。
3. 服务端逻辑的错误
- 每次read前直接清空
inputBuffer:如果上一次read有未处理的半包数据,会直接被清空丢失。 - 未做粘包拆包处理:一次read可能拿到1个、多个或者半个包,你直接调用一次
handleInput,只会处理一次,剩下的多包数据直接被忽略。 - 未判断read返回值:如果返回-1说明客户端断开连接,返回0说明没有读到有效数据,当前逻辑没有处理这些边界情况,容易出现异常。
修复方案
客户端调整
替换栈为队列,增加半包发送处理逻辑,建议额外给每个包增加4字节的长度包头,方便服务端拆包,参考代码如下:
Queue<byte[]> sendQueue = new Queue<byte[]>(); // 缓存当前发送到一半的包和已发送偏移量 byte[] currentSendingBuffer; int sentOffset = 0; bool isSending = false; void Update() { // 没有正在发送的包且队列有数据,取出下一个待发包 if (!isSending && sendQueue.Count > 0) { lock(sendQueue) // 避免多线程调用SendRequest导致并发异常 { currentSendingBuffer = sendQueue.Dequeue(); } sentOffset = 0; isSending = true; BeginSendCurrentBuffer(); } } void BeginSendCurrentBuffer() { int remaining = currentSendingBuffer.Length - sentOffset; socket.BeginSend(currentSendingBuffer, sentOffset, remaining, SocketFlags.None, OnSendCallback, null); } void OnSendCallback(IAsyncResult ar) { try { int actuallySent = socket.EndSend(ar); sentOffset += actuallySent; // 当前包未发完,继续发送剩余部分 if (sentOffset < currentSendingBuffer.Length) { BeginSendCurrentBuffer(); } // 当前包发完,标记空闲 else { isSending = false; } } catch (SocketException e) { // 自行处理发送异常,比如断开重连逻辑 Debug.LogError($"发送失败:{e.Message}"); isSending = false; } } public void SendRequest(byte[] data) { // 新增4字节长度包头,方便服务端拆包 byte[] lengthHeader = BitConverter.GetBytes(data.Length); byte[] fullPacket = lengthHeader.Concat(data).ToArray(); lock(sendQueue) { sendQueue.Enqueue(fullPacket); } }
服务端调整
给每个连接绑定独立的累计接收缓冲区,按照约定的4字节长度包头解析完整数据包,参考代码如下:
// 给User类新增累计接收缓冲区属性 class User { ByteBuffer recvBuffer = ByteBuffer.allocate(1024 * 8); // 其他原有属性... } public void update(int tick) { selector.selectNow(); Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); while(iterator.hasNext()) { SelectionKey key = iterator.next(); iterator.remove(); if(key.isAcceptable()) { SocketChannel clientSocket = ((ServerSocketChannel)key.channel()).accept(); clientSocket.configureBlocking(false); User user = new User(); clientSocket.register(selector, SelectionKey.OP_READ, user); } else if(key.isReadable()) { SocketChannel clientSocket = (SocketChannel)key.channel(); User user = (User)key.attachment(); ByteBuffer recvBuffer = user.recvBuffer; int readBytes = clientSocket.read(recvBuffer); // 客户端主动断开连接,做资源清理 if (readBytes == -1) { key.cancel(); clientSocket.close(); continue; } // 缓冲区切换为读模式 recvBuffer.flip(); // 循环解析所有完整包 while (recvBuffer.remaining() >= 4) { recvBuffer.mark(); // 标记当前读取位置 int packetLength = recvBuffer.getInt(); // 剩余数据不足一个完整包,回滚标记,等待下一次读取拼接 if (recvBuffer.remaining() < packetLength) { recvBuffer.reset(); break; } // 读取完整包体 byte[] packetData = new byte[packetLength]; recvBuffer.get(packetData); // 处理单个业务包 handleInput(user, packetData); } // 缓冲区切换为写模式,保留未解析的半包数据 recvBuffer.compact(); } } }
内容的提问来源于stack exchange,提问作者이재원
相关产品推荐
相关产品推荐

