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控制台输出如下图所示:
文章图片
接着,切换到客户端的调试模式窗口,按Alt+F8,或者点击Evalute图标,打开评估器,切换成代码模式:
文章图片
输入以下代码,向socketChannel中写入"hi":
sc.write(Charset.defaultCharset().encode("hi"));
文章图片
点击Evalute进行评估,再切换ServerSelector的调试窗口,发现输出了空指针异常:
文章图片
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); ,如下图所示位置:
文章图片
问题分析 问题其实很简单,关键在于对Selector的设计理解。
Selector中有两个集合,分别是keys和selectedKeys,
- keys:所有注册在selector上channel的selectionKey。
- selectedKeys:所有注册在selector上,等待IO操作发生(即有事件发生)channel的selectionKey。
服务端注册时
文章图片
客户端启动时
文章图片
注:selector会在发生事件后,向selectedKeys中加入key。当事件被处理后,selectionKey会清除事件,但不会删除。所以在下个流程时(客户端注册时),我们看到sscKey的事件标记被清除了,由 "sscKey@c46bcd4 - accept事件 - ssc" 变成了 "sscKey@c46bcd4 - ssc" 。
客户端注册时
文章图片
客户端写消息时
文章图片
此后通过继续遍历,
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()方法删除元素。
推荐阅读
- 为什么你的路演总会超时()
- 财商智慧课(六)
- 吃了早餐,反而容易饿(为什么?)
- 为什么越花钱的人越有钱,越舍不得花钱的人却越穷()
- dubbo基本认识
- 为什么985/211的学生能胜任工作获得老板的青睐。
- 年轻人,干了孤独这杯酒
- 为什么孩子一定要学会可视化思维!
- 关于this的一些问题(1)
- 为什么有些女孩喜欢看玛丽苏爱情片()