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

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,提问作者이재원

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.06 15:27:00