You need to enable JavaScript to run this app. Java TCP读输入流解析报文触发BufferUnderflowException问题
优惠活动
大模型
产品
解决方案
定价
更多

Java TCP读输入流解析报文触发BufferUnderflowException问题

粘包半包的本质原因

TCP是面向字节流的传输协议,不存在应用层意义上的“报文边界”,协议本身只负责保证字节的顺序、不丢不重,不会对应用层发送的报文做拆分、合并的边界对齐:

  • 当一次发送多个小报文时,TCP可能会把多个报文打包成一个TCP段发送,对应到读取端就是一次read读到多个叠在一起的报文,也就是常说的“粘包”
  • 当单个报文长度超过TCP最大段大小、或者网络传输中被分片时,TCP可能会把单个报文拆成多个段依次发送,对应到读取端就是一次read只能读到报文的前半段,也就是“半包”

原代码抛出BufferUnderflowException,本质是把单次read读到的字节当成了“完整、边界对齐的报文集合”,既没有留存上次没解析完的半截字节,也没判断当前读到的字节是否足够凑成一条完整报文,遇到最后只剩个报头的截断情况,自然会去访问不存在的字节。

符合固定缓冲区要求的实现方案

结合单条报文最大长度20字节的约束,不需要为了适配高频发送场景开大接收缓冲区,核心逻辑是把“从socket读字节”和“解析完整报文”解耦,用一个固定大小的累积缓存,暂存所有没解析完的半截字节,凑齐足够长度再解析:

  1. 固定大小的累积缓存仅需20字节:因为单条报文最长20字节,残留的半截报文最长不会超过20字节,完全不存在内存膨胀的问题
  2. 每次从socket读到新数据时,不要直接从零开始解析,先把新字节拼到累积缓存的已有残留字节后面
  3. 按报文格式逐段解析:
    • 先取当前位置的1个字节作为载荷长度值,计算整条报文需要的总长度(1字节报头+载荷长度)
    • 如果当前累积缓存里的剩余字节够凑齐整条报文,就切出来做业务处理,把解析指针移到这条报文的末尾继续
    • 如果剩余字节不够,立刻停止解析,把剩下的未处理字节整体挪到累积缓存开头,等下一次读到新字节后拼进来再凑
  4. 所有解析完的报文对应的字节直接挪走,不要把残留字节覆盖丢失
参考实现代码
InputStream stream = socket.getInputStream();
// 固定20字节累积区,完全匹配单条报文最大20字节的约束
byte[] cumulativeBuf = new byte[20];
int validBufLen = 0; // 累积区内当前存的有效字节长度
int readBytes;
// 临时socket读取数组,大小可按网络情况调整,不影响逻辑
byte[] tempReadArray = new byte[1024];

while ((readBytes = stream.read(tempReadArray)) != -1) {
    // 把新读到的字节拼到累积区的有效字节后面
    System.arraycopy(tempReadArray, 0, cumulativeBuf, validBufLen, readBytes);
    validBufLen += readBytes;

    int parseCursor = 0;
    while (parseCursor < validBufLen) {
        // 不足1字节,读不到报头,停止解析
        if (validBufLen - parseCursor < 1) break;
        int payloadLength = cumulativeBuf[parseCursor];
        int totalMsgLength = 1 + payloadLength;

        // 校验报文长度合法性,避免异常输入撑爆缓冲区
        if (totalMsgLength > 20) {
            throw new IOException("非法报文长度,超过单报文最大20字节限制");
        }

        // 剩余字节不够拼整条报文,停止解析
        if (validBufLen - parseCursor < totalMsgLength) {
            break;
        }

        // 切出完整报文做业务处理
        byte[] completeMessage = Arrays.copyOfRange(
                cumulativeBuf, parseCursor, parseCursor + totalMsgLength
        );
        handleBusinessMessage(completeMessage);

        parseCursor += totalMsgLength;
    }

    // 把未解析的残留字节挪到累积区开头,给下一次读数据腾位置
    int remainLength = validBufLen - parseCursor;
    System.arraycopy(cumulativeBuf, parseCursor, cumulativeBuf, 0, remainLength);
    validBufLen = remainLength;
}
常见避坑点
  • 不要每次read都重置解析位置:原代码每次read都从新数组的第0位开始解析,直接把上次残留的半截报文数据覆盖丢失,是触发缓冲区下溢的核心原因
  • 不需要为高频发送场景设置超大缓冲区:哪怕用户连续发上千条报文,累积区里始终只会留最多半条未凑齐的报文,内存占用几乎可以忽略,完全可以扛住高并发发送的场景
  • 必须加报文长度合法性校验:如果客户端发送的报头值超过约定的最大长度,要直接判定为非法连接断开,避免缓冲区越界

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.11.09 00:54:03