Java TCP读输入流解析报文触发BufferUnderflowException问题
粘包半包的本质原因
TCP是面向字节流的传输协议,不存在应用层意义上的“报文边界”,协议本身只负责保证字节的顺序、不丢不重,不会对应用层发送的报文做拆分、合并的边界对齐:
- 当一次发送多个小报文时,TCP可能会把多个报文打包成一个TCP段发送,对应到读取端就是一次
read读到多个叠在一起的报文,也就是常说的“粘包” - 当单个报文长度超过TCP最大段大小、或者网络传输中被分片时,TCP可能会把单个报文拆成多个段依次发送,对应到读取端就是一次
read只能读到报文的前半段,也就是“半包”
原代码抛出BufferUnderflowException,本质是把单次read读到的字节当成了“完整、边界对齐的报文集合”,既没有留存上次没解析完的半截字节,也没判断当前读到的字节是否足够凑成一条完整报文,遇到最后只剩个报头的截断情况,自然会去访问不存在的字节。
符合固定缓冲区要求的实现方案
结合单条报文最大长度20字节的约束,不需要为了适配高频发送场景开大接收缓冲区,核心逻辑是把“从socket读字节”和“解析完整报文”解耦,用一个固定大小的累积缓存,暂存所有没解析完的半截字节,凑齐足够长度再解析:
- 固定大小的累积缓存仅需20字节:因为单条报文最长20字节,残留的半截报文最长不会超过20字节,完全不存在内存膨胀的问题
- 每次从socket读到新数据时,不要直接从零开始解析,先把新字节拼到累积缓存的已有残留字节后面
- 按报文格式逐段解析:
- 先取当前位置的1个字节作为载荷长度值,计算整条报文需要的总长度(1字节报头+载荷长度)
- 如果当前累积缓存里的剩余字节够凑齐整条报文,就切出来做业务处理,把解析指针移到这条报文的末尾继续
- 如果剩余字节不够,立刻停止解析,把剩下的未处理字节整体挪到累积缓存开头,等下一次读到新字节后拼进来再凑
- 所有解析完的报文对应的字节直接挪走,不要把残留字节覆盖丢失
参考实现代码
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
相关产品推荐
相关产品推荐

