2023-09-23
原文作者:李林超 原文地址: https://www.lilinchao.com/archives/2102.html

[TOC]

一、概述

Selector一般称为选择器,也可以翻译为多路复用器,是Java NIO核心组件之一,主要功能是 用于检查一个或者多个NIO Channel(通道)的状态是否处于可读、可写 。如此可以实现单线程管理多个Channel(通道),当然也可以管理多个网络连接。

202309232223397191.png

使用Selector的好处在于,可以 使用更少的线程来处理更多的通道 ,相比使用更多的线程, 避免了线程上下文切换带来的开销 等。

二、Selector(选择器)方法

2.1 Selector的创建

通过调用静态工厂方法Selector.open()方法创建一个Selector对象。

    Selector selector = Selector.open();

open()方法实际上是向SPI发出请求,通过默认的SelectorProvider对象获取一个新的Selector实例。

2.2 注册Channel到Selector

    channel.configureBlocking(false);
    SelectionKey key = channel.register(selector, Selectionkey.OP_READ);
  • 代码的第一行 :让这个Channel(通道)是非阻塞的。

    • 它是 SelectableChannel 抽象类里的方法,用于使通道处于阻塞模式或非阻塞模式,false表示非阻塞,true表示阻塞。
    • 它的签名是:
            abstract SelectableChannel configureBlocking(boolean block)
 *  要想Channel注册到Selector中,那么这个Channel必须是 **非阻塞的** 。
    
    所以FileChannel不适合Selector,因为FileChannel不能切换为非阻塞模式,更准确的说是因为FileChannel没有继承SelectableChannel。但是SocketChannel可以正常使用。
  • 代码的第二行 :register()方法就是将通道注册到Selector中,并且让Selector监听感兴趣的事件(第二个参数)。

    • 着重讲一下第二个参数,它是一个“interest集合”,意思是在通过Selector监听Channel对什么事件感兴趣。可以监听四种不同类型的事件:Connect、Accept、Read、Write。
参数 常量表示 说明
Connect SelectionKey.OP_CONNECT 成功连接到另一个服务器称为“连接就绪”
Accept SelectionKey.OP_ACCEPT ServerSocketChannel准备好接收新进入的连接称为“接收就绪”
Read SelectionKey.OP_READ 有数据可读的通道称为“读就绪”
Write SelectionKey.OP_WRITE 等待写数据的通道称为“写就绪”

如果对不止一种事件感兴趣,可以使用或( | )运算符来操作

    int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

2.3 SelectionKey

一个SelectionKey键表示了一个特定的通道对象和一个特定的选择器对象之间的注册关系。

    key.attachment(); //返回SelectionKey的attachment,attachment可以在注册channel的时候指定。
    key.channel(); // 返回该SelectionKey对应的channel。
    key.selector(); // 返回该SelectionKey对应的Selector。
    key.interestOps(); //返回代表需要Selector监控的IO操作的bit mask
    key.readyOps(); // 返回一个bit mask,代表在相应channel上可以进行的IO操作。

方法介绍

  • key.interestOps()

描述 :通过这个方法来判断Selector是否对Channel的某种事件感兴趣;

    int interestSet = selectionKey.interestOps();
    boolean isInterestedInAccept = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
    boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
    boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;
    boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;
  • key.readyOps()

描述 :readySet 集合时通道已经准备就绪的操作的集合。

Java中定义了以下几个方法来检查这些操作是否就绪:

    //创建ready集合的方法
    int readySet = selectionKey.readyOps();
    //检查这些操作是否就绪的方法
    selectionKey.isAcceptable();//等价selectionKey.readyOps()&SelectionKey.OP_ACCEPT
    selectionKey.isConnectable();
    selectionKey.isReadable();
    selectionKey.isWritable();
  • key.attachment()

描述 :可以将一个对象或者更多信息附着到SelectionKey上,这样就能方便的识别某个特定的通道。

例如,可以附加与通道一起使用的Buffer,或者包含聚集数据的某个对象。如:

    key.attach(theObject);
    Object attachedObj = key.attachment();

还可以在register()方法使用的时候(即Selector注册Channel的时候)附加对象:

    SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
  • key.channel()和key.selector()

描述 :取出SelectionKey关联的Channel和Selector

    Channel channel = key.channel();
    Selector selector = key.selector();

三、Selector中的Channel

选择器维护注册过的通道,这种选择器与通道的注册关系被封装在SelectionKey中。

    public abstract class Selector
    {
        ...
        public abstract Set keys();
        public abstract Set selectedKeys();
        public abstract int select() throws IOException;
        public abstract int select(long timeout) throws IOException;
        public abstract int selectNow() throws IOException;
        public abstract void wakeup();
        ...   
    }

Selector维护的三种类型SelectionKey集合

(1)已注册的键的集合(Registered key set)

所有与选择器关联的通道所生成的键的集合称为已注册键的集合。这个集合通过keys()方法返回,并且有可能是空的。

**注意:**并不是所有注册过的键都有效。同时已注册键的集合是不可以直接修改的,若这么做的话,将会抛出java.lang.UnsupportedOperationException 异常。

(2)已选择键的集合(Selected key set)

已注册键的集合的子集,这个集合的每个成员都是相关的通道被选择器判断为已经准备好的并且包含于键的interest集合中的操作。这个集合通过selectedKeys()方法返回(有可能是空的)。

注意 :这些键可以直接从这个集合中移除,但是不能添加。若这么做的话将会抛出java.lang.UnsupportedOperationException异常。

(3)已取消键的集合(Cancelled key set)

已注册键的集合的子集,这个集合包含了cancel()方法被调用过的键(这个键已经被无效化),但他们还没有被注销。这个集合是选择器对象的私有成员,因而无法直接访问。

注意 :当键被取消(可以通过isValid()方法来判断)时,它将被放在相关的选择器的已取消的键的集合里。注册不会立即被取消,但键会立即失效。当再次调用select()方法时(或者一个正在进行的select()调用结束时),已取消的键的集合中的被取消的键将会被清理掉,并且相应的注销也将会完成。通道会被注销,新的SelectionKey将被返回。当通道关闭时,所有相关的键会自动取消(一个通道可以被注册到多个选择器上)。当选择器关闭时,所有被注册到该选择器的通道都将被注销,并且相应的键将立即被无效化(取消),一旦键被无效化,调用它的与之相关的方法就将抛出CancelledKeyException 异常。

四、select()方法

在刚初始化的Selector对象中,上面讲述的三个集合都是空的。通过Selector的select()方法可以选择已经准备就绪的通道(这些通道包含你感兴趣的事件)。比如你对读就绪的通道感兴趣,那么select()方法就会返回读事件已经就绪的那些通道。

下面是Selector重载的几个select()方法:

  • int select() :阻塞到至少有一个通道在你注册的事件上就绪了;
  • int select(long timeout) :和select()一样,但最长阻塞时间为timeout毫秒;
  • int selectNow() :非阻塞,执行就绪检查过程,但不阻塞,如果当前没有通道就绪,立刻返回0;

select()方法返回的int值表示有多少通道已经就绪,是自上次调用select()方法后有多少通道变成就绪状态。

之前在调用select()时进入就绪的通道不会在本次调用中被计入,而在前一次select()调用进入就绪但现在已经不在就绪状态的通道也不会被计入。

例如:首次调用select()方法,如果有一个通道变成了就绪状态,返回了1,若再次调用select()方法,如果另一个通道就绪了,它会再次返回1。如果对第一个就绪的Channel没有做任何操作,现在就有两个就绪的通道,但在每次select()方法调用之间,只有一个通道就绪了。

一旦调用了select()方法,并且返回值不为0时,则可以通过调用Selector的selectedKeys()方法来访问已选择键的集合。如下:

    Set selectedKeys = selector.selectedKeys();
    Iterator keyIterator = selectedKeys.iterator();
    while(keyIterator.hasNext()) {
        SelectionKey key = keyIterator.next();
        if(key.isAcceptable()) {
            // a connection was accepted by a ServerSocketChannel.
        } else if (key.isConnectable()) {
            // a connection was established with a remote server.
        } else if (key.isReadable()) {
            // a channel is ready for reading
        } else if (key.isWritable()) {
            // a channel is ready for writing
        }
        keyIterator.remove();
    }

请注意keyIterator.remove()每次迭代结束时的调用。在Selector删除SelectionKey作为自己选择的关键实例,当你完成处理后,你必须这样做。这样的话才能在通道下一次变为“就绪”时,Selector将再次将其添加到所选的键集合。

五、停止选择

选择器执行选择的过程,系统底层会一次询问每个通道是否就绪,这个过程可能会造成调用线程进入阻塞状态,那么我们有以下两种方式来唤醒在Select()方法中阻塞的线程。

1)wakeup()方法 :一个线程调用select()方法的那个对象上调用Selector.wakeup()方法。阻塞在select()方法上的线程会立马返回。如果有其它线程调用了wakeup()方法,但当前没有线程阻塞在select()方法上,下个调用select()方法的线程会立即“醒来(wake up)”。

(2)close()方法 :该方法使得任何一个在选择操作中阻塞的线程都被唤醒,用完Selector后调用其close()方法会关闭该Selector,且使注册到该Selector上的所有SelectionKey实例无效。通道本身并不会关闭*。*

附原文链接地址:

https://www.cnblogs.com/Joe-Go/p/9970375.html

阅读全文