【Java】NIO中Channel的註冊原始碼分析

鬆餅人發表於2019-05-17

Channel的註冊是在SelectableChannel中定義的:

1 public abstract SelectionKey register(Selector sel, int ops, Object att)
2         throws ClosedChannelException;
3         
4 public final SelectionKey register(Selector sel, int ops)
5         throws ClosedChannelException {
6     return register(sel, ops, null);
7 }

而其具體實現是在AbstractSelectableChannel中:

 1 public final SelectionKey register(Selector sel, int ops,
 2                                        Object att)
 3         throws ClosedChannelException {
 4     synchronized (regLock) {
 5         if (!isOpen())
 6             throw new ClosedChannelException();
 7         if ((ops & ~validOps()) != 0)
 8             throw new IllegalArgumentException();
 9         if (blocking)
10             throw new IllegalBlockingModeException();
11         SelectionKey k = findKey(sel);
12         if (k != null) {
13             k.interestOps(ops);
14             k.attach(att);
15         }
16         if (k == null) {
17             // New registration
18             synchronized (keyLock) {
19                 if (!isOpen())
20                     throw new ClosedChannelException();
21                 k = ((AbstractSelector)sel).register(this, ops, att);
22                 addKey(k);
23             }
24         }
25         return k;
26     }
27 }

其中regLock和keyLock是兩個物件,分別用來做註冊鎖和key集合鎖

1 // Lock for key set and count
2 private final Object keyLock = new Object();
3 
4 // Lock for registration and configureBlocking operations
5 private final Object regLock = new Object();

isOpen判斷Channel是否關閉,只有在Channel關閉後才會令isOpen返回false;接著檢驗傳入進來的ops(SelectionKey的狀態,包括OP_READ、OP_WRITE、OP_CONNECT、OP_ACCEPT四種)是否滿足條件,validOps方法在不同的Channel子類中有不同的實現:
SocketChannel中:

1 public final int validOps() {
2     return (SelectionKey.OP_READ
3             | SelectionKey.OP_WRITE
4             | SelectionKey.OP_CONNECT);
5 }

那麼ops只要是上面三種狀態的任意一種或者一種以上,再和validOps的結果運算都為0,若是其他值則丟擲IllegalArgumentException異常;
ServerSocketChannel中:

1 public final int validOps() {
2     return SelectionKey.OP_ACCEPT;
3 }

和上面同理ServerSocketChannel在註冊時,只能傳入OP_ACCEPT狀態。

回到AbstractSelectableChannel的register方法,接下來是對blocking成員的判斷,

1 boolean blocking = true;

這是很重要的一步,因為NIO是既支援阻塞模式也支援非阻塞模式,但是若使用非阻塞模式,那麼必然需要Selector的輪詢,若是在註冊Selector之前沒有通過Channel呼叫configureBlocking方法設定為非阻塞模式,那麼就會在此時註冊時丟擲IllegalBlockingModeException異常。


configureBlocking方法的實現也是在AbstractSelectableChannel中:

 1 public final SelectableChannel configureBlocking(boolean block)
 2         throws IOException {
 3     synchronized (regLock) {
 4         if (!isOpen())
 5             throw new ClosedChannelException();
 6         if (blocking == block)
 7             return this;
 8         if (block && haveValidKeys())
 9             throw new IllegalBlockingModeException();
10         implConfigureBlocking(block);
11         blocking = block;
12     }
13     return this;
14 }

前兩個判斷邏輯都很簡單,在Channel開啟的情況下根據引數block設定阻塞或者非阻塞模式,注意到第二個判斷說明重複設定相同的阻塞模式直接返回,而第三個判斷則表明block 和blocking不相等,那麼就是在之前設定為了非阻塞模式,而haveValidKeys則間接表明已經完成了註冊,並且已經擁有了自己的SelectionKey集合,此時再設定為非阻塞模式就會引起IllegalBlockingModeException異常。

haveValidKeys方法:

 1 private SelectionKey[] keys = null;
 2 private int keyCount = 0;
 3 
 4 private boolean haveValidKeys() {
 5     synchronized (keyLock) {
 6         if (keyCount == 0)
 7             return false;
 8         for (int i = 0; i < keys.length; i++) {
 9             if ((keys[i] != null) && keys[i].isValid())
10                 return true;
11         }
12         return false;
13     }
14 }

邏輯比較簡單,先檢查keys的個數,為0直接返回沒有可用的SelectionKey,接著遍歷keys集合,找到一個可用的就返回true,其中isValid方法在AbstractSelectionKey中實現:

1 private volatile boolean valid = true;
2 
3 public final boolean isValid() {
4     return valid;
5 }

可以看到在初始化時valid = true就代表自身是可用狀態,當SelectionKey執行cancel方法撤銷時或者在Channel關閉時的撤銷都會改變:

 1 public final void cancel() {
 2     // Synchronizing "this" to prevent this key from getting canceled
 3     // multiple times by different threads, which might cause race
 4     // condition between selector's select() and channel's close().
 5     synchronized (this) {
 6         if (valid) {
 7             valid = false;
 8             ((AbstractSelector)selector()).cancel(this);
 9         }
10     }
11 }

Channel關閉時的撤銷在後續的部落格給出,這裡先不討論。

在configureBlocking中的implConfigureBlocking是一個抽象方法,具體的實現和使用的Channel有關,ServerSocketChannel和SocketChannel的實現分別是在ServerSocketChannelImpl和
SocketChannelImpl中,這兩個的實現方式也是完全一樣:

1 protected void implConfigureBlocking(boolean var1) throws IOException {
2     IOUtil.configureBlocking(this.fd, var1);
3 }

而IOUtil的configureBlocking方法是一個native方法,主要是對底層的操作,這裡就不討論了。


繼續回到AbstractSelectableChannel的register方法,在對阻塞模式判斷完畢後,呼叫findKey方法:

 1 private SelectionKey findKey(Selector sel) {
 2     synchronized (keyLock) {
 3         if (keys == null)
 4             return null;
 5         for (int i = 0; i < keys.length; i++)
 6             if ((keys[i] != null) && (keys[i].selector() == sel))
 7                 return keys[i];
 8         return null;
 9     }
10 }

在同步塊中,首先判斷keys是否初始化過,如果是第一次註冊,那麼keys必定為null,直接就返回null結束;否則已經註冊過,則遍歷keys這個SelectionKey集合,找的傳入的Selector 持有的SelectionKey後直接返回該SelectionKey物件,若沒找到則返回null;

接著對findKey方法的返回值k判斷
若k不為null,則說明註冊過這個Selector ,先呼叫interestOps方法,該方法是在SelectionKeyImpl中實現的:

1 public SelectionKey interestOps(int var1) {
2     this.ensureValid();
3     return this.nioInterestOps(var1);
4 }


首先通過ensureValid檢驗當前的SelectionKey是否可用(沒有被撤銷,呼叫cancel方法會撤銷):

1 private void ensureValid() {
2     if (!this.isValid()) {
3         throw new CancelledKeyException();
4     }
5 }

比較簡單,使用之前說過的isValid方法,檢查當前SelectionKey是否可用
nioInterestOps方法:

1 public SelectionKey nioInterestOps(int var1) {
2     if ((var1 & ~this.channel().validOps()) != 0) {
3         throw new IllegalArgumentException();
4     } else {
5         this.channel.translateAndSetInterestOps(var1, this);
6         this.interestOps = var1;
7         return this;
8     }
9 }

這個判斷和一開始的register中的檢查ops狀態是否合法一樣,若是合法需要呼叫Channel的translateAndSetInterestOps方法,同樣不同的Channel有不同的實現:


SocketChannel是在SocketChannelImpl中實現的:

 1 public void translateAndSetInterestOps(int var1, SelectionKeyImpl var2) {
 2     int var3 = 0;
 3     if ((var1 & 1) != 0) {
 4         var3 |= Net.POLLIN;
 5     }
 6 
 7     if ((var1 & 4) != 0) {
 8         var3 |= Net.POLLOUT;
 9     }
10 
11     if ((var1 & 8) != 0) {
12         var3 |= Net.POLLCONN;
13     }
14 
15     var2.selector.putEventOps(var2, var3);
16 }

之前說過SelectionKey有四種狀態:

1 public static final int OP_READ = 1 << 0;              // 0
2 public static final int OP_WRITE = 1 << 2;             // 4
3 public static final int OP_CONNECT = 1 << 3;         // 8
4 public static final int OP_ACCEPT = 1 << 4;          // 16

正如之前所說的SocketChannel只允許存在OP_READ、OP_WRITE 、OP_CONNECT 這三種狀態,所以上面就根據這三種狀態得到對應的POLL事件,最後給SelectionKey繫結的Selector設定POLL事件響應。

putEventOps的實現是在WindowsSelectorImpl中:

 1 public void putEventOps(SelectionKeyImpl var1, int var2) {
 2     Object var3 = this.closeLock;
 3     synchronized(this.closeLock) {
 4         if (this.pollWrapper == null) {
 5             throw new ClosedSelectorException();
 6         } else {
 7             int var4 = var1.getIndex();
 8             if (var4 == -1) {
 9                 throw new CancelledKeyException();
10             } else {
11                 this.pollWrapper.putEventOps(var4, var2);
12             }
13         }
14     }
15 }

還是一樣若是Selector關閉則丟擲異常,否則得到SelectionKey的index(在Selector中儲存的SelectionKey陣列的下標),判斷下標的合法性,然後給pollWrapper設定事件響應,而pollWrapper的putEventOps方法是一個native方法,這裡就不仔細討論了。

pollWrapper是存放socket控制程式碼fdVal和事件響應events的,用八個位來儲存一對。

而ServerSocketChannel的translateAndSetInterestOps實現和上面一樣,只不過只負責OP_ACCEPT 狀態:

1 public void translateAndSetInterestOps(int var1, SelectionKeyImpl var2) {
2     int var3 = 0;
3     if ((var1 & 16) != 0) {
4         var3 |= Net.POLLIN;
5     }
6 
7     var2.selector.putEventOps(var2, var3);
8 }


還是回到AbstractSelectableChannel的register方法中,interestOps呼叫結束後呼叫SelectionKey的attach方法:

 1 private volatile Object attachment = null;
 2 
 3 private static final AtomicReferenceFieldUpdater<SelectionKey,Object>
 4         attachmentUpdater = AtomicReferenceFieldUpdater.newUpdater(
 5             SelectionKey.class, Object.class, "attachment"
 6         );
 7         
 8 public final Object attach(Object ob) {
 9     return attachmentUpdater.getAndSet(this, ob);
10 }

這裡直接使用了原子更新器物件來更新attachment 。

k不為null的情況解決了,接下來就是解決k為null的情況,即第一次註冊,或者是再次註冊沒有找到和Selector對應的的SelectionKey。
首先在同步塊內還是先檢查Channel是否關閉,若沒有關閉,呼叫AbstractSelector的register方法完成Selector對SelectionKey的註冊:
而這個register方法的實現是在SelectorImpl中:

 1 protected final SelectionKey register(AbstractSelectableChannel var1, int var2, Object var3) {
 2     if (!(var1 instanceof SelChImpl)) {
 3         throw new IllegalSelectorException();
 4     } else {
 5         SelectionKeyImpl var4 = new SelectionKeyImpl((SelChImpl)var1, this);
 6         var4.attach(var3);
 7         Set var5 = this.publicKeys;
 8         synchronized(this.publicKeys) {
 9             this.implRegister(var4);
10         }
11 
12         var4.interestOps(var2);
13         return var4;
14     }
15 }

檢查Channel型別是否符合,然後直接建立一個SelectionKeyImpl物件:

1 final SelChImpl channel;
2 public final SelectorImpl selector;
3 
4 SelectionKeyImpl(SelChImpl var1, SelectorImpl var2) {
5     this.channel = var1;
6     this.selector = var2;
7 }

SelectionKeyImpl構造很簡單,直接給兩個成員賦值;然後呼叫SelectionKeyImpl物件的attach方法更新附件,接著在同步塊中呼叫抽象方法implRegister
implRegister方法是在WindowsSelectorImpl中實現的:

 1 protected void implRegister(SelectionKeyImpl var1) {
 2     Object var2 = this.closeLock;
 3     synchronized(this.closeLock) {
 4         if (this.pollWrapper == null) {
 5             throw new ClosedSelectorException();
 6         } else {
 7             this.growIfNeeded();
 8             this.channelArray[this.totalChannels] = var1;
 9             var1.setIndex(this.totalChannels);
10             this.fdMap.put(var1);
11             this.keys.add(var1);
12             this.pollWrapper.addEntry(this.totalChannels, var1);
13             ++this.totalChannels;
14         }
15     }
16 }

首先呼叫growIfNeeded方法,因為Selector選擇器解決非阻塞,就是使用輪詢的方式,它儲存了一個SelectionKeyImpl陣列,而SelectionKeyImpl記錄了channel以及SelectionKey的狀態,那麼就是根據SelectionKey的狀態和channel來完成通訊。由於在服務端的時候需要和多個客戶端連線,那麼這個陣列必定是動態維持的,所以就考慮到擴容。

1 private SelectionKeyImpl[] channelArray = new SelectionKeyImpl[8];
2  private int totalChannels = 1;

可以看到這個channelArray一開始固定初始化大小是8,而totalChannels 一開始就是1,這是為了方便後面的操作,channelArray 中下標為0的元素沒用使用,直接從下標為1開始。

growIfNeeded方法:

 1 private void growIfNeeded() {
 2     if (this.channelArray.length == this.totalChannels) {
 3         int var1 = this.totalChannels * 2;
 4         SelectionKeyImpl[] var2 = new SelectionKeyImpl[var1];
 5         System.arraycopy(this.channelArray, 1, var2, 1, this.totalChannels - 1);
 6         this.channelArray = var2;
 7         this.pollWrapper.grow(var1);
 8     }
 9 
10     if (this.totalChannels % 1024 == 0) {
11         this.pollWrapper.addWakeupSocket(this.wakeupSourceFd, this.totalChannels);
12         ++this.totalChannels;
13         ++this.threadsCount;
14     }
15 
16 }

因為totalChannels 是從1開始,所以直接判斷totalChannels是否達到了陣列長度,若已達到就需要擴容,可以看到每次擴容都是原來兩倍,從原陣列下標為1的地方開始一直到最後一個元素,拷貝到新陣列下標為1的位置上,再更新channelArray,同時還要給pollWrapper擴容。

pollWrapper的grow方法:

 1 void grow(int var1) {
 2     PollArrayWrapper var2 = new PollArrayWrapper(var1);
 3 
 4     for(int var3 = 0; var3 < this.size; ++var3) {
 5         this.replaceEntry(this, var3, var2, var3);
 6     }
 7 
 8     this.pollArray.free();
 9     this.pollArray = var2.pollArray;
10     this.size = var2.size;
11     this.pollArrayAddress = this.pollArray.address();
12 }
13 
14 void replaceEntry(PollArrayWrapper var1, int var2, PollArrayWrapper var3, int var4) {
15     var3.putDescriptor(var4, var1.getDescriptor(var2));
16     var3.putEventOps(var4, var1.getEventOps(var2));
17 }

邏輯很簡單,就是把原來的socket控制程式碼fdVal和事件響應events複製到新的PollArrayWrapper物件中,且位置不變。

再回到growIfNeeded,可以看到第二個判斷是檢查totalChannels是否達到了1024的整數次方(totalChannels初始是1,排除0),若是則需要pollWrapper.addWakeupSocket(this.wakeupSourceFd, this.totalChannels)這個操作在WindowsSelectorImpl構造方法時也被呼叫:

1 WindowsSelectorImpl(SelectorProvider var1) throws IOException {
2     super(var1);
3     this.wakeupSourceFd = ((SelChImpl)this.wakeupPipe.source()).getFDVal();
4     SinkChannelImpl var2 = (SinkChannelImpl)this.wakeupPipe.sink();
5     var2.sc.socket().setTcpNoDelay(true);
6     this.wakeupSinkFd = var2.getFDVal();
7     this.pollWrapper.addWakeupSocket(this.wakeupSourceFd, 0);
8 }

addWakeupSocket方法:

1 void addWakeupSocket(int var1, int var2) {
2     this.putDescriptor(var2, var1);
3     this.putEventOps(var2, Net.POLLIN);
4 }

可以看到設定的事件響應是Net.POLLIN,其實就對應OP_READ,而這個wakeupSourceFd是初始化時就設定的wakeupPipe的source的描述符fdVal,即一開始建立的ServerSocketChannel端的SocketChannel(SourceChannel)的描述符fdVal,之前說過Selector的select方法是一個阻塞的操作,呼叫select方法時只有註冊在Selector上的Channel有事件就緒時才會被喚醒;如果說有很多Channel註冊了,但是隻有一個Channel事件就緒,那麼豈不是要做很多無用的輪詢,而fdVal就是解決這個問題,實際上交給作業系統的輪詢的是wakeupSourceFd,作業系統在輪詢pollWrapper中的這些wakeupSourceFd描述符後就能知道哪些wakeupSourceFd上有事件就緒。
可以看到在growIfNeeded後面有一個++this.threadsCount操作,實際上Channel事件的輪詢是交給執行緒來做的,WindowsSelectorImpl中有如下成員:

1 private int threadsCount = 0;
2 private final List<WindowsSelectorImpl.SelectThread> threads = new ArrayList();

SelectThread是Thread的子類,threadsCount記錄輪詢執行緒個數。
那麼就有這種關係,在pollWrapper中,總是以wakeupSourceFd描述符開頭,後面跟著1024個Channel的描述,再往後就又是這種形式;那麼作業系統在輪詢pollWrapper中的這些wakeupSourceFd知道哪些wakeupSourceFd上有事件就緒,進而得到pollWrapper中的wakeupSourceFd起始的偏移地址,每個執行緒只負責輪詢1024個Channel的描述,哪個wakeupSourceFd上有事件就緒,就讓負責的執行緒去輪詢,這樣就減少了不必要的輪詢。
所以在totalChannels達到1024的整數次方時,需要增加新的輪詢執行緒。

growIfNeeded方法結束,channelArray中增添新的SelectionKeyImpl,並且設定下標(呼應前面獲取下標的操作),然後將SelectionKeyImpl存放在fdMap
fdMap儲存的時Channel的描述符和SelectionKeyImpl的對映關係:

 1 private static final class MapEntry {
 2     SelectionKeyImpl ski;
 3     long updateCount = 0L;
 4     long clearedCount = 0L;
 5     
 6     MapEntry(SelectionKeyImpl var1) {
 7        this.ski = var1;
 8     }
 9 }
10     
11 private static final class FdMap extends HashMap<Integer, WindowsSelectorImpl.MapEntry> {
12     static final long serialVersionUID = 0L;
13 
14     private FdMap() {
15     }
16 
17     private WindowsSelectorImpl.MapEntry get(int var1) {
18         return (WindowsSelectorImpl.MapEntry)this.get(new Integer(var1));
19     }
20 
21     private WindowsSelectorImpl.MapEntry put(SelectionKeyImpl var1) {
22         return (WindowsSelectorImpl.MapEntry)this.put(new Integer(var1.channel.getFDVal()), new WindowsSelectorImpl.MapEntry(var1));
23     }
24 
25     private WindowsSelectorImpl.MapEntry remove(SelectionKeyImpl var1) {
26         Integer var2 = new Integer(var1.channel.getFDVal());
27         WindowsSelectorImpl.MapEntry var3 = (WindowsSelectorImpl.MapEntry)this.get(var2);
28         return var3 != null && var3.ski.channel == var1.channel ? (WindowsSelectorImpl.MapEntry)this.remove(var2) : null;
29     }
30 }

程式碼邏輯都很簡單,就不詳細介紹了。

接著呼叫keys的add方法,keys是父類SelectorImpl的成員:

1 protected HashSet<SelectionKey> keys = new HashSet();

接著呼叫pollWrapper的addEntry方法:

1 void addEntry(int var1, SelectionKeyImpl var2) {
2     this.putDescriptor(var1, var2.channel.getFDVal());
3 }

可以看到僅僅是新增了channel的描述符fdVal,還沒有設定事件響應,最後totalChannels自增implRegister方法結束。

回到SelectorImpl的register方法,在implRegister方法結束後,呼叫SelectionKeyImpl的interestOps方法,前面說過的,在此時設定了事件響應,最後返回SelectionKeyImpl物件賦給AbstractSelectableChannel方法中的k,之後呼叫addKey方法,返回k,register方法呼叫全部結束。

addKey方法:

 1 private void addKey(SelectionKey k) {
 2     assert Thread.holdsLock(keyLock);
 3     int i = 0;
 4     if ((keys != null) && (keyCount < keys.length)) {
 5         // Find empty element of key array
 6         for (i = 0; i < keys.length; i++)
 7             if (keys[i] == null)
 8                 break;
 9     } else if (keys == null) {
10         keys =  new SelectionKey[3];
11     } else {
12         // Grow key array
13         int n = keys.length * 2;
14         SelectionKey[] ks =  new SelectionKey[n];
15         for (i = 0; i < keys.length; i++)
16             ks[i] = keys[i];
17         keys = ks;
18         i = keyCount;
19     }
20     keys[i] = k;
21     keyCount++;
22 }

邏輯很清晰,首先檢查有沒有沒有使用的key,若存在,直接用k覆蓋結束;若keys沒有初始化大小為3的陣列,先初始化keys,再將k放在下標為0的位置結束;若是keys已經初始化且keyCount == keys.length,就需要給keys擴容,並將原來的元素拷貝,最後將k放在新keys下標為keyCount的位置。

Channel的註冊到此全部結束。

相關文章