问题来源于笔者在学习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控制台输出如下图所示:
接着,切换到客户端的调试模式窗口,按Alt F8,或者点击Evalute图标,打开评估器,切换成代码模式:
输入以下代码,向socketChannel中写入"hi": sc.write(Charset.defaultCharset().encode("hi"));
点击Evalute进行评估,再切换ServerSelector的调试窗口,发现输出了空指针异常:
代码语言: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);,如下图所示位置:
问题分析
问题其实很简单,关键在于对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" 。
客户端注册时
客户端写消息时
此后通过继续遍历,
代码语言: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()方法删除元素。