粘包与拆包是TCP流式特性的必然表现,本质是消息边界缺失;解决关键在于识别边界,常用方案为长度字段前缀(推荐,抗干扰强)和特殊分隔符(适合文本场景),均需精细管理ByteBuffer的position、limit、capacity。

在 NIO 原生开发中,粘包与拆包不是 Bug,而是 TCP 流式特性的必然表现。关键不在于“防止”,而在于“识别边界”——用长度字段或分隔符把一串连续字节切分成逻辑完整的消息。下面直接讲两种最常用、最可控的实战方案。
用长度字段前缀实现可靠解析
这是生产环境首选方式,抗干扰强、无歧义、适合二进制协议。
-
协议设计:每条消息开头固定 2 或 4 字节(推荐大端序),表示后续 payload 的真实字节数。例如:
0x00 0x0A + "HELLO"表示后面跟 10 字节数据(注意:长度字段本身是否计入总长需提前约定)。 -
接收端逻辑:
- 先尝试读够长度字段(如 2 字节)。不够?缓存等待下次 read()。
- 读到后解析出 length 值,做基础校验(如 ≤ 1MB、≥ 0)。
- 再尝试读取 length 字节。若 buffer 中已有部分数据,用
buffer.get(byteArray, offset, remaining)拷贝;若不足,调用compact()保留未读内容,等下一轮。 - 拼成完整消息后,重置 buffer 状态(
clear()或新建 buffer),继续下一轮。
- 注意点:发送端必须严格按协议写——先写长度、再写内容;避免 length 字段被截断(比如只读到 1 字节),否则整个解析链会错位。
用特殊分隔符做文本类协议的轻量解析
适合日志上报、指令交互等纯文本场景,实现简单但对消息体有约束。
-
协议设计:统一用不可见或低概率出现的字节作结尾,如
、,或自定义如0x00 0x00。严禁在业务数据中直接出现该序列——要么转义(如→ \n),要么要求发送方预处理。 -
接收端逻辑:
- 每次 read() 后调用
flip()进入读模式,遍历 buffer 查找分隔符位置(可用for循环 +get(i),或用ByteBuffer.array()转为 byte[] 后用indexOf)。 - 找到首个分隔符,提取从 position 到该位置前的所有字节作为一条完整消息。
- 更新 buffer 的 position 到分隔符后一位,再调用
compact()把剩余未解析数据移到开头,为下次读取准备。 - 若没找到分隔符,说明是半包,不处理,等待下一次 read() 补齐。
- 每次 read() 后调用
-
注意点:不要用
toString()直接转整个 buffer——可能含未解析的半包或多个粘连消息;也不要每次读完就clear(),会丢掉残留数据。
绕不开的 ByteBuffer 管理细节
无论选哪种策略,NIO 原生开发都得亲手管好 buffer 的三个指针:
- position:下一次读/写的起始索引,read() 后它指向新数据末尾;flip() 后它归零;compact() 后它指向已复制数据的末尾。
- limit:读/写操作的上限。read() 后 limit 仍为 capacity;flip() 后 limit = position(即有效数据长度);compact() 后 limit = capacity − 已复制字节数。
- capacity:buffer 固定容量,不能变。建议设为略大于最大单条消息(如 8KB),避免频繁扩容或截断。
为什么不建议手写、而推荐 Netty
上面逻辑看似清晰,但高并发下极易出错:比如多线程竞争 buffer、异常时未 reset 状态、长度越界未防护、分隔符扫描性能差……Netty 的 LengthFieldBasedFrameDecoder 和 LineBasedFrameDecoder 已封装全部边界处理,你只需配置参数和写业务解码器。原生 NIO 适合理解原理,上线系统优先用成熟框架。










