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控制台输出如下图所示:

    image-20211013211349801

    接着,切换到客户端的调试模式窗口,按Alt+F8,或者点击Evalute图标,打开评估器,切换成代码模式:

    NIO:为什么Selector的selectedKeys遍历处理事件后要移除?_第1张图片

    输入以下代码,向socketChannel中写入"hi":

    sc.write(Charset.defaultCharset().encode("hi"));

NIO:为什么Selector的selectedKeys遍历处理事件后要移除?_第2张图片

点击Evalute进行评估,再切换ServerSelector的调试窗口,发现输出了空指针异常:

NIO:为什么Selector的selectedKeys遍历处理事件后要移除?_第3张图片

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遍历处理事件后要移除?_第4张图片

问题分析

问题其实很简单,关键在于对Selector的设计理解。

Selector中有两个集合,分别是keys和selectedKeys,

  • keys:所有注册在selector上channel的selectionKey。
  • selectedKeys:所有注册在selector上,等待IO操作发生(即有事件发生)channel的selectionKey。

我把程序执行过程大致分为四个时点:分别是服务端注册时客户端启动时客户端注册时客户端写消息时,通过对对应时点代码分析,得到以下状态图:

服务端注册时

NIO:为什么Selector的selectedKeys遍历处理事件后要移除?_第5张图片

客户端启动时

NIO:为什么Selector的selectedKeys遍历处理事件后要移除?_第6张图片

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

客户端注册时

NIO:为什么Selector的selectedKeys遍历处理事件后要移除?_第7张图片

客户端写消息时

NIO:为什么Selector的selectedKeys遍历处理事件后要移除?_第8张图片

此后通过继续遍历,

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 集合中删除,所以需要自行删除。

即在遍历 selectedKeys 集合时要用迭代器遍历,使用Iterator的remove()方法删除元素。

你可能感兴趣的:(javanio)