NIO(为什么Selector的selectedKeys遍历处理事件后要移除())

问题来源于笔者在学习NIO的Selector的使用时,由于对Selector的机制不了解,导致程序出现了空指针异常。
该问题来源于后面两段代码。
问题现场还原 服务端代码
package com.jielihaofeng.netty.c4; import lombok.extern.slf4j.Slf4j; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Iterator; /** * @description Selector使用 * @author Johnnie Wind * @date 2021/10/11 22:08 */ @Slf4j public class ServerSelector {public static void main(String[] args) throws IOException {// 1. 创建 selector,管理多个 channel Selector selector = Selector.open(); ServerSocketChannel ssc = ServerSocketChannel.open(); ssc.configureBlocking(false); // 一定要配置,否则报异常 java.nio.channels.IllegalBlockingModeException// 2. 建立 selector 和 channel 之间的联系 // SelectionKey 就是将来事件发生后,通过它可以知道事件和哪个channel的事件 SelectionKey sscKey = ssc.register(selector, 0, null); // 事件的四种类型: // accept - 会在有连接请求时触发 // connect - 是客户端,连接建立后触发 // read - 可读事件 // write - 可写事件 // key只关注 accept事件 sscKey.interestOps(SelectionKey.OP_ACCEPT); log.debug("sscKey:{}",sscKey); ssc.bind(new InetSocketAddress(8080)); while (true){ // 3. select 方法,没有事件发生,线程阻塞,有事件,线程才会恢复运行 // select 在事件未处理时,它不会阻塞,事件发生后要么处理要么取消,不能置之不理 selector.select(); // 4.处理事件,selectedKeys 内部包含了所有发生的事件 Iterator iterator = selector.selectedKeys().iterator(); // accept,read while (iterator.hasNext()){ SelectionKey key = iterator.next(); log.debug("key:{}",key); // 5. 区分事件类型 if (key.isAcceptable()){ // 如果是 accept ServerSocketChannel channel = (ServerSocketChannel)key.channel(); SocketChannel sc = channel.accept(); sc.configureBlocking(false); SelectionKey scKey = sc.register(selector, 0, null); scKey.interestOps(SelectionKey.OP_READ); log.debug("sc {}",sc); log.debug("scKey:{}",scKey); }else if (key.isReadable()){ // 如果是 read SocketChannel channel = (SocketChannel) key.channel(); // 拿到触发事件的channel ByteBuffer buffer = ByteBuffer.allocate(16); channel.read(buffer); buffer.flip(); while(buffer.hasRemaining()){ System.out.println((char)buffer.get()); } buffer.clear(); } } } } }

客户端代码
package com.jielihaofeng.netty.c4; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.channels.SocketChannel; /** * @description 客户端 * @author Johnnie Wind * @date 2021/10/11 22:17 */ public class Client { public static void main(String[] args) throws IOException { SocketChannel sc = SocketChannel.open(); sc.connect(new InetSocketAddress("localhost", 8080)); System.out.println("waiting..."); // 注意,要在此处打断点进行调试启动 } }

启动调试过程
  • Debug或者Run模式运行服务端代码。
  • Debug模式运行客户端代码。
    启动成功,ServerSelector控制台输出如下图所示:
    NIO(为什么Selector的selectedKeys遍历处理事件后要移除())
    文章图片

    接着,切换到客户端的调试模式窗口,按Alt+F8,或者点击Evalute图标,打开评估器,切换成代码模式:
    NIO(为什么Selector的selectedKeys遍历处理事件后要移除())
    文章图片

    输入以下代码,向socketChannel中写入"hi":
    sc.write(Charset.defaultCharset().encode("hi"));

NIO(为什么Selector的selectedKeys遍历处理事件后要移除())
文章图片

点击Evalute进行评估,再切换ServerSelector的调试窗口,发现输出了空指针异常:
NIO(为什么Selector的selectedKeys遍历处理事件后要移除())
文章图片

21:11:44.400 [main] DEBUG com.jielihaofeng.netty.c4.ServerSelector - sscKey:sun.nio.ch.SelectionKeyImpl@c46bcd4 21:12:08.322 [main] DEBUG com.jielihaofeng.netty.c4.ServerSelector - key:sun.nio.ch.SelectionKeyImpl@c46bcd4 21:12:08.323 [main] DEBUG com.jielihaofeng.netty.c4.ServerSelector - sc java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:62001] 21:12:08.323 [main] DEBUG com.jielihaofeng.netty.c4.ServerSelector - scKey:sun.nio.ch.SelectionKeyImpl@4923ab24 21:23:57.723 [main] DEBUG com.jielihaofeng.netty.c4.ServerSelector - key:sun.nio.ch.SelectionKeyImpl@c46bcd4 Exception in thread "main" java.lang.NullPointerException at com.jielihaofeng.netty.c4.ServerSelector.main(ServerSelector.java:57) Disconnected from the target VM, address: '127.0.0.1:64394', transport: 'socket'

对应代码行为 sc.configureBlocking(false); ,如下图所示位置:
NIO(为什么Selector的selectedKeys遍历处理事件后要移除())
文章图片

问题分析 问题其实很简单,关键在于对Selector的设计理解。
Selector中有两个集合,分别是keys和selectedKeys,
  • keys:所有注册在selector上channel的selectionKey。
  • selectedKeys:所有注册在selector上,等待IO操作发生(即有事件发生)channel的selectionKey。
我把程序执行过程大致分为四个时点:分别是服务端注册时、客户端启动时、客户端注册时、客户端写消息时,通过对对应时点代码分析,得到以下状态图:
服务端注册时
NIO(为什么Selector的selectedKeys遍历处理事件后要移除())
文章图片

客户端启动时
NIO(为什么Selector的selectedKeys遍历处理事件后要移除())
文章图片

注:selector会在发生事件后,向selectedKeys中加入key。当事件被处理后,selectionKey会清除事件,但不会删除。所以在下个流程时(客户端注册时),我们看到sscKey的事件标记被清除了,由 "sscKey@c46bcd4 - accept事件 - ssc" 变成了 "sscKey@c46bcd4 - ssc" 。
客户端注册时
NIO(为什么Selector的selectedKeys遍历处理事件后要移除())
文章图片

客户端写消息时
NIO(为什么Selector的selectedKeys遍历处理事件后要移除())
文章图片

此后通过继续遍历,
Iterator iterator = selector.selectedKeys().iterator();

发现 selectedKeys 集合中的元素有两个:第一个是服务端ssc监听accept事件留下来的key和后续客户端sc监听read事件新加入的key!
iterator 拿到了第一个元素进入了 acceptable 的 if 分支:
if (key.isAcceptable()){ // 如果是 accept // ... }

而此时没有新的客户端加入,导致获取的 sc 为空!
SocketChannel sc = channel.accept(); // 此时的事件是sc的read,ssc获取sc为空!

进而导致该行空指针:
sc.configureBlocking(false);

所以,在 selectedKeys 集合中的元素,处理完事件后要移除。
SelectionKey key = iterator.next(); // 处理完事件后一定要从 selectedKeys 集合中删除 iterator.remove();

回顾&总结 回顾本次的事件经过
1.客户端连接时触发了 sscKey 的 accept 事件,没有移除事件。
2.客户端写消息时触发了 scKey 上的 read 事件,拿到了上次 ssckey 的 accept 事件进行处理,并没有客户端连接进入了错误的事件分支,导致了获取客户端的 channel 为空,进而空指针异常
总结
selector 在 select 发生事件后,会把事件相关的 key 放入 selectedKeys 集合,当事件处理完后不会主动的从 selectedKeys 集合中删除,所以需要自行删除。
【NIO(为什么Selector的selectedKeys遍历处理事件后要移除())】即在遍历 selectedKeys 集合时要用迭代器遍历,使用Iterator的remove()方法删除元素。

    推荐阅读