【Netty】「NIO」(三)剖析 Selector

828 阅读7分钟

前言

本篇博文是《从0到1学习 Netty》中 NIO 系列的第三篇博文,主要内容是介绍通过使用 Selector,一个单独的线程可以有效地监视多个通道,从而提高应用程序的处理效率,往期系列文章请访问博主的 Netty 专栏,博文中的所有代码全部收集在博主的 GitHub 仓库中;

介绍

在 Java 中,Selector 是 NIO(New Input/Output)库中的一种对象,用于监控多个通道的状态,例如文件 I/O 或者网络 I/O。

Selector 的工作原理是使用 select() 方法轮询已注册的通道,获取它们的就绪状态,并返回一个已准备好进行 I/O 操作的通道集合。通过使用此机制,可以监视几个通道的状态,并且只有当至少一个通道处于就绪状态时才会执行 I/O 操作,从根本上避免了 CPU 的浪费。

总之,Selector 是一种强大的工具,可实现高效的 I/O 操作和网络编程,因为它能够轻松地监视多个通道的状态并在需要时对它们进行操作。

使用

1、创建 selector,管理多个 channel

Selector selector = Selector.open();

2、注册 selectorchannel 的联系;

SelectionKey sscKey = ssc.register(selector, 0, null);  
sscKey.interestOps(SelectionKey.OP_ACCEPT);

SelectionKey 表示一个通道(Channel)与一个选择器(Selector)之间的注册关系。每个通道在与选择器进行注册时都会创建一个对应的 SelectionKey 对象,这个对象包含了关于通道和选择器的一些元数据信息。

这里 SelectionKey 调用 interestOps() 方法指定感兴趣的事件类型,绑定的事件类型有以下几种:

  • connect - 客户端连接成功时触发;
  • accept - 服务器端成功接受连接时触发;
  • read - 数据可读入时触发,有因为接收能力弱,数据暂不能读入的情况;
  • write - 数据可写出时触发,有因为发送能力弱,数据暂不能写出的情况;

3、通过 selector 监听事件,并获得就绪的通道个数,若没有通道就绪,线程会被阻塞:

  • 阻塞直到绑定事件发生;

    int count = selector.select();
    
  • 阻塞直到绑定事件发生,或是超时(时间单位为 ms);

    int count = selector.select(long timeout);
    
  • 不会阻塞,即不管有没有事件,立刻返回,根据返回值检查是否有事件;

    int count = selector.selectNow();
    

4、处理事件,SelectionKey 内部包含了所有发生的事件:

selector.select();
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while (iter.hasNext()) {
    SelectionKey key = iter.next();
    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
    SocketChannel sc = channel.accept();
}

5、整体代码如下所示:

@Slf4j
public class SelectorTest {
    public static void main(String[] args) {
        try {
            // 1. 创建选择器来管理多个 channel
            Selector selector = Selector.open();

            ByteBuffer buffer = ByteBuffer.allocate(16);
            ServerSocketChannel ssc = ServerSocketChannel.open();
            // 通道必须设置为非阻塞模式
            ssc.configureBlocking(false);

            // 2. 注册 selector 和 channel 的联系
            SelectionKey sscKey = ssc.register(selector, 0, null);
            sscKey.interestOps(SelectionKey.OP_ACCEPT);
            log.debug("Register Key: {}", sscKey);

            ssc.bind(new InetSocketAddress(7999));

            while (true) {
                // 3. 在没有事件发生时,线程阻塞;反之,则线程恢复运行
                selector.select();

                // 4. 处理事件,SelectionKey 内部包含了所有发生的事件
                Iterator<SelectionKey> iter = selector.selectedKeys().iterator();

                while (iter.hasNext()) {
                    SelectionKey key = iter.next();
                    log.debug("Key: {}", key);
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    SocketChannel sc = channel.accept();
                    log.debug("{}", sc);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

运行结果:

20:22:20 [DEBUG] [main] c.s.n.c.SelectorTest - Register Key: channel=sun.nio.ch.ServerSocketChannelImpl[unbound], selector=sun.nio.ch.WindowsSelectorImpl@4f51b3e0, interestOps=16, readyOps=0
20:22:35 [DEBUG] [main] c.s.n.c.SelectorTest - Key: channel=sun.nio.ch.ServerSocketChannelImpl[/[0:0:0:0:0:0:0:0]:7999], selector=sun.nio.ch.WindowsSelectorImpl@4f51b3e0, interestOps=16, readyOps=16
20:22:35 [DEBUG] [main] c.s.n.c.SelectorTest - java.nio.channels.SocketChannel[connected local=/127.0.0.1:7999 remote=/127.0.0.1:61605]

进阶

在上个阶段,我们只是简单地使用了 selector,但对于其绑定的事件类型,我们并没有进行特别的关注,然而,在实际应用中,我们不可能只使用一种事件类型,因此,我们需要改进我们的代码;

这里我们需要对事件类型进行判断,SelectionKey 正好提供了相关的方法:

image.png

改进代码如下所示:

while (true) {
    // 3. 在没有事件发生时,线程阻塞;反之,则线程恢复运行
    selector.select();

    // 4. 处理事件,SelectionKey 内部包含了所有发生的事件
    Set<SelectionKey> keySet = selector.selectedKeys();
    Iterator<SelectionKey> iter = keySet.iterator();
    log.debug("count: {}", keySet.size());

    while (iter.hasNext()) {
        SelectionKey key = iter.next();
        log.debug("Selection Key: {}", key);

        // 5. 区分事件类型
        if (key.isAcceptable()) {
            ServerSocketChannel channel = (ServerSocketChannel) key.channel();
            SocketChannel sc = channel.accept();
            sc.configureBlocking(false);
            sc.register(selector, SelectionKey.OP_READ);
            log.debug("sc Key: {}", sc);
        } else if (key.isReadable()) {
            SocketChannel channel = (SocketChannel) key.channel();
            ByteBuffer buffer = ByteBuffer.allocate(16);
            channel.read(buffer);
            buffer.flip();
            debugRead(buffer);
            buffer.clear();
        }

    }
}

但是在运行时会发现报错空指针异常 NullPointerException

16:42:24 [DEBUG] [main] c.s.n.c.SelectorTest - Register Key: channel=sun.nio.ch.ServerSocketChannelImpl[unbound], selector=sun.nio.ch.WindowsSelectorImpl@4f51b3e0, interestOps=16, readyOps=0
16:42:45 [DEBUG] [main] c.s.n.c.SelectorTest - count: 1
16:42:45 [DEBUG] [main] c.s.n.c.SelectorTest - Selection Key: channel=sun.nio.ch.ServerSocketChannelImpl[/[0:0:0:0:0:0:0:0]:7999], selector=sun.nio.ch.WindowsSelectorImpl@4f51b3e0, interestOps=16, readyOps=16
16:42:45 [DEBUG] [main] c.s.n.c.SelectorTest - sc Key: java.nio.channels.SocketChannel[connected local=/127.0.0.1:7999 remote=/127.0.0.1:60700]
16:43:02 [DEBUG] [main] c.s.n.c.SelectorTest - count: 2
16:43:02 [DEBUG] [main] c.s.n.c.SelectorTest - Selection Key: channel=java.nio.channels.SocketChannel[connected local=/127.0.0.1:7999 remote=/127.0.0.1:60700], selector=sun.nio.ch.WindowsSelectorImpl@4f51b3e0, interestOps=1, readyOps=1

+--------+-------------------- read -----------------------+----------------+
position: [0], limit: [6]
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 73 69 64 69 6f 74                               |sidiot          |
+--------+-------------------------------------------------+----------------+

16:43:02 [DEBUG] [main] c.s.n.c.SelectorTest - Selection Key: channel=sun.nio.ch.ServerSocketChannelImpl[/[0:0:0:0:0:0:0:0]:7999], selector=sun.nio.ch.WindowsSelectorImpl@4f51b3e0, interestOps=16, readyOps=16
Exception in thread "main" java.lang.NullPointerException
        at com.sidiot.netty.c2.SelectorTest.main(SelectorTest.java:52)

这是由于我们没有及时 remove() 造成的,当调用了 server.register() 方法后,Selector 中维护了一个集合,用于存放 SelectionKey 以及其对应的通道

1.png

Selector 的通道对应的事件发生后SelecionKey 会被放到另一个集合中,在这个集合中,即使 SelecionKey 被使用了,它也不会自动移除,所以在处理完一个事件后,需要手动移除迭代器中的 SelecionKey,否则会导致已被处理过的事件再次被处理,引发一些错误,例如上述的空指针异常。

2.png


当客户端主动断开连接时,也会出现异常,控制台输出如下:

java.net.SocketException: Connection reset
        at java.base/sun.nio.ch.SocketChannelImpl.throwConnectionReset(SocketChannelImpl.java:345)
        at java.base/sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:376)
        at com.sidiot.netty.c2.SelectorTest.main(SelectorTest.java:60)

这是因为当客户端与服务器之间的连接断开时,会给服务器端发送一个读事件,因此,我们需要进行判断,当 channel.read() 的返回值为-1时,表示连接断开,需要调用 key.cancel() 方法取消此事件;

改进代码如下所示:

if (key.isReadable()) {
    SocketChannel channel = (SocketChannel) key.channel();
    ByteBuffer buffer = ByteBuffer.allocate(16);
    try {
        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();
    }
}

整体代码如下所示:

@Slf4j
public class SelectorTest {
    public static void main(String[] args) {
        try {
            // 1. 创建选择器来管理多个 channel
            Selector selector = Selector.open();

            ServerSocketChannel ssc = ServerSocketChannel.open();
            // 通道必须设置为非阻塞模式
            ssc.configureBlocking(false);

            // 2. 注册 selector 和 channel 的联系
            SelectionKey sscKey = ssc.register(selector, 0, null);
            sscKey.interestOps(SelectionKey.OP_ACCEPT);
            log.debug("Register Key: {}", sscKey);

            ssc.bind(new InetSocketAddress(7999));

            while (true) {
                // 3. 在没有事件发生时,线程阻塞;反之,则线程恢复运行
                selector.select();

                // 4. 处理事件,SelectionKey 内部包含了所有发生的事件
                Set<SelectionKey> keySet = selector.selectedKeys();
                Iterator<SelectionKey> iter = keySet.iterator();
                log.debug("count: {}", keySet.size());

                while (iter.hasNext()) {
                    SelectionKey key = iter.next();
                    log.debug("Selection Key: {}", key);

                    // 5. 区分事件类型
                    if (key.isAcceptable()) {
                        ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                        SocketChannel sc = channel.accept();
                        sc.configureBlocking(false);
                        sc.register(selector, SelectionKey.OP_READ);
                        log.debug("sc Key: {}", sc);
                        iter.remove();
                    } else if (key.isReadable()) {
                        SocketChannel channel = (SocketChannel) key.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(4);
                        
                        try {
                            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();
                        }
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

后记

通过本文的介绍,我们对 Selector 这一关键的 Java NIO 组件有了深入的了解,并学习了它的使用和进阶技巧。

在使用 Selector 时,我们需要将通道注册到 Selector 上,并指定感兴趣的事件类型,如读取、写入等。然后,通过调用 select() 方法,Selector 会阻塞直到至少一个通道准备就绪。一旦有通道就绪,就可以获取就绪通道的集合,并进行相应的操作。

当然,Selector 的使用不仅限于简单的单通道管理,还可以通过 SelectionKey 对象进一步控制和优化。

总而言之,Selector 是 Java NIO 中一个强大而灵活的组件,它为我们提供了高效的事件驱动编程模型。通过合理地使用 Selector 和相应的技巧,我们可以实现高并发、高性能的网络编程应用。

以上就是 剖析 Selector 的所有内容了,希望本篇博文对大家有所帮助!

参考:

📝 上篇精讲:「NIO」(二)阻塞模式与非阻塞模式

💖 我是 𝓼𝓲𝓭𝓲𝓸𝓽,期待你的关注,创作不易,请多多支持;

👍 公众号:sidiot的技术驿站

🔥 系列专栏:探索 Netty:源码解析与应用案例分享