• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

四、Java NIO Selector

武飞扬头像
骑士梦
帮助1


一、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 集合,判断就绪事件类型,实现具体的业务操作。
第八步:根据业务决定是否需要再次注册监听事件,重复执行第三步操作。

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhgjeeji
系列文章
更多 icon
同类精品
更多 icon
继续加载