26

NIO源码分析:SelectionKey

 3 years ago
source link: http://www.cnblogs.com/gaofei200/p/13973671.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

SelectionKey

SelectionKey,选择键,在每次通道注册到选择器上时都会创建一个SelectionKey储存在该选择器上,该SelectionKey保存了注册的通道、注册的选择器、通道事件类型操作符等信息。

SelectionKey是一个抽象类,它有俩个实现类了 AbstractSelectionKey(抽象类) SelectionKeyImpl(最终实现类) 。SelectionKey有6个属性:

//读操作符,左移位后的整型值为1
public static final int OP_READ = 1 << 0;
//写操作符,左移位后的整型值为4
public static final int OP_WRITE = 1 << 2;
//连接操作符,左移位后的整型值为8
public static final int OP_CONNECT = 1 << 3;
//接收操作符,左移位后的整型值为16
public static final int OP_ACCEPT = 1 << 4;
//附件
private volatile Object attachment = null;
//附件更新者,当要更新附件时需调用该对象的方法
private static final AtomicReferenceFieldUpdater<SelectionKey,Object>
    attachmentUpdater = AtomicReferenceFieldUpdater.newUpdater(
    SelectionKey.class, Object.class, "attachment"
);

这些属性中较为重要的是4个操作符属性,需记住它们左移位后的整型值,在后面对选择通道的操作事件判断需使用到。

SelectionKey除开构造器方法,有13个方法:

public abstract SelectableChannel channel();//返回该SelectionKey对应通道
public abstract Selector selector();//返回该SelectionKey注册的选择器
public abstract boolean isValid();//判断该SelectionKey是否有效
public abstract void cancel();//撤销该SelectionKey
public abstract int interestOps();//返回SelectionKey的关注操作符
//设置该SelectionKey的关注键,返回更改后新的SelectionKey
public abstract SelectionKey interestOps(int ops);
public abstract int readyOps();//返回SelectionKey的预备操作符

//这里readyOps()方法返回的是该SelectionKey的预备操作符,至于什么是预备操作符在最终实现类SelectionKeyImpl中会讲解。
//判断该SelectionKey的预备操作符是否是OP_READ
public final boolean isReadable() {
    return (readyOps() & OP_READ) != 0;
}
//判断该SelectionKey的预备操作符是否是OP_WRITE
public final boolean isWritable() {
    return (readyOps() & OP_WRITE) != 0;
}
//判断该SelectionKey的预备操作符是否是OP_CONNECT
public final boolean isConnectable() {
    return (readyOps() & OP_CONNECT) != 0;
}
//判断该SelectionKey的预备操作符是否是OP_ACCEPT
public final boolean isAcceptable() {
    return (readyOps() & OP_ACCEPT) != 0;
}

//设置SelectionKey的附件
public final Object attach(Object ob) {
    return attachmentUpdater.getAndSet(this, ob);
}
//返回SelectionKey的附件
public final Object attachment() {
    return attachment;
}

AbstractSelectionKey

AbstractSelectionKey继承了SelectionKey类,它也是一个抽象类,相比其他俩个类,它的代码就简洁多了,它只有一个属性:

//用于判断该SelectionKey是否有效,true为有效,false为无效,默认有效
private volatile boolean valid = true;

AbstractSelectionKey除开构造器方法,只要三个实现方法:

//判断该SelectionKey是否有效
public final boolean isValid() {
    return valid;
}

//将该SelectionKey设为无效
void invalidate() {                                 
    valid = false;
}
//将该SelectionKey从选择器中删除
//注意,删除的SelectionKey并不会马上从选择器上删除,而是会加入一个需删除键的集合中,等到下一次调用选择方法才会将它从选择器中删除,至于具体实现会在选择器源码分析章节中讲
public final void cancel() {
    synchronized (this) {
        if (valid) {
            valid = false;
            ((AbstractSelector)selector()).cancel(this);
        }
    }
}

SelectionKeyImpl

SelectionKeyImpl是SelectionKey的最终实现类,它继承了AbstractSelectionKey类,在该类中不仅实现了SelectionKey和抽象类的方法,而且扩展了其他方法。

SelectionKeyImpl的属性

SelectionKeyImpl中有5个新的属性:

//该SelectionKey对应的通道
final SelChImpl channel;
//该SelectionKey注册的选择器
public final SelectorImpl selector;
//该SelectionKey在注册选择器中储存SelectionKey集合中的下标索引,当该SelectionKey被撤销时,index为-1
private int index;
//SelectionKey的关注操作符
private volatile int interestOps;
//SelectionKey的预备操作符
private int readyOps;

从上面属性中可以看到,SelectionKeyImpl有俩个操作符属性: 关注操作符interestOps 预备操作符readyOps

interestOps 是储存通道的注册方法register(Selector sel, int ops)输入的ops参数,可以在register方法的最终实现中看出,代表程序需选择器对通道关注的操作事件。

public final SelectionKey register(Selector sel, int ops, Object att) throws ClosedChannelException
{
    synchronized (regLock) {
        if (!isOpen())
            throw new ClosedChannelException();
        if ((ops & ~validOps()) != 0)
            throw new IllegalArgumentException();
        if (blocking)
            throw new IllegalBlockingModeException();
        SelectionKey k = findKey(sel);
        if (k != null) {
            //将输入的参数ops储存在SelectionKey的interestOps属性中
            k.interestOps(ops);
            k.attach(att);
        }
        if (k == null) {
            synchronized (keyLock) {
                if (!isOpen())
                    throw new ClosedChannelException();
                k = ((AbstractSelector)sel).register(this, ops, att);
                addKey(k);
            }
        }
        return k;
    }
}

readyOps 是通道实际发生的操作事件,当我们对选择器Selector.select()方法层层追溯,到达该方法的最终实现doSelect(long var1)方法,会发现doSelect方法调用了一个updateSelectedKeys()方法来更新选择器的SelectionKey集合,而updateSelectedKeys方法又调用了updateSelectedKeys(this.updateCount)方法来进行实际的更新操作,而updateSelectedKeys方法最终通过processFDSet方法来实现更新。在processFDSet方法里有两个方法调用实现了SelectionKey里readyOps属性的更新: translateAndSetReadyOps(用于设置readyOps) translateAndUpdateReadyOps(用于更新readyOps)

public boolean translateAndUpdateReadyOps(int var1, SelectionKeyImpl var2) {
    return this.translateReadyOps(var1, var2.nioReadyOps(), var2);
}
public boolean translateAndSetReadyOps(int var1, SelectionKeyImpl var2) {
    return this.translateReadyOps(var1, 0, var2);
}

通过观察两个方法,可以发现它们都调用了translateReadyOps方法:

/**
* @param var1 该通道发生的事件类型,为Net中的POLL类型属性,这里需注意的有3个POLL类型属性:POLLIN、
* POLLOUT、POLLCONN,分别对应SelectionKey的OP_READ、OP_WRITE、OP_CONNECT
* @param var2 translateAndUpdateReadyOps中该参数为var3的readyOps,在translateAndSetReadyOps  * 中该参数为0
* @param var3 需修改的SelectionKey 
*/
public boolean translateReadyOps(int var1, int var2, SelectionKeyImpl var3) {
    int var4 = var3.nioInterestOps();
    int var5 = var3.nioReadyOps();
    int var6 = var2;
    if ((var1 & Net.POLLNVAL) != 0) {
        return false;
    } else if ((var1 & (Net.POLLERR | Net.POLLHUP)) != 0) {
        var3.nioReadyOps(var4);
        this.readyToConnect = true;
        return (var4 & ~var5) != 0;
    } else {
//var1 & var2 !=0 类似于 var1 == var2,或var1=0或var2=0
//判断发生的事件是否是Net.POLLIN,如果是则判断该通道对应的SelectionKey的readyOps是否等于1(OP_READ的值)
        if ((var1 & Net.POLLIN) != 0 && (var4 & 1) != 0 && this.state == 2) {
            var6 = var2 | 1;//等同于:var2 | OP_READ
        }
//判断发生的事件是否是Net.POLLCONN,如果是则判断该通道对应的SelectionKey的readyOps是否等于8(OP_CONNECT的值)
        if ((var1 & Net.POLLCONN) != 0 && (var4 & 8) != 0 && (this.state == 0 || this.state == 1)) {
            var6 |= 8;//等同于:var6 | OP_CONNECT
            this.readyToConnect = true;
        }
//判断发生的事件是否是Net.POLLOUT,如果是则这判断该通道对应的SelectionKey的readyOps是否等于4(OP_OP_WRITE的值)
        if ((var1 & Net.POLLOUT) != 0 && (var4 & 4) != 0 && this.state == 2) {
            var6 |= 4;//等同于:var6 | OP_READ
        }

        var3.nioReadyOps(var6);
        return (var6 & ~var5) != 0;
    }
}

看完上面源码可以发现,在判断通道实际发生事件的POLL类型时,还需判断该POLL类型对应的SelectionKey的ops类型是否与该SelectionKey的关注键interestOps相同,当所有条件满足时再将var6与其ops类型按位或,最后将该SelectionKey即var3的预备键readyOps设为var6。总结起来就是:

当通道实际发生的操作事件类型不等于该通道对应的SelectionKey的关注键interestOps时, 预备键readyOps不会被修改,且该发生事件的通道对应的SelectionKey也不会被加入Selector的selectedKeys集合中(从下面代码中可看出),但readyOps不一定就等于interestOps,因为调用interestOps的修改或设置方法时并不会同时修改readyOps。为了防止在进行select操作时,有另一个线程修改了某一SelectionKey的interestOps属性,在interestOps前添加了volatile修饰符,保证其可见性。

//这里是processFDSet方法中的一段代码,var10为SelectionKey
/*可以看出在进行了translateAndSetReadyOps或translateAndUpdateReadyOps方法设置readyOps属性后,
* 为了防止这时有另一线程修改了interestOps或readyOps为0,还判断了SelectionKey的readyOps和interestOps是否相同,
*/相同才把该SelectionKey加入到选择器的selectedKeys属性中
if (var9.clearedCount != var1) {
    var10.channel.translateAndSetReadyOps(var4, var10);
    if ((var10.nioReadyOps() & var10.nioInterestOps()) != 0) {
        WindowsSelectorImpl.this.selectedKeys.add(var10);
        var9.updateCount = var1;
        ++var6;
    }
} else {
    var10.channel.translateAndUpdateReadyOps(var4, var10);
    if ((var10.nioReadyOps() & var10.nioInterestOps()) != 0) {
        WindowsSelectorImpl.this.selectedKeys.add(var10);
        var9.updateCount = var1;
        ++var6;
    }
}

SelectionKeyImpl的方法

在SelectionKeyImpl中除开实现了SelectionKey抽象方法的简单方法外,需要关注几个较为重要的方法:

//确保该SelectionKey是有效的,如无效则会之间抛出异常
//在几个关于SelectionKey的属性方法中都有调用,如interestOps() 、interestOps(int var1)、readyOps()
private void ensureValid() {
    if (!this.isValid()) {
        throw new CancelledKeyException();
    }
}
//设置readyOps的方法,不会确保该SelectionKey的有效性
public void nioReadyOps(int var1) {
    this.readyOps = var1;
}
//该方法是SelectionKeyImpl自定义的获取readyOps方法,不同于readyOps(),它不会确保SelectionKey的有效性
public int nioReadyOps() {
    return this.readyOps;
}
//获取interestOps的方法,interestOps()会通过调用该方法进行获取,是最终实现获取nterestOps的方法
public int nioInterestOps() {
    return this.interestOps;
}

在SelectionKeyImpl类里,设置interestOps相比设置readyOps较为复杂,因为他调用了两个通道的方法validOps()和translateAndSetInterestOps(int var1, SelectionKeyImpl var2):

//interestOps(int var1)中调用了该方法来设置interestOps,是最终实现设置interestOps的方法
public SelectionKey nioInterestOps(int var1) {
    if ((var1 & ~this.channel().validOps()) != 0) {
        throw new IllegalArgumentException();
    } else {
        this.channel.translateAndSetInterestOps(var1, this);
        this.interestOps = var1;
        return this;
    }
}

首先先来看看nioInterestOps方法中判断语句内的代码块:

(var1 & ~this.channel().validOps()) != 0

这里的this.channel有5个可能的实现类:SeverSocketChannel、SocketChannel、SinkChannel、SourceChannel、DatagramChannel,因为不同的通道可能只会发生对应的操作事件,如SeverSocketChannel只会发生OP_ACCEPT操作,SocketChannel会发生OP_READ、OP_WRITE和OP_CONNECT,而validOps()就是返回通道对应可能发生的操作事件(有效操作事件):

//SeverSocketChannel
public final int validOps() {
    return SelectionKey.OP_ACCEPT;
}
//SocketChannel
public final int validOps() {
    return (SelectionKey.OP_READ
            | SelectionKey.OP_WRITE
            | SelectionKey.OP_CONNECT);
}

所以nioInterestOps方法的判断语句可以改为:

(var1 & ~有效的操作事件) != 0

而后又对有效的操作事件进行了取非,变成了:

(var1 & 无效的操作事件) != 0

在之前说过 var1 & var2 !=0 类似于 var1 == var2,所以当判断语句内的条件成立时,即说明设置的输入的参数不在该SelectionKey对应的通道的有效操作事件里,所以抛出 IllegalArgumentException() 异常。

当参数var1为该SelectionKey对应的通道的有效操作事件时,在调用通道的translateAndSetInterestOps方法将该关注类型对应的POLL类型写入到选择器的pollWrapper属性中:

//SocketChannel
public void translateAndSet InterestOps(int var1, SelectionKeyImpl var2) {
    int var3 = 0;
    //OP_READ
    if ((var1 & 1) != 0) {
        var3 |= Net.POLLIN;
    }
    //OP_WRITE
    if ((var1 & 4) != 0) {
        var3 |= Net.POLLOUT;
    }
    //OP_CONNECT
    if ((var1 & 8) != 0) {
        var3 |= Net.POLLCONN;
    }
    var2.selector.putEventOps(var2, var3);
}

最后再将该SelectionKey的interestOps设为参数值var1,并返回新的SelectionKey。

(以上为本人对源码的个人理解,如果有错误,欢迎各位前辈指出)


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK