Java NIO中serverSocketChannel.accept()返回null的问题排查
我编写了一个简单的Java NIO客户端服务端程序,代码如下:
package org.lb; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Set; public class Test { public static void main(String[] args) throws IOException { ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.bind(new InetSocketAddress(8080)); serverSocketChannel.configureBlocking(false); Selector selector = Selector.open(); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); while (true) { selector.select(); Set<SelectionKey> selectionKeySet = selector.selectedKeys(); for (SelectionKey selectionKey : selectionKeySet) { if (selectionKey.isAcceptable()) { SocketChannel socketChannel = serverSocketChannel.accept(); if (socketChannel == null) { System.out.println("No connection..."); } else { socketChannel.configureBlocking(false); socketChannel.register(selector, SelectionKey.OP_READ); System.out.println("Connection done!"); } } if (selectionKey.isReadable()) { System.out.println("isReadable"); SocketChannel socketChannel = (SocketChannel) selectionKey.channel(); socketChannel.close(); } } } } }
我仅在selectionKey.isAcceptable()返回true时调用serverSocketChannel.accept(),原以为该方法应始终返回有效的SocketChannel实例,但有时会返回null。使用Postman、浏览器或curl发送请求时均出现此情况:
Postman单次请求输出:
Connection done! No connection... isReadable Connection done! No connection... isReadable
curl单次请求输出:
Connection done! isReadable No connection...
我预期“Connection done!”仅打印一次,但实际打印了两次,同时还出现“No connection...”,这是为什么?
问题原因
未移除已处理的SelectionKey
Selector的selectedKeys集合不会自动清除已处理的键。每次调用select()后,之前触发过的事件键会保留在集合中,导致下一次循环时重复处理同一个OP_ACCEPT事件。第一次处理完accept()后,该键仍在集合里,下次循环再次进入isAcceptable()分支,此时没有新连接可处理,accept()就会返回null。HTTP客户端的连接特性
浏览器、Postman等工具可能会建立多个连接(比如预连接、请求结束后的复用尝试),curl在某些场景下也会触发额外的连接行为,这会导致多次OP_ACCEPT事件触发,但后续的accept()没有新连接可获取,因此返回null。
解决方案
核心修复点是处理完每个SelectionKey后必须从集合中移除,避免重复处理:
修改代码中的循环部分,使用迭代器并手动移除已处理的键:
while (true) { selector.select(); Set<SelectionKey> selectionKeySet = selector.selectedKeys(); // 使用迭代器遍历,方便移除已处理的键 for (Iterator<SelectionKey> iterator = selectionKeySet.iterator(); iterator.hasNext();) { SelectionKey selectionKey = iterator.next(); // 关键步骤:移除已处理的键,防止重复处理 iterator.remove(); if (selectionKey.isAcceptable()) { SocketChannel socketChannel = serverSocketChannel.accept(); if (socketChannel == null) { System.out.println("No connection..."); } else { socketChannel.configureBlocking(false); socketChannel.register(selector, SelectionKey.OP_READ); System.out.println("Connection done!"); } } if (selectionKey.isReadable()) { System.out.println("isReadable"); SocketChannel socketChannel = (SocketChannel) selectionKey.channel(); socketChannel.close(); } } }
另外需要明确:在非阻塞模式下,即使OP_ACCEPT事件被触发,accept()返回null是正常行为(当所有待处理连接都已被处理完毕时),你的null判断逻辑是合理的,重复处理事件才是导致多次打印的根本原因。
内容的提问来源于stack exchange,提问作者Rahul

