Java NIO中Readable SelectionKey反复出现在SelectedKeys的问题
问题根源分析
你的代码里有三个关键问题导致了重复触发可读事件:
1. 客户端数据发送逻辑错误
客户端代码中,byteBuffer.flip()后立刻调用了byteBuffer.clear(),这会重置缓冲区的position为0、limit为容量,导致后续socketChannel.write(byteBuffer)写入的是缓冲区里的无效空数据,并且客户端直接关闭了连接。
2. 服务器未处理连接关闭场景
当客户端关闭连接后,服务器端的SocketChannel.read()会返回-1,这表示TCP流已经到达末尾。此时如果不取消该SelectionKey并关闭通道,Selector会持续将这个Key标记为可读(EOF属于可读事件的触发条件),导致每次select()都会把它加入selectedKeys集合。
3. 错误注册了OP_CONNECT事件
服务器通过accept()获取的SocketChannel已经处于连接完成状态,OP_CONNECT事件是给客户端SocketChannel使用的(用于监听连接是否建立),服务器端注册这个事件完全多余,还可能干扰Selector的事件判断。
修复方案
修复客户端代码
移除byteBuffer.clear()调用,确保有效数据能正确写入通道:
public class TcpClient { public static void main(String[] args) { SocketAddress socketAddress = new InetSocketAddress("127.0.0.1", 885); SocketChannel socketChannel = null; try { socketChannel = SocketChannel.open(socketAddress); socketChannel.configureBlocking(false); while (!socketChannel.finishConnect()) { } ByteBuffer byteBuffer = ByteBuffer.allocate(240); byteBuffer.put("Hello Kurumi".getBytes()); byteBuffer.flip(); // 移除错误的clear()调用 socketChannel.write(byteBuffer); socketChannel.shutdownOutput(); socketChannel.close(); } catch (IOException e) { System.out.println(e); throw new RuntimeException(e); } } }
修复服务器代码
- 移除不必要的OP_CONNECT事件,注册SocketChannel时只关注OP_READ:
// 替换原注册代码 socketChannel.register(selector, SelectionKey.OP_READ);
- 新增连接关闭处理逻辑,当read()返回-1时取消SelectionKey并关闭通道:
else if (selectionKey.isReadable()) { System.out.println("is readable"); SocketChannel socketChannel = (SocketChannel)selectionKey.channel(); ByteBuffer byteBuffer = ByteBuffer.allocate(24); int readBytes; while ((readBytes = socketChannel.read(byteBuffer)) > 0) { byteBuffer.flip(); System.out.println(byteBuffer.limit()); System.out.println(StandardCharsets.UTF_8.decode(byteBuffer).toString()); byteBuffer.clear(); } // 处理客户端关闭连接的情况 if (readBytes == -1) { System.out.println("客户端已关闭连接"); selectionKey.cancel(); // 取消Key的注册 socketChannel.close(); // 关闭通道 continue; } }
额外说明
你之前从selectedKeys迭代器中移除Key,只是把它从已选择集合中清除,但并没有取消该Key在Selector上的注册状态。只要Key还在注册,并且对应的事件触发条件(比如这里的EOF)持续满足,下次select()就会再次把它加入selectedKeys集合。只有调用selectionKey.cancel()才能彻底把Key从Selector中移除,避免后续事件触发。
内容的提问来源于stack exchange,提问作者Ethan Tan

