08-NIO SelectionKey

NIO SelectionKey

  • 在前面的NIO部分尤其是 Selector 部分涉及到了很多关于 SelectionKey 的内容,Selector的select方法阻塞,返回之后得到的就是 SelectionKey集合(一个SelectionKey 对应一个Channel),可以认为 SelectionKey 封装了发送的事件。在代码上 SelectionKey内部封装了Selector 和 Channel,内部通过一个整数来表示兴趣事件值,不同的bit 位表示一种事件,可以认为SelectionKey 是 Selector 和 Cahnnel的纽带,SelectionKey的代码不多也比较简单,本文来看看。

一、SelectionKey

  • SelectionKey 封装了事件,包括兴趣事件和就绪事件,内部通过两个整数来表示这两类事件,因为一个整数有多个bit 位,特定的bit 位代表特定的时间,因此也可以理解为两个事件集合。

  • 在SelectionKey对象的有效期间,Selector 会一直轮询它管理的 Channel,有事件之后将事件添加到 与某个 Channel对应的 SelectionKey的集合中去,然后上层程序通过 SelectionKey 来处理对应的事件。

  • 下面是源码的注释:

/**
 *  一个代表通道和选择器之间的注册关系
 * A token representing the registration of a {@link SelectableChannel} with a
 * {@link Selector}.
 *
 *   通道注册到Selector的时候会创建 SelectionKey,直到调用 SelectionKey的cancle,关闭通道或者
 *   关闭选择器之前 SelectionKey 都是有效的,可以通过 isValid 方法判断 SelectionKey是否有效
 *   取消一个SelectionKey不会马上被移除,而是在下一次选择操作的时候添加到cancelled-key集合(取消键集合)
 *   
 * <p> A selection key is created each time a channel is registered with a
 * selector.  A key remains valid until it is <i>cancelled</i> by invoking its
 * {@link #cancel cancel} method, by closing its channel, or by closing its
 * selector.  Cancelling a key does not immediately remove it from its
 * selector; it is instead added to the selector's <a
 * href="Selector.html#ks"><i>cancelled-key set</i></a> for removal during the
 * next selection operation.  The validity of a key may be tested by invoking
 * its {@link #isValid isValid} method.
 *
 * <a name="opsets"></a>
 *
 
    SelectionKey 内部包含两个操作集合,它们由两个整数表示,它的每一bit表示一种SelectionKey对应的Channel支持的操作
 * <p> A selection key contains two <i>operation sets</i> represented as
 * integer values.  Each bit of an operation set denotes a category of
 * selectable operations that are supported by the key's channel.
 *
 *
 *   兴趣集合表示哪些操作在下一次select 操作中会被检测是否被准备好,换言之只有兴趣事
 *   件,select才会帮我们去检测这个时间是否被准备好,兴趣事件是SelectionKey 被创建的时
 *   候在初始化的时候指定,其实就是 Channel注册到 Selector的时候创建的,后们可以通过方法 interestOps(int) 修改兴趣事件
 *
 *   <li><p> The <i>interest set</i> determines which operation categories will
 *   be tested for readiness the next time one of the selector's selection
 *   methods is invoked.  The interest set is initialized with the value given
 *   when the key is created; it may later be changed via the {@link
 *   #interestOps(int)} method. </p></li>
 *
 *
 *   就绪事件是另一个集合,它代表与 SelectionKey对应的 Channel的哪些事件被 Selector 检测到已经准备好了。
 *   刚刚创建 SelectionKey 的时候就绪事件是0,也就对应着空集合
 *   兴趣事件在后续的 select 操作可能被改变,但是不能直接修改
 *   (这里可以理解,兴趣事件由外部触发,而不能自己修改,自己只能改变兴趣事件)   
 *
 *   <li><p> The <i>ready set</i> identifies the operation categories for which
 *   the key's channel has been detected to be ready by the key's selector.
 *   The ready set is initialized to zero when the key is created; it may later
 *   be updated by the selector during a selection operation, but it cannot be
 *   updated directly. </p></li>
 *
 * </ul>
 *
 * 就绪事件表示某一时刻 Channel 已经准备好可以执行的操作,但是不能保证,他可以由线程执行且不会引起线程阻塞;
 * 就绪集合在selector操作之后很可能是准确的,对应的Channel 外部的IO操作被调用之后,很可能是不准确的
 * 这里感觉不好理解,我觉得大意就是就绪事件并不是总是准确的,而是某一时刻的,在select 操作之后它通常是准确的,但是
 * Channel  触发了一些IO操作之后他就不准确了,因此select 后处理完有需要循环继续监听
 * 
 * <p> That a selection key's ready set indicates that its channel is ready for
 * some operation category is a hint, but not a guarantee, that an operation in
 * such a category may be performed by a thread without causing the thread to
 * block.  A ready set is most likely to be accurate immediately after the
 * completion of a selection operation.  It is likely to be made inaccurate by
 * external events and by I/O operations that are invoked upon the
 * corresponding channel.
 *
 *
 *
 * 这个类定义了所有已知的操作bit位, 但是精确的哪些bit位是Channel支持的,这取决于通道的类型。
 * SelectableChannel的所以子类定义了validOps() 方法 返回Channel支持的操作,尝试不支持的操作可能会引起运行期异常
 * 这里多说一句:SocketChannel支持  SelectionKey.OP_READ   | SelectionKey.OP_WRITE | SelectionKey.OP_CONNECT 
 * ServerSocketChanne 只支持:SelectionKey.OP_ACCEPT 
 * 
 * <p> This class defines all known operation-set bits, but precisely which
 * bits are supported by a given channel depends upon the type of the channel.
 * Each subclass of {@link SelectableChannel} defines an {@link
 * SelectableChannel#validOps() validOps()} method which returns a set
 * identifying just those operations that are supported by the channel.  An
 * attempt to set or test an operation-set bit that is not supported by a key's
 * channel will result in an appropriate run-time exception.
 *
 *
 * 
 * 通常关联一些应用具体的数据到 SelectionKey 上是必要的,比如一个代表高优先级协议状态的数据对象。
 * SelectionKey 因此支持添加一个单一任意对象,通过attach 方法添加对象, attachment获取对象 
 * 
 * <p> It is often necessary to associate some application-specific data with a
 * selection key, for example an object that represents the state of a
 * higher-level protocol and handles readiness notifications in order to
 * implement that protocol.  Selection keys therefore support the
 * <i>attachment</i> of a single arbitrary object to a key.  An object can be
 * attached via the {@link #attach attach} method and then later retrieved via
 * the {@link #attachment() attachment} method.
 *
 * 
 * Selection keys 多线程使用是安全的,对兴趣集合的操作通常会在具体的selector 操作的时候做现场安全同步,
 * 究竟这些同步是如何执行的取决于具体的独立实现,在native 实现中,如果 select 操作已经在执行,那么对兴趣集合的读写会无限期阻塞  
 * 在高性能实现中,读写兴趣集合会短暂的阻塞,甚至不阻塞,
 * 在任何情况下,select 操作使用的兴趣事件的值是 select 操作开始的那一瞬间的值
 * 
 * <p> Selection keys are safe for use by multiple concurrent threads.  The
 * operations of reading and writing the interest set will, in general, be
 * synchronized with certain operations of the selector.  Exactly how this
 * synchronization is performed is implementation-dependent: In a naive
 * implementation, reading or writing the interest set may block indefinitely
 * if a selection operation is already in progress; in a high-performance
 * implementation, reading or writing the interest set may block briefly, if at
 * all.  In any case, a selection operation will always use the interest-set
 * value that was current at the moment that the operation began.  </p>
 */
  • 翻译不一定精准,但是读了之后会加深对NIO 中组件的理解,建议阅读。

二、源码解读

2.1 获取Channel和Selector

  • SelectionKey 是Channel 和 Selector的纽带,内部封装了二者,因此内部持有这两个对象,通过对应的方法获取,
 //返回创建 SelectionKey 的 Channel,即使key取消也会返回
    public abstract SelectableChannel channel();

    //返回创建 SelectionKey 的 Selector,即使key取消也会返回
    public abstract Selector selector();

2.2 SelectionKey事件

  • 事件类型:SelectionKey内部定义了事件,类型一共四种,通过不同的 bit位表示
    //四种事件
    public static final int OP_READ = 1 << 0;       //接受事件,仅适用于服务端,准备好接受新的连接
    public static final int OP_WRITE = 1 << 2;      //连接事件,仅适用与客户端、连接成功
    public static final int OP_CONNECT = 1 << 3;    //读事件,有数据可读
    public static final int OP_ACCEPT = 1 << 4;     //写事件,有数据可写
  • 事件判断:通过位与运算判断是否为某种事件,注意readyOps() 方法会返回一个int 代表事件,由子类实现
    //判断是否为对应的事件类型
    public final boolean isReadable() {
        return (readyOps() & OP_READ) != 0;
    }
    
    public final boolean isWritable() {
        return (readyOps() & OP_WRITE) != 0;
    }
   
    public final boolean isConnectable() {
        return (readyOps() & OP_CONNECT) != 0;
    }
    
    public final boolean isAcceptable() {
        return (readyOps() & OP_ACCEPT) != 0;
    }
  • 事件获取和判断:获取事件,取消事件,判断是否合法等,由子类实现

    //判断 SelectionKey 是否有效,三种请求会导致无效,cancle,Channel关闭,Selector关闭
    public abstract boolean isValid();

    //取消和key关联的Channel和Selector之间的注册关系
    //方法返回后,key就会无效并被添加到取消键集合,在下一个selection操作期间键在Selector的所有的键集合中被移除
    //一旦被调用,key永远无效,取消后再次调用该方法没有影响
    //这个方法在取消键集合会同步,因此并发场景下调用会有一点点阻塞
    public abstract void cancel();

    //返回兴趣事件集合
    public abstract int interestOps();

    //根据给定值返回键的兴趣事件集合
    public abstract SelectionKey interestOps(int ops);

    //返回就绪事件集合
    public abstract int readyOps();

  • 事件触发:OP_READ 事件不仅仅只有可读时才触发,以下情况都会触发
1.channel 中数据读完
2.连接管道的另一端被关闭
3.有一个错误的 pending
4.对方发送消息过来

2.3 attachment属性

  • SelectionKey 可以添加和获取一个属性
    //属性字段
    private volatile Object attachment = null;

    //属性原子更新
    private static final AtomicReferenceFieldUpdater<SelectionKey,Object>
        attachmentUpdater = AtomicReferenceFieldUpdater.newUpdater(
            SelectionKey.class, Object.class, "attachment"
        );

    //添加属性
    public final Object attach(Object ob) {
        return attachmentUpdater.getAndSet(this, ob);
    }

    //获取属性
    public final Object attachment() {
        return attachment;
    }
}

2.4 关闭SelectionKey

  • 在以下情况下,SelectionKey 对象会失效,意味着 Selector 再也不会监控与它相关的事件:
1.调用 SelectionKey 的 cancel() 方法
2.关闭与 SelectionKey 关联的Channel
3.与 SelectionKey 关联的 Selector 被关闭
  • 从这些条件可以看出,SelectionKey 作为 Channel 和 Selector的纽带,三者任意一个都可能使 SelectionKey 无效

三、实现类

3.1 AbstractSelectionKey

  • AbstractSelectionKey 实现了SelectionKey,主要实现了cancel 和 valid方法
public abstract class AbstractSelectionKey extends SelectionKey {

    //合法标志位
    private volatile boolean valid = true;

    public final boolean isValid() {
        return valid;
    }

    //设置为不合法
    void invalidate() {          // package-private
        valid = false;
    }

    /**
     * Cancels this key.
     * <p> If this key has not yet been cancelled then it is added to its
     * selector's cancelled-key set while synchronized on that set.  </p>
     *
     * 取消key,如果键没有被取消,就加到取消键集合,并且在集合操作的时候需要同步
     * 同步是为了避免一个键被多个线程取消多次,这可能会造成selector的select()和channel的close() 之间的竞争
     */
    public final void cancel() {
        // Synchronizing "this" to prevent this key from getting canceled
        // multiple times by different threads, which might cause race
        // condition between selector's select() and channel's close().
        synchronized (this) {
            if (valid) {
                valid = false;
                ((AbstractSelector)selector()).cancel(this);
            }
        }
    }
}
  • valid:通过内部 volatile 变量实现;
  • cancel:同步操作,通过Selector 的 cancel 方法实现;

3.2 SelectionKeyImpl

  • sun.nio.ch.SelectionKeyImpl 是 SelectorKey的具体实现类,也是SelectionKey唯一的具体实现类
public class SelectionKeyImpl extends AbstractSelectionKey {

    //封装的channel和Selector对象,该属性是包私有的,NIO的很多 Channel都实现了SelChImpl 接口
    final SelChImpl channel;
    public final SelectorImpl selector;
    
    //index表示该 SelectionKey 对象存储在与其关联的 Selector 对象中所在的位置(一个Selector显然可以有很多SelectionKey,一个Channel对应一个SelectionKey)
    private int index;
    
    //兴趣事件变量和就绪事件变量
    private volatile int interestOps;
    private int readyOps;


    //构造方法
    SelectionKeyImpl(SelChImpl var1, SelectorImpl var2) {
        this.channel = var1;
        this.selector = var2;
    }

    //获取Channel 、Selector 和 index读写 
    public SelectableChannel channel() {
        return (SelectableChannel)this.channel;
    }

    public Selector selector() {
        return this.selector;
    }
    
    int getIndex() {
        return this.index;
    }

    void setIndex(int var1) {
        this.index = var1;
    }

    //确认有效,由 AbstractSelectionKey 的isValid 方法实现
    private void ensureValid() {
        if (!this.isValid()) {
            throw new CancelledKeyException();
        }
    }

    //获取兴趣事件变量
    public int interestOps() {
        this.ensureValid();
        return this.interestOps;
    }
    
    //获取就绪事件变量
    public int readyOps() {
        this.ensureValid();
        return this.readyOps;
    }
    
    //设置兴趣事件和就绪事件
    public SelectionKey interestOps(int var1) {
        this.ensureValid();
        return this.nioInterestOps(var1);
    }

    public void nioReadyOps(int var1) {
        this.readyOps = var1;
    }

    //获取兴趣事件变量
    public int nioInterestOps() {
        return this.interestOps;
    }
    
    //获取就绪事件变量
    public int nioReadyOps() {
        return this.readyOps;
    }

    //设置兴趣事件
    public SelectionKey nioInterestOps(int var1) {
        //validOps()方法返回 Channel支持的事件,如果事件不支持,就抛出异常
        if ((var1 & ~this.channel().validOps()) != 0) {
            throw new IllegalArgumentException();
        } else {
            //设置兴趣事件为var1
            this.channel.translateAndSetInterestOps(var1, this);
            this.interestOps = var1;
            return this;
        }
    }
}

四、示例

  • 为了更清晰的看到 SelectionKey 中表示事件的值,我把 BIO、NIO到Netty 中的NIO 示例添加了日志,查看不同的事件对应的值,关键代码如下:
  while (iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();
                //可接受事件
                if (selectionKey.isAcceptable()) {
                    //拿到channel对象

                    int interestOps = selectionKey.interestOps();
                    int readyOps = selectionKey.readyOps();
                    System.out.println("兴趣事件值:" + interestOps);
                    System.out.println("就绪事件值:" + readyOps);

                    省略....
                    
                    System.out.println("服务端处理端口:" + socketChannel.socket().getPort());
                    
                    省略....
                    
                    System.out.println("获取到客户端的连接: " + socketChannel);
                } else if (selectionKey.isReadable()) {
                    
                    int interestOps = selectionKey.interestOps();
                    int readyOps = selectionKey.readyOps();
                    System.out.println("兴趣事件值:" + interestOps);
                    System.out.println("就绪事件值:" + readyOps);

                    省略....
                        
                    System.out.println("服务端收到消息 : " + body);
                      
                    省略....
                }
            }

  • 服务端输出:
兴趣事件值:16
就绪事件值:16
服务端处理端口:60728
获取到客户端的连接: java.nio.channels.SocketChannel[connected local=/127.0.0.1:12345 remote=/127.0.0.1:60728]
兴趣事件值:1
就绪事件值:1
服务端收到消息 : hellonio
  • 从示例其实能够看出,时间本身就是一个整型值,前文分析的,通过不同的bit 位表示不同的事件,通过与运算判断事件是否属于某个类型。Selector 管理着若干的 Channel,如果有些 Channel有事件发生,则 select方法返回,返回有事件发生的 Channel对应的SelectionKey 会返回,通过 SelectionKey 我们可以知道发生了什么事件,然后进行后续的对应事件的处理。

  • 完整的源码参考

五、参考

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值