netty-第一部分-NIO-3 核心概念3 Selector

Selector

昨天水了一篇,今天就详细来说这个Selector怎么用。

Selector又叫事件选择器。
简单来说,Selector可以让一个线程监控多个Channel的事件,而不是仅仅只能一对一。

selector 版
selector
thread
channel
channel
channel

本图摘自黑马netty课程笔记,mermaid编写,感觉这个mermaid画图还挺有效果。有空可以学一下

好处在于,
一个线程使用Selector可以同时监控多个Channel的事件,发生事件再处理,避免非阻塞模式下的空转。
充分利用单线程,减少线程数量。

它的底层实现好像是多路复用中的epoll实现。后面可能具体说一下。
本文还是主要放在它怎么用。
下面简单说一下Selector怎么监听的。是通过它自己和配套的SelectionKey实现的。

Selector与SelectionKey

SelectionKey是什么呢?可以理解为,被Selector监控的Channel都有一个对应的SelectionKey,内部记录监听了什么类型的事件,与哪些事件就绪了。
结构如下
在这里插入图片描述
处理Accept事件时,SelectionKey与Selector的作用见下图
代码请见下部分
在这里插入图片描述
那么我们可以知道Selector中存在的两个集合。分别存放所有注册Channel对应的key,与发生事件的key(调用方法elector.SelectedKeys()获取)。
在这里插入图片描述

如何监听Channel

直接看代码。
会详细写上注释。
基本包括常用方法。

三步走
创建
注册ChannelSelector,并给出要监控什么事件
监听事件

//创建
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

监听后如何对对应事件处理

分别说对acceptreadwrite事件的处理

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(),把keySelector中删除

write事件处理

write的关键在于可能buffer内数据过多,不能一次就把所有都写到Channel里,所以要根据write返回的实际写入字节数,多次去write

这里利用之前SelectionKey中的Attach,这次写没完成,把buffer放到这个ChannelSelectionKeyAttach部分。下次缓冲区有足够位置,又触发写事件,我们Select又拿到写事件,我们取出Attach中的buffer,继续写入剩余数据。这样在缓冲区满的时候不会去重复尝试写导致阻塞的状态(while快速尝试写,中间很多次循环因为缓冲区满了而无法写入,浪费性能)。

selector 监听所有 channel 的可写事件,每个 channel 都需要一个 key 来跟踪 buffer,但这样又会导致占用内存过多,就有两阶段策略

  • 当消息处理器第一次写入消息时,才将 channel 注册到 selector
  • selector 检查 channel 上的可写事件,如果所有的数据写完了,就取消 channel 的注册
  • 如果不取消,会每次可写均会触发 write 事件

即连接事件处理时accept后直接write,若一次没写完,则注册写事件到Selector就结束本次处理,去处理其他事件。
等之后注册写事件的Channel对应的缓冲区有空间了,就会触发写时间,被Select获取到,再次写入之前剩下的数据即可。(剩下的数据放在SelectionKeyAttach下)

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高性能的关键了。

后面会有个多线程使用多个Selector充分提高性能的一篇,这部分老师的代码感觉写得很好。
还有一篇IO模型(感觉网上有很多好的博客了,不一定会写),和零拷贝。

如果喜欢本篇文章可以点个赞,欢迎批评指正

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值