Selector
昨天水了一篇,今天就详细来说这个Selector
怎么用。
Selector
又叫事件选择器。
简单来说,Selector
可以让一个线程监控多个Channel
的事件,而不是仅仅只能一对一。
本图摘自黑马netty
课程笔记,mermaid
编写,感觉这个mermaid
画图还挺有效果。有空可以学一下
好处在于,
一个线程使用Selecto
r可以同时监控多个Channel
的事件,发生事件再处理,避免非阻塞模式下的空转。
充分利用单线程,减少线程数量。
它的底层实现好像是多路复用中的epoll
实现。后面可能具体说一下。
本文还是主要放在它怎么用。
下面简单说一下Selector怎么监听的。是通过它自己和配套的SelectionKey实现的。
Selector与SelectionKey
SelectionKey
是什么呢?可以理解为,被Selector
监控的Channel
都有一个对应的SelectionKey
,内部记录监听了什么类型的事件,与哪些事件就绪了。
结构如下
处理Accept事件时,SelectionKey与Selector的作用见下图
代码请见下部分
那么我们可以知道Selector中存在的两个集合。分别存放所有注册Channel对应的key,与发生事件的key(调用方法elector.SelectedKeys()获取)。
如何监听Channel
直接看代码。
会详细写上注释。
基本包括常用方法。
三步走
创建
注册Channel
到Selector
,并给出要监控什么事件
监听事件
//创建
Selector sl = Selector.open();
//注册事件,要把Channel注册到Selector中,并给出需要监听的事件
//必须要是非阻塞模式
socketChannel.configureBlocking(false);
socketChannel.register(sl,SelectionKey.OP_READ);//监控读事件
//事件有accept事件,read事件,write时间,connect事件
//分别用SelectionKey的4个常量表示,OP_READ,OP_WRITE,OP_CONNECT,OP_ACCEPT
//accept事件,仅服务器端成功连接时触发
//read在有数据可读入时触发
//write事件,在缓冲区可写时触发,因为发送数据的缓冲区有限,可能需要等缓冲区腾出空来
//connect事件,在客户端连接成功时触发
//监听
//阻塞等待,直到有绑定的事件发生,返回值为多少Channel发生事件
int nums = sl.select();
int nums = sl.select( 10000 );//超时返回,单位ms
//非阻塞,立即返回,我们自己检查返回值来看是否发生了事件
int nums = sl.selectNow();
select什么时候不阻塞(摘自课程讲义)
- 事件发生时
- 客户端发起连接请求,会触发 accept 事件
- 客户端发送数据过来,客户端正常、异常关闭时,都会触发 read 事件,另外如果发送的数据大于 buffer 缓冲区,会触发多次读取事件
- channel 可写,会触发 write 事件
- 在 linux 下 nio bug 发生时
- 调用 selector.wakeup()
- 调用 selector.close()
- selector 所在线程 interrupt
监听后如何对对应事件处理
分别说对accept
,read
,write
事件的处理
accept事件
@Slf4j
public class ChannelDemo6 {
public static void main(String[] args) {
//accept事件是服务器接受连接时触发
//ServerSocketChannel专门处理连接请求
try (ServerSocketChannel channel = ServerSocketChannel.open()) {
channel.bind(new InetSocketAddress(8080));//绑定端口
System.out.println(channel);
Selector selector = Selector.open();
channel.configureBlocking(false);//非阻塞
//注册ServerSocketChannel到Selector,监控accept事件
channel.register(selector, SelectionKey.OP_ACCEPT);
//开始循环的处理连接事件,每次select返回
while (true) {
//select阻塞到发生accept事件
int count = selector.select();
// int count = selector.selectNow();
log.debug("select count: {}", count);
// if(count <= 0) {
// continue;
// }
// 获取所有事件(刚下select发现的accept事件)
Set<SelectionKey> keys = selector.selectedKeys();
// 遍历所有事件,逐一处理
Iterator<SelectionKey> iter = keys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
// 判断事件类型
if (key.isAcceptable()) {
//前面说过SelectionKey 中包含了对应Channel
//这里连接事件一定是由ServerSocketChannel触发的,因为他本就是用来处理连接请求的,使用SelectionKey 对应我们使用的ServerSocketChannel
ServerSocketChannel c = (ServerSocketChannel) key.channel();
// 建立连接,获取新连接的SocketChannel
SocketChannel sc = c.accept();
log.debug("{}", sc);
}
// 处理完毕,必须将事件移除
iter.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
//客户端,建立连接
public class Client {
public static void main(String[] args) {
try (Socket socket = new Socket("localhost", 8080)) {
System.out.println(socket);
socket.getOutputStream().write("world".getBytes());
System.in.read();
} catch (IOException e) {
e.printStackTrace();
}
}
}
TIPS:
事件发生后,要么处理,要么取消(cancel),不能什么都不做,否则下次该事件仍会触发,这是因为 nio 底层使用的是水平触发
read事件处理
要监控Accept
事件,同时在处理accept
请求时,把新连接对应Channel
注册读事件到Selector
中
@Slf4j
public class ChannelDemo6 {
public static void main(String[] args) {
try (ServerSocketChannel channel = ServerSocketChannel.open()) {
channel.bind(new InetSocketAddress(8080));
System.out.println(channel);
Selector selector = Selector.open();
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
int count = selector.select();
// int count = selector.selectNow();
log.debug("select count: {}", count);
// if(count <= 0) {
// continue;
// }
// 获取所有事件
Set<SelectionKey> keys = selector.selectedKeys();
// 遍历所有事件,逐一处理
Iterator<SelectionKey> iter = keys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
// 判断事件类型
//连接事件,接受连接,并把新连接对应Channel注册读事件到Selector中
if (key.isAcceptable()) {
ServerSocketChannel c = (ServerSocketChannel) key.channel();
// 必须处理
SocketChannel sc = c.accept();
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ);
log.debug("连接已建立: {}", sc);
//之前加入Selector的Channel触发了读事件,用ByteBuffer读出数据
} else if (key.isReadable()) {
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(128);
int read = sc.read(buffer);
//read返回-1说明该Channel的连接已经中断了,要把这个断开的连接清除,即用cancel()把Channel从Selector的注册表移除,从Selector的keys集合中删除对应的Key
if(read == -1) {
key.cancel();
sc.close();//释放资源
} else {
buffer.flip();
debug(buffer);
}
}
// 处理完毕,必须将事件移除
iter.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
//客户端
public class Client {
public static void main(String[] args) {
try (Socket socket = new Socket("localhost", 8080)) {
System.out.println(socket);
socket.getOutputStream().write("world".getBytes());
System.in.read();
} catch (IOException e) {
e.printStackTrace();
}
}
}
这里面老师给了几个关键TIPS:
- 为什么要
iter.remove()
因为 select 在事件发生后,就会将相关的 key 放入 selectedKeys 集合,但不会在处理完后从 selectedKeys 集合中移除,需要我们自己编码删除。例如
- 第一次触发了 ssckey 上的 accept 事件,没有移除 ssckey
- 第二次触发了 sckey 上的 read 事件,但这时 selectedKeys 中还有上次的 ssckey ,在处理时因为没有真正的 serverSocket 连上了,就会进入isAcceptable()分支调用accept方法 导致空指针异常
cancel
作用cancel 会取消注册在 selector 上的 channel,并从 keys 集合中删除 key 后续不会再监听事件
处理断开的连接
客户端主动断开时(close
方法)会产生OP_READ
事件。
正常情况read
方法返回读到的字节数。
断开连接后产生的读事件,我们调用read
返回-1
我们对-1做处理即可,像上面代码那样,使用SelectionKey.cancel()
,与SocketChannel.close()
。
同时异常断开也会产生读事件,也要处理
因为是异常断开,通道已经关闭,read
方法会抛出异常IOException
异常处理即可
也是用SelectionKey.cancel()
,把key
从Selector
中删除
write事件处理
write
的关键在于可能buffer
内数据过多,不能一次就把所有都写到Channel
里,所以要根据write
返回的实际写入字节数,多次去write
这里利用之前SelectionKey
中的Attach
,这次写没完成,把buffer
放到这个Channel
的SelectionKey
的Attach
部分。下次缓冲区有足够位置,又触发写事件,我们Select
又拿到写事件,我们取出Attach
中的buffer
,继续写入剩余数据。这样在缓冲区满的时候不会去重复尝试写导致阻塞的状态(while
快速尝试写,中间很多次循环因为缓冲区满了而无法写入,浪费性能)。
用 selector
监听所有 channel
的可写事件,每个 channel
都需要一个 key 来跟踪 buffer,但这样又会导致占用内存过多,就有两阶段策略
- 当消息处理器第一次写入消息时,才将
channel
注册到selector
上 selector
检查channel
上的可写事件,如果所有的数据写完了,就取消channel
的注册- 如果不取消,会每次可写均会触发
write
事件
即连接事件处理时accept
后直接write
,若一次没写完,则注册写事件到Selector
就结束本次处理,去处理其他事件。
等之后注册写事件的Channel
对应的缓冲区有空间了,就会触发写时间,被Select
获取到,再次写入之前剩下的数据即可。(剩下的数据放在SelectionKey
的Attach
下)
public class WriteServer {
public static void main(String[] args) throws IOException {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
ssc.bind(new InetSocketAddress(8080));
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
while(true) {
selector.select();
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove();
if (key.isAcceptable()) {
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
SelectionKey sckey = sc.register(selector, SelectionKey.OP_READ);
// 1. 向客户端发送内容
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 3000000; i++) {
sb.append("a");
}
ByteBuffer buffer = Charset.defaultCharset().encode(sb.toString());
int write = sc.write(buffer);
// 3. write 表示实际写了多少字节
System.out.println("实际写入字节:" + write);
// 4. 如果有剩余未读字节,才需要关注写事件
if (buffer.hasRemaining()) {
// read 1 write 4
// 在原有关注事件的基础上,多关注 写事件
sckey.interestOps(sckey.interestOps() + SelectionKey.OP_WRITE);
// 把 buffer 作为附件加入 sckey
sckey.attach(buffer);
}
} else if (key.isWritable()) {
ByteBuffer buffer = (ByteBuffer) key.attachment();
SocketChannel sc = (SocketChannel) key.channel();
int write = sc.write(buffer);
System.out.println("实际写入字节:" + write);
if (!buffer.hasRemaining()) { // 写完了
key.interestOps(key.interestOps() - SelectionKey.OP_WRITE);
key.attach(null);
}
}
}
}
}
}
客户端
public class WriteClient {
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
SocketChannel sc = SocketChannel.open();
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_CONNECT | SelectionKey.OP_READ);
sc.connect(new InetSocketAddress("localhost", 8080));
int count = 0;
while (true) {
selector.select();
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove();
if (key.isConnectable()) {
System.out.println(sc.finishConnect());
} else if (key.isReadable()) {
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
count += sc.read(buffer);
buffer.clear();
System.out.println(count);
}
}
}
}
}
结语
真的多
写了3个小时左右,还有一两个细节没说,明天补上,代码直接拿课程讲义里的了。
难的是按自己的话讲出来,有些话直接就搬过来了。
Selector属于是NIO的精华了,应该是NIO高性能的关键了。
如果喜欢本篇文章可以点个赞,欢迎批评指正