前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >NIO:为什么Selector的selectedKeys遍历处理事件后要移除?

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

原创
作者头像
借力好风
修改2021-10-28 09:42:07
1.2K0
修改2021-10-28 09:42:07
举报
文章被收录于专栏:NIO

问题来源于笔者在学习NIO的Selector的使用时,由于对Selector的机制不了解,导致程序出现了空指针异常。 该问题来源于后面两断代码。

问题现场还原

服务端代码

代码语言:javascript
复制
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<SelectionKey> 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();
                }
            }
        }
    }
}

客户端代码

代码语言:javascript
复制
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
image-20211013211349801

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

image-20211013212306751
image-20211013212306751

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

image-20211013212355269
image-20211013212355269

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

image-20211013212540344
image-20211013212540344
代码语言:javascript
复制
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);,如下图所示位置:

image-20211013212641210
image-20211013212641210

问题分析

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

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

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

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

服务端注册时

image-20211013223757763
image-20211013223757763

客户端启动时

image-20211013223830788
image-20211013223830788

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

客户端注册时

客户端写消息时

image-20211013223938568
image-20211013223938568

此后通过继续遍历,

代码语言:javascript
复制
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();

发现 selectedKeys 集合中的元素有两个:第一个是服务端ssc监听accept事件留下来的key和后续客户端sc监听read事件新加入的key!

iterator 拿到了第一个元素进入了 acceptable 的 if 分支:

代码语言:javascript
复制
if (key.isAcceptable()){ // 如果是 accept
// ...
}

而此时没有新的客户端加入,导致获取的 sc 为空!

代码语言:javascript
复制
SocketChannel sc = channel.accept(); // 此时的事件是sc的read,ssc获取sc为空!

进而导致该行空指针:

代码语言:javascript
复制
sc.configureBlocking(false);

所以,在 selectedKeys 集合中的元素,处理完事件后要移除。

代码语言:javascript
复制
SelectionKey key = iterator.next();
// 处理完事件后一定要从 selectedKeys 集合中删除
iterator.remove();

回顾&总结

回顾本次的事件经过

1.客户端连接时触发了 sscKey 的 accept 事件,没有移除事件。

2.客户端写消息时触发了 scKey 上的 read 事件,拿到了上次 ssckey 的 accept 事件进行处理,并没有客户端连接进入了错误的事件分支,导致了获取客户端的 channel 为空,进而空指针异常

总结

selector 在 select 发生事件后,会把事件相关的 key 放入 selectedKeys 集合,当事件处理完后不会主动的从 selectedKeys 集合中删除,所以需要自行删除。

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 问题现场还原
    • 服务端代码
      • 客户端代码
        • 启动调试过程
        • 问题分析
          • 服务端注册时
            • 客户端启动时
              • 客户端注册时
                • 客户端写消息时
                • 回顾&总结
                  • 回顾本次的事件经过
                    • 总结
                    相关产品与服务
                    腾讯云代码分析
                    腾讯云代码分析(内部代号CodeDog)是集众多代码分析工具的云原生、分布式、高性能的代码综合分析跟踪管理平台,其主要功能是持续跟踪分析代码,观测项目代码质量,支撑团队传承代码文化。
                    领券
                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档