NIO Selector

Summary::同时检测多个IO通道的对象

概念定义

在 Java 中,Selector 是 NIO(New Input/Output)库中的一种对象,用于监控多个通道的状态,例如文件 I/O 或者网络 I/O。
Selector 的工作原理是使用 select() 方法轮询已注册的通道,获取它们的就绪状态,并返回一个已准备好进行 I/O 操作的通道集合。通过使用此机制,可以监视几个通道的状态,并且只有当至少一个通道处于就绪状态时才会执行 I/O 操作,从根本上避免了 CPU 的浪费。
总之,Selector 是一种强大的工具,可实现高效的 I/O 操作和网络编程,因为它能够轻松地监视多个通道的状态并在需要时对它们进行操作。
Pasted image 20240729102258.png

相关概念

Selector的几个核心的概念

使用

1.创建选择器

Selector selector = Selector.open();

2.获取通道

ServerSocketChannel channel = ServerSocketChannel.open(); // 创建通道
channel.bind(new InetSocketAddress(8080));  // 绑定端口
channel.configureBlocking(false); // 设置为非阻塞,注册到selector上的通道一定设置为非阻塞,否则会报IllegalBlockingModeException错误

3.将通道注册到选择器上

如果一个 Channel 要注册到 Selector 中, 那么这个 Channel 必须是非阻塞的, 即channel.configureBlocking(false);
因为 Channel 必须要是非阻塞的, 因此 FileChannel 是不能够使用选择器的, 因为 FileChannel 都是阻塞的.

channel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE); // 将通道注册到选择器上,监听可接收事件,对于监听多个事件可以用“按位或”来操作

4.轮询已就绪的事件,并对不同的事件进行处理

while (true) {
    int count = selector.select();  // 获取已就绪事件的数量
    if (count == 0) {
        continue;
    }
    Set<SelectionKey> selectionKeys = selector.selectedKeys(); // 获取已就绪键集
    Iterator<SelectionKey> iterator = selectionKeys.iterator();
    while (iterator.hasNext()) {
        SelectionKey key = iterator.next();           
        if(key.isAcceptable()) {
            // 接收处理
        } else if (key.isConnectable()) {
            // 连接处理
        } else if (key.isReadable()) {
            // 读取处理
        } else if (key.isWritable()) {
            // 写入处理
        }
        key.remove(); // 移除键,防止下次重复处理
    }
}             
// 注意:key.isWritable只要建立连接,就会触发,所以在设置可写入事件时,在写入之后要改回可监听事件,否则就会死循环

注意, 在每次迭代时, 我们都调用 "keyIterator.remove()" 将这个 key 从迭代器中删除, 因为 select() 方法仅仅是简单地将就绪的 IO 操作放到 selectedKeys 集合中, 因此如果我们从 selectedKeys 获取到一个 key, 但是没有将它删除, 那么下一次 select 时, 这个 key 所对应的 IO 事件还在 selectedKeys 中.

5.客户端断开连接时处理,避免读buffer异常导致程序终止

if (key.isReadable()) {
    SocketChannel channel = (SocketChannel) key.channel();
    ByteBuffer buffer = ByteBuffer.allocate(16);
    try {
		// 读取buffer内容需要先判断buffer是否正常
		// 在客户端主动断开连接的时候, read()方法会返回-1,需要关闭客户端连接
        int read = channel.read(buffer);
        if (read == -1) {
            key.cancel();
            channel.close();
        } else {
            buffer.flip();
            debugRead(buffer);
            buffer.clear();
        }
        iter.remove();
    } catch (IOException e) {
        e.printStackTrace();
        key.cancel();
        channel.close();
        iter.remove();
    }
}

源码解析

Selector的创建

创建的语句为Selector selector = Selector.open();

public static Selector open() throws IOException {  
    return SelectorProvider.provider().openSelector();  
}

SelectorProvider.provider():

public static SelectorProvider provider() {  
    synchronized (lock) {  
        if (provider != null)  
            return provider;  
        return AccessController.doPrivileged(  
            new PrivilegedAction<SelectorProvider>() {  
                public SelectorProvider run() {  
                        if (loadProviderFromProperty())  
                            return provider;  
                        if (loadProviderAsService())  
                            return provider;  
                        provider = sun.nio.ch.DefaultSelectorProvider.create();  
                        return provider;  
                    }  
                });  
    }  
}

sun.nio.ch.DefaultSelectorProvider.create()的返回在每个平台的JDK都不一样,但他们对上层应用都提供统一的Selector接口,这使得Java能够跨平台运行。

windows返回WindowsSelectorProvider

mac返回KQueueSelectorProvider

Linux返回EPollSelectorProvider

参考Java网络编程(七):浅谈 Linux 中 Selector 的实现原理_interestops-CSDN博客

后续看这篇保证你彻底搞懂Java NIO的Selector事件选择器 - 沧海一滴 - 博客园

参考文章

【Netty】「NIO」(三)剖析 Selector-腾讯云开发者社区-腾讯云
Java Nio中Selector是什么?Selector怎么使用呢?_虚类selector是什么-CSDN博客
Java网络编程(七):浅谈 Linux 中 Selector 的实现原理_interestops-CSDN博客