selector无法主动发现异常断开,最可靠信号是op_read就绪后read()返回-1,须立即close通道、cancel键并清理上下文;遇ioexception(如“connection reset”)或read()返回0也应同样处理,并配合心跳或tcp keepalive探测假活连接。

Java NIO 中 Selector 本身不会主动“发现”客户端异常断开(如断网、进程崩溃、强制 kill),它只能通过 I/O 事件间接感知——而最可靠、最及时的信号,就是在 OP_READ 就绪后调用 read() 返回 -1。但异常断开与正常关闭(FIN)在 TCP 层表现不同,需结合多种情况综合判断和应对。
OP_READ 就绪 + read() 返回 -1:确认对端已关闭(含异常后残留 FIN)
即使客户端是异常崩溃,只要内核曾发送过 FIN(例如应用层未清理就退出,但 TCP 栈仍完成四次挥手的前半段),本端就会收到该信号。此时:
- Selector 触发 OP_READ 是因为接收缓冲区进入“EOF 状态”,并非真有数据
- channel.read(buffer) 必然返回 -1,这是唯一可信赖的断连依据
- 必须立即执行:channel.close() + key.cancel() + 从上下文容器中移除该 channel
- 切勿仅 close channel 而忽略 cancel —— 否则该失效 key 可能在下次 select 中重复出现,引发空指针或逻辑错乱
OP_READ 就绪 + read() 返回 0 或抛出 IOException:大概率是异常断连或网络中断
非阻塞模式下 read() 返回 0 较少见,但若频繁发生且无业务数据,可能是连接半死;更常见的是直接抛出 IOException,例如:
Java项目代码review工具。分析Git变更+完整调用链路上下文,推断业务需求,进行多维度评分和分类汇总,生成完整PRD文档。包含细粒度Java代码审查清单(Null安全、异常处理、Streams、并发、equals/hashCode、资源管理、API设计、性能、MyBatis/ORM、事务边界、SQL/DD...
- Connection reset by peer:对方 abrupt close(RST 包),典型异常断开
- Broken pipe:尝试向已关闭连接写入时触发(常出现在 WRITE 分支)
- IOException: Bad file descriptor 或 socket closed:本地资源已被误释放
- 遇到这些异常,同样要执行 channel.close() 和 key.cancel(),并清理关联状态
长时间无读写事件?不能单靠超时被动等待
异常断开后,若对端未发 FIN/RST,本端 TCP 连接可能长期处于 ESTABLISHED 状态(即“假活”)。此时 Selector 不会触发任何事件,必须主动探测:
- 为每个连接维护最后通信时间戳(如收/发成功时间)
- 在事件循环中定期扫描,对超时连接发起心跳(如写一个轻量 ping 包)或直接关闭
- 启用 TCP keepalive(socket.setKeepAlive(true)),由内核自动探测,但默认间隔长(2小时),生产环境建议配合应用层心跳
务必手动清理 selectedKeys 集合
每次处理完一批就绪 key 后,必须用迭代器调用 iter.remove()(或显式 keyIterator.remove())。否则该 key 会一直留在 selectedKeys 中,导致:
- 下次 select() 仍返回它,造成重复处理或空指针
- 若未 cancel,即使 channel 已 close,key 仍可能被反复遍历
- 这是 NIO 编程中最容易遗漏却后果严重的一环
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










