NIO原始碼分析:SelectionKey

小高飛發表於2020-11-14

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。

 

(以上為本人對原始碼的個人理解,如果有錯誤,歡迎各位前輩指出)

相關文章