四、Java NIO Selector


一、Selector 简介


1. SelectorChannel 关系

  • Selector 选择器,也可以翻译为 多路复用器
  1. 它是 Java NIO 核心组件中的一个。
  2. 用于检查一个或多个 NIO Channel(通道)的状态,是否处于可读、可写。
  3. 如此可以实现单线程管理多个 Channel,也就是可以管理多个网络链接。
    在这里插入图片描述
  • 使用 Selector 的好处。
  1. 使用更少的线程就可以来处理通道了。
  2. 相比使用多个线程,避免了线程上下文切换带来的开销。

2. SelectableChannel 可选择通道

  • 不是所有的 Channel 都可以被 Selector 复用的。
  1. FileChannel 就不能被选择器复用。
  2. 判断一个 Channel 能被 Selector 复用,前提是判断他是否继承了一个抽象类 SelectableChannel
  3. 如果继承了 SelectableChannel,则可以被复用,否则不能。
  • SelectableChannel 类提供了实现通道的可选择性,所需要的公共方法。
  1. 它是所有支持就绪检查的通道类的父类。
  2. 所有 Socket 通道,都继承了 SelectableChannel 类都是可选择的,包括从管道(Pipe)对象的中获得的通道。
  3. 而 FileChannel 类,没有继承 SelectableChannel,因此是不是可选通道。
  • 一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次。
  1. 通道和选择器之间的关系,使用注册的方式完成。
  2. SelectableChannel 可以被注册到 Selector 对象上,在注册的时候,需要指定通道的哪些操作,是 Selector 感兴趣的。
    在这里插入图片描述

3. Channel 注册到 Selector

  • channel.register(Selector sel, int ops); 方法,将一个通道注册到一个选择器。
  1. 第一个参数,指定通道要注册的选择器。
  2. 第二个参数,指定选择器需要查询的通道操作。
  • 供选择器查询的通道操作(从类型来分):
  1. SelectionKey.OP_READ 可读。
  2. SelectionKey.OP_WRITE 可写。
  3. SelectionKey.OP_CONNECT 连接。
  4. SelectionKey.OP_ACCEPT 接收。
  • 如果 Selector 对通道的多操作类型感兴趣,可以用 位或 操作符实现。
// 可读可写
int key = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
  • 选择器查询的不是通道的操作,而是通道的某个操作的一种就绪状态。
  1. 什么是操作的就绪状态?
    一旦通道具备完成某个操作的条件,表示该通道的某个操作已经就绪,就可以被 Selector 查询到,程序可以对通道进行对应的操作。
  2. 一个 SocketChannel 通道可以连接到一个服务器,则处于 连接就绪(OP_CONNECT)
  3. 一个 ServerSocketChannel 服务器通道准备好接收新进入的连接,则处于 接收就绪(OP_ACCEPT) 状态。
  4. 一个有数据可读的通道,可以说是 读就绪(OP_READ)
  5. 一个等待写数据的通道,可以说是 写就绪(OP_WRITE)

4. SelectionKey 选择键

  • Channel 注册后,并且一旦通道处于某种就绪状态,就可以被选择器查询到。
  1. 使用选择器(Selector)的 select() 方法完成。
  2. select() 方法的作用,对感兴趣的通道操作,进行就绪状态的查询。
  • Selector 可以不断的查询 Channel 中发生操作的就绪状态。并且挑选感兴趣的操作就绪状态。
  1. 一旦通道有操作的就绪状态达成,并且是 Selector 感兴趣的操作,就会被 Selector 选中,放入选择键集合中。
  • 一个选择键。
  1. 首先是包含了注册在 Selector 的通道操作的类型。
    比方说 SelectionKey.OP_READ 可读。
  2. 也包含了 特定的通道 与 特定的选择器 之间的注册关系。
  3. 开发应用程序时,选择键是编程的关键。
  4. NIO 的编程,就是根据对应的选择键,进行不同的业务逻辑处理。
  • 选择键的概念 和 事件的概念比较相似。
  1. 一个选择键类似监听器模式里边的一个事件。
  2. 由于 Selector 不是事件触发的模式,而是主动去查询的模式,所以不叫事件 Event,而是叫 SelectionKey 选择键。

5. Selector 示例

/**
 * @author: wy
 * describe: Selector 示例
 * 1. Selector 创建
 * 2. Channel 注册到 Selector
 * 3. 轮询查询就绪操作
 */
public class Selector1 {

    public static void main(String[] args) throws IOException {
        // 一、获取通道
        ServerSocketChannel channel = ServerSocketChannel.open();
        // 1. 绑定连接
        channel.bind(new InetSocketAddress(9999));
        // 2. 设置为非阻塞
        channel.configureBlocking(false);

        // 二、获取 Selector 选择器
        Selector selector = Selector.open();

        /*
        三、将通道注册到选择器上,并指定监听事件为: 接收事件
        1. Channel 必须处于非阻塞模式下,否则将抛出异常 IllegalBlockingModeException。
         */
        channel.register(selector, SelectionKey.OP_ACCEPT);

        // 四、查询已经就绪通道操作
        Set<SelectionKey> selectionKeys = selector.selectedKeys();
        // 1. 遍历集合
        Iterator<SelectionKey> iterator = selectionKeys.iterator();
        while (iterator.hasNext()) {
            SelectionKey key = iterator.next();
            // 2. 判断key就绪状态操作
            if (key.isAcceptable()) {
                // 3. ServerSocketChannel 已接受连接
            } else if (key.isConnectable()) {
                // 4. 已与远程服务器建立连接
            } else if (key.isReadable()) {
                // 5. 通道已准备好读取
            } else if (key.isWritable()) {
                // 6. 通道已准备好写入
            }
            iterator.remove();
        }
    }
}
5.1 Selector 创建
// 二、获取 Selector 选择器
Selector selector = Selector.open();
5.2 Channel 注册到 Selector
  • 实现 Selector 管理 Channel。
  1. 需要将 Channel 注册到相应的 Selector 上。
// 一、获取通道
ServerSocketChannel channel = ServerSocketChannel.open();
// 1. 绑定连接
channel.bind(new InetSocketAddress(9999));
// 2. 设置为非阻塞
channel.configureBlocking(false);

// 二、获取 Selector 选择器
Selector selector = Selector.open();

/*
三、将通道注册到选择器上,并指定监听事件为: 接收事件
1. Channel 必须处于非阻塞模式下,否则将抛出异常 IllegalBlockingModeException。
 */
channel.register(selector, SelectionKey.OP_ACCEPT);
  • 注意
  1. 与 Selector 一起使用时,Channel 必须处于非阻塞模式下,否则将抛出异常 IllegalBlockingModeException。
  2. 意味着 FileChannel 不能与 Selector 一起使用,因为 FileChannel 不能切换到非阻塞模式,而套接字相关的所有的通道都可以。
  • 一个通道,并没有一定要支持所有的四种操作。
  1. 比如服务器通道 ServerSocketChannel 支持 Accept 接受操作。
  2. 而 SocketChannel 客户端通道则不支持。
  3. 可以通过通道上的 validOps() 方法,来获取特定通道下所有支持的操作集合。
5.3 轮询查询就绪操作
  • 通过 Selector 的 select() 方法,可以查询出已经就绪的通道操作。
  1. 这些就绪的状态集合,存在一个 Set<SelectionKey> 集合中。
  • Selector.select() 几个重载的查询方法:
  1. select():阻塞到至少有一个通道在注册的事件上就绪了。
  2. select(long timeout):和 select() 一样,但最长阻塞时间为 timeout 毫秒。
  3. selectNow():非阻塞,只要有通道就绪就立刻返回。
  • select() 方法,返回 int 值,表示有多少通道已经就绪。
  1. 准确的说,是自前一次 select() 方法到这一次 select() 方法之间的时间段,有多少通道变成就绪状态。
  2. 如:首次调用 select() 方法,如果有一个通道变成就绪状态,返回了 1。
  3. 再次调用 select() 方法,如果另一个通道就绪了,会再次返回 1。
  4. 如果对第一个就绪的 Channel 没有做任何操作,现在就有两个就绪的通道。
  5. 但在每次 select() 方法调用之间,只有一个通道就绪了。
  • select() 方法,返回值不为 0 时。
  1. 在 Selector 中有一个 selectedKeys() 方法,用来访问已选择键集合。
  2. 迭代集合的每一个选择键元素,根据就绪操作的类型,完成对应的操作。
// 四、查询已经就绪通道操作
Set<SelectionKey> selectionKeys = selector.selectedKeys();
// 1. 遍历集合
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
    SelectionKey key = iterator.next();
    // 2. 判断key就绪状态操作
    if (key.isAcceptable()) {
        // 3. ServerSocketChannel 已接受连接
    } else if (key.isConnectable()) {
        // 4. 已与远程服务器建立连接
    } else if (key.isReadable()) {
        // 5. 通道已准备好读取
    } else if (key.isWritable()) {
        // 6. 通道已准备好写入
    }
    iterator.remove();
}

6. 唤醒 select() 方法

  • 选择器执行选择的过程,系统底层会依次询问每个通道是否已经就绪。
  1. 这个过程,可能会造成调用线程进入阻塞状态。
  2. 下面两个方法,可以唤醒在 select() 方法中阻塞的线程。
  1. wakeup() 方法。
  1. 通过调用 Selector 对象的 wakeup() 方法,让处在阻塞状态的 select() 方法立刻返回。
  2. 该方法使得选择器上的第一个还没有返回的选择操作立即返回。
  3. 如果当前没有进行中的选择操作,那么下一次对 select() 方法的一次调用将立即返回。
  1. close() 方法。
  1. 通过 close() 方法关闭 Selector。
  2. 该方法使得任何一个在选择操作中阻塞的线程,都被唤醒(类似:wakeup() 方法)。
  3. 同时使得注册到该 Selector 的所有 Channel 被注销,所有的键将被取消。
  4. 但是 Channel 本身并不会关闭。

7. 服务端示例

/**
 * 1. 服务端示例
 */
@Test
public void server() throws IOException {
    // 一、获取服务端通道
    ServerSocketChannel channel = ServerSocketChannel.open();
    // 1. 绑定端口号
    channel.bind(new InetSocketAddress(8080));
    // 2. 切换到非阻塞模式
    channel.configureBlocking(false);

    // 二、获取 Selector 选择器
    Selector selector = Selector.open();

    /*
    三、将通道注册到选择器上,并指定监听事件为: 接收事件
    1. Channel 必须处于非阻塞模式下,否则将抛出异常 IllegalBlockingModeException。
     */
    channel.register(selector, SelectionKey.OP_ACCEPT);

    // 四、创建 Buffer,添加数据
    ByteBuffer readBuffer = ByteBuffer.allocate(1024);
    ByteBuffer writeBuffer = ByteBuffer.allocate(128);
    writeBuffer.put("服务端".getBytes());
    writeBuffer.flip();

    System.out.println("服务端已启动...");
    // 五、选择器进行轮询,进行后续操作
    int ready = 0;
    while ((ready = selector.select()) > 0) {
        // 1. 查询已经就绪通道操作
        Set<SelectionKey> selectionKeys = selector.selectedKeys();
        // 2. 遍历集合
        Iterator<SelectionKey> iterator = selectionKeys.iterator();
        while (iterator.hasNext()) {
            // 3. 获取就绪操作
            SelectionKey key = iterator.next();
            // 4. 判断key就绪状态操作
            if (key.isAcceptable()) {
                // 5. ServerSocketChannel 已接受连接
                // 创建新的连接
                SocketChannel socketChannel = channel.accept();
                // 切换到非阻塞模式
                socketChannel.configureBlocking(false);
                // 把连接注册到 Selector 上,声明这个 Channel 只对读操作感兴趣
                socketChannel.register(selector, SelectionKey.OP_READ);
                System.out.printf("%s, 注册成功!", socketChannel.getLocalAddress()).println();
            } else if (key.isReadable()) {
                // 6. 通道已准备好读取
                SocketChannel socketChannel = (SocketChannel) key.channel();
                // 读取数据
                int length = 0;
                while ((length = socketChannel.read(readBuffer)) > 0) {
                    readBuffer.flip();
                    System.out.printf("%s: %s", socketChannel.getRemoteAddress(), new String(readBuffer.array(), 0, length)).println();
                    readBuffer.clear();
                }
                key.interestOps(SelectionKey.OP_WRITE);
            } else if (key.isWritable()) {
                // 7. 通道已准备好写入
                writeBuffer.rewind();
                SocketChannel socketChannel = (SocketChannel) key.channel();
                socketChannel.write(writeBuffer);
                key.interestOps(SelectionKey.OP_READ);
            }
            iterator.remove();
        }
    }
    System.out.println("服务端结束!");
}

8. 客户端示例

/**
 * 2. 客户端示例
 */
public static void main(String[] args) throws IOException {
    // 一、获取通道,绑定主机和端口号
    SocketChannel channel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8080));
    // 1. 切换到非阻塞模式
    channel.configureBlocking(false);

    // 2. 创建 Buffer
    ByteBuffer buffer = ByteBuffer.allocate(1024);

    System.out.println("客户端已启动...");
    Scanner selector = new Scanner(System.in);
    while (selector.hasNext()) {
        String next = selector.next();
        String date = DateFormat.getDateInstance().format(new Date());

        // 1. 添加数据
        buffer.put(String.format("date: %s, str: %s", date, next).getBytes());
        // 2. 切换模式
        buffer.flip();
        // 3. 倒回
        buffer.rewind();
        // 4. 写入通道
        channel.write(buffer);
        // 5. 清除
        buffer.clear();
        // 6. 读取
        int read = channel.read(buffer);
        System.out.printf("read: %s", read).println();
    }
}

二、NIO 编程步骤总结

第一步:创建 Selector 选择器。
第二步:创建 ServerSocketChannel 通道,并绑定监听端口。
第三步:设置 Channel 通道是非阻塞模式。
第四步:把 Channel 注册到 Socketor 选择器上,监听连接事件。
第五步:调用 Selector 的 select() 方法(循环调用),监测通道的就绪状况。
第六步:调用 selectKeys() 方法获取就绪 Channel 集合。
第七步:遍历就绪 Channel 集合,判断就绪事件类型,实现具体的业务操作。
第八步:根据业务决定是否需要再次注册监听事件,重复执行第三步操作。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: Java NIO 中的 Selector 可以用于多路复用 I/O,它可以同时监控多个 Channel 的 IO 状态,如读写就绪等,从而让你的程序可以同时处理多个网络连接。 使用 Selector 的基本流程如下: 1. 创建 Selector 对象:使用 `Selector.open()` 方法。 2. 创建并配置 Channel:每个 Channel 都必须注册到 Selector 上。 3. 向 Selector 注册感兴趣的事件:使用 `SelectionKey` 对象将 Channel 和感兴趣的事件绑定。 4. 通过 `select()` 方法监控 Channel:该方法会阻塞,直到至少有一个 Channel 处于就绪状态。 5. 处理就绪的 Channel:通过 `selectedKeys()` 方法获取所有就绪的 Channel,然后遍历每一个 Key,并根据 Key 的事件状态进行相应的处理。 6. 关闭 Selector:使用 `close()` 方法关闭 Selector。 以上就是 Selector 的基本使用方法。希望这些信息能帮助你理解和使用 Java NIO 中的 Selector。 ### 回答2: Java NIO(New Input/Output)提供了一种非阻塞I/O的能力,其中的selector是一种重要的组件。它允许程序通过一个单线程来监听多个通道上的事件并做出相应的处理。 使用Selector主要包括以下步骤: 1. 创建Selector实例: Selector selector = Selector.open(); 2. 创建Channel并设置为非阻塞模式: 在使用Selector之前,需要确保Channel处于非阻塞模式,例如SocketChannel或ServerSocketChannel: SocketChannel socketChannel = SocketChannel.open(); socketChannel.configureBlocking(false); 3. 将Channel注册到Selector上: 通过SelectionKey来表示Channel的注册状态,包括感兴趣的操作集合及其附加的数据。可以使用以下方法将Channel注册到Selector上: SelectionKey key = socketChannel.register(selector, SelectionKey.OP_READ); 4. 进行事件监听: 使用Selector的select()方法进行事件监听,它会阻塞,直到有一个或多个事件发生: int readyChannels = selector.select(); if (readyChannels == 0) { continue; } 5. 获取已就绪的事件集合: 通过调用selector.selectedKeys()方法获取已经就绪的事件集合: Set<SelectionKey> selectedKeys = selector.selectedKeys(); 6. 遍历已就绪的事件集合并处理: 遍历selectedKeys集合,处理每一个就绪的事件: Iterator<SelectionKey> keyIterator = selectedKeys.iterator(); while (keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if (key.isReadable()) { // 可读事件处理逻辑 } if (key.isWritable()) { // 可写事件处理逻辑 } keyIterator.remove(); // 处理完毕后需要手动移除该事件,避免重复处理 } 7. 关闭Selector: 使用完Selector后需要及时关闭: selector.close(); 使用Selector可以实现多个通道的事件监听和处理,极大地提高了应用程序的性能和资源利用率。需要注意的是,在使用Selector时,一个线程可以管理多个Channel,但要谨慎处理每个Channel上的事件,以避免阻塞整个Selector处理线程。 ### 回答3: Java NIO(New I/O)是一种非阻塞I/O操作的Java API。它提供了一组用于高效处理I/O操作的类和接口。其中,SelectorNIO的核心组件之一,用于实现非阻塞I/O。 Selector是一个类似于调度员的对象,它可以同时监视多个通道的I/O事件。使用Selector可以实现单线程同时管理多个通道的I/O操作,提高了系统的效率。 使用Selector的主要步骤如下: 1. 创建一个Selector对象:通过调用Selector.open()方法创建一个Selector对象。 2. 将通道注册到Selector上:将需要监视的通道注册到Selector上,例如SocketChannel、ServerSocketChannel等。通过调用通道的register()方法完成注册。 3. 设置通道的非阻塞模式:通过调用通道的configureBlocking(false)方法将通道设置为非阻塞模式。 4. 选择通道:通过调用Selector的select()方法选择通道,并返回已准备就绪的通道的数量。 5. 处理选择的通道:通过调用Selector的selectedKeys()方法获取选择的通道集合,可以通过遍历通道集合进行相应的读写操作。 6. 取消选择的通道:通过调用SelectionKey的cancel()方法取消选择的通道的注册。 示例代码如下: ```java Selector selector = Selector.open(); SocketChannel socketChannel = SocketChannel.open(); socketChannel.configureBlocking(false); socketChannel.connect(new InetSocketAddress("example.com", 80)); socketChannel.register(selector, SelectionKey.OP_CONNECT); while (true) { int readyChannels = selector.select(); if (readyChannels == 0) { continue; } Set<SelectionKey> selectedKeys = selector.selectedKeys(); Iterator<SelectionKey> keyIterator = selectedKeys.iterator(); while (keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if (key.isConnectable()) { // 处理连接就绪的通道 SocketChannel channel = (SocketChannel) key.channel(); if (channel.isConnectionPending()) { channel.finishConnect(); } channel.configureBlocking(false); channel.register(selector, SelectionKey.OP_READ); } else if (key.isReadable()) { // 处理读就绪的通道 SocketChannel channel = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate(1024); channel.read(buffer); buffer.flip(); // 处理读取到的数据 } keyIterator.remove(); } } ``` 以上是一个简单的Selector的使用示例,通过这些步骤,可以实现对多个通道的非阻塞I/O操作的监视和处理。需要注意的是,Selector是基于事件驱动的,可以实现高效的I/O操作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

骑士梦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值