java nio非阻塞写需处理“写半包”:write()可能只写部分数据,须检查返回值并循环续写;返回0需注册op_write等待就绪,返回小于remaining()则保留未写完buffer,返回等于remaining()才完成。

Java NIO 在非阻塞模式下写入 Channel 时,write() 方法可能只写出部分数据(比如 buffer 中有 1024 字节,但实际只写出去 300 字节),这叫“写半包”,不是接收端的半包,而是发送端的写不完整。它和粘包/读半包不同,但同样必须处理,否则消息发不全、连接卡死或逻辑错乱。
理解 write() 的非阻塞行为
非阻塞 SocketChannel.write(ByteBuffer) 不保证一次写完全部内容:它返回实际写出的字节数,可能为 0(缓冲区满、TCP 窗口小)、也可能小于 buffer.remaining()。你不能假设调用一次就完成;必须检查返回值,持续写直到 buffer 清空或遇到阻塞/异常。
- 返回值为 0:不代表错误,只是暂时无法写入(如内核发送缓冲区已满),需等 OP_WRITE 就绪再试
- 返回值大于 0 但小于 remaining():说明只写了一部分,buffer 中剩余数据还需继续写
- 返回值等于 remaining():本次写操作完成,可复用或清理 buffer
正确处理写半包的三步法
核心是:把未写完的 ByteBuffer 持久化、等待可写事件、续写。不能直接 clear() 或 flip(),否则丢数据。
- 注册 OP_WRITE 关注点:仅当 write() 返回值
-
缓存未写完的 buffer:把该 ByteBuffer 存到 Channel 对应的附件(attachment)或独立映射(如 Map
)中,避免 GC 或误复用 - 在 OP_WRITE 处理中续写:收到 OP_WRITE 事件后,从附件取出 buffer,调用 channel.write(buffer);若仍没写完,保持 OP_WRITE 注册;若写完,移除 OP_WRITE 并清理附件
避免常见错误
很多代码在写半包时踩坑,导致数据丢失或无限循环:
- 每次 OP_WRITE 都 new 一个 ByteBuffer —— 浪费内存,且旧数据丢了
- 在 write() 后无条件调用 buffer.clear() —— 把还没写完的数据清掉了
- 一直注册 OP_WRITE 却不取消 —— 触发大量无效轮询,CPU 升高
- 用同一个 buffer 同时读写 —— position/limit 冲突,数据错位
推荐封装写辅助类
业务层不该反复写“判断剩余、注册、缓存、续写”逻辑。建议封装一个 WriteFlusher:
- 提供 flush(SocketChannel ch, ByteBuffer data) 方法:尝试写;若未写完,自动注册 OP_WRITE + 缓存 buffer
- 提供 onWriteReady(SelectionKey key) 回调:内部续写,写完自动取消 OP_WRITE 并触发完成回调
- 支持批量写(合并多个小消息到一个 buffer 再 flush),减少系统调用开销
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











