BIO到NIO原始碼的一些事兒之NIO 上
前言
此篇文章會詳細解讀NIO的功能逐步豐滿的路程,為Reactor-Netty 庫的講解鋪平道路。
關於Java程式設計方法論-Reactor與Webflux的視訊分享,已經完成了Rxjava 與 Reactor,b站地址如下:
Rxjava原始碼解讀與分享:www.bilibili.com/video/av345…
Reactor原始碼解讀與分享:www.bilibili.com/video/av353…
場景代入
接上一篇 BIO到NIO原始碼的一些事兒之BIO,我們來接觸NIO的一些事兒。
在上一篇中,我們可以看到,我們要做到非同步非阻塞,我們自己進行的是建立執行緒池同時對部分程式碼做timeout的修改來對接客戶端,但是弊端也很清晰,我們轉換下思維,這裡舉個場景例子,A班同學要和B班同學一起一對一完成任務,每對人拿到的任務是不一樣的,消耗的時間有長有短,任務因為有獎勵所以同學們會搶,傳統模式下,A班同學和B班同學不經管理話,即便只是一個心跳檢測的任務都得一起,在這種情況下,客戶端根本不會有資料要傳送,只是想告訴伺服器自己還活著,這種情況下,假如B班再來一個同學做對接的話,就很有問題了,B班的每一個同學都可以看成伺服器端的一個執行緒。所以,我們需要一個管理者,於是Selector
Selector
更側重於動作,針對於這些狀態標籤來做事情就可以了,那這些狀態標籤其實也是需要管理的,於是SelectionKey
也就應運而生。接著我們需要對這些同學進行包裝增強,使之攜帶這樣的標籤。同樣,對於同學我們應該進一步解放雙手的,比如給其配臺電腦,這樣,同學是不是可以做更多的事情了,那這個電腦在此處就是Buffer的存在了。 於是在NIO中最主要是有三種角色的,Buffer
緩衝區,Channel
通道,Selector
選擇器,我們都涉及到了,接下來,我們對其原始碼一步步分析解讀。Channel解讀
賦予Channel可非同步可中斷的能力
有上可知,同學其實都是代表著一個個的Socket
的存在,那麼這裡Channel
就是對其進行的增強包裝,也就是Channel
的具體實現裡應該有Socket
這個欄位才行,然後具體實現類裡面也是緊緊圍繞著Socket
具備的功能來做文章的。那麼,我們首先來看java.nio.channels.Channel
介面的設定:
public interface Channel extends Closeable {
/**
* Tells whether or not this channel is open.
*
* @return {@code true} if, and only if, this channel is open
*/
public boolean isOpen();
/**
* Closes this channel.
*
* <p> After a channel is closed, any further attempt to invoke I/O
* operations upon it will cause a {@link ClosedChannelException} to be
* thrown.
*
* <p> If this channel is already closed then invoking this method has no
* effect.
*
* <p> This method may be invoked at any time. If some other thread has
* already invoked it, however, then another invocation will block until
* the first invocation is complete, after which it will return without
* effect. </p>
*
* @throws IOException If an I/O error occurs
*/
public void close() throws IOException;
}
複製程式碼
此處就是很直接的設定,判斷Channel是否是open狀態,關閉Channel的動作,我們在接下來會講到ClosedChannelException
是如何具體在程式碼中發生的。 有時候,一個Channel可能會被非同步關閉和中斷,這也是我們所需求的。那麼要實現這個效果我們須得設定一個可以進行此操作效果的介面。達到的具體的效果應該是如果執行緒在實現這個介面的的Channel中進行IO操作的時候,另一個執行緒可以呼叫該Channel的close方法。導致的結果就是,進行IO操作的那個阻塞執行緒會收到一個AsynchronousCloseException
異常。
同樣,我們應該考慮到另一種情況,如果執行緒在實現這個介面的的Channel中進行IO操作的時候,另一個執行緒可能會呼叫被阻塞執行緒的interrupt
方法(Thread#interrupt()
),從而導致Channel關閉,那麼這個阻塞的執行緒應該要收到ClosedByInterruptException
異常,同時將中斷狀態設定到該阻塞執行緒之上。
這時候,如果中斷狀態已經在該執行緒設定完畢,此時在其之上的有Channel又呼叫了IO阻塞操作,那麼,這個Channel會被關閉,同時,該執行緒會立即受到一個ClosedByInterruptException
異常,它的interrupt狀態仍然保持不變。 這個介面定義如下:
public interface InterruptibleChannel
extends Channel
{
/**
* Closes this channel.
*
* <p> Any thread currently blocked in an I/O operation upon this channel
* will receive an {@link AsynchronousCloseException}.
*
* <p> This method otherwise behaves exactly as specified by the {@link
* Channel#close Channel} interface. </p>
*
* @throws IOException If an I/O error occurs
*/
public void close() throws IOException;
}
複製程式碼
其針對上面所提到邏輯的具體實現是在java.nio.channels.spi.AbstractInterruptibleChannel
進行的,關於這個類的解析,我們來參考這篇文章InterruptibleChannel 與可中斷 IO
賦予Channel可被多路複用的能力
我們在前面有說到,Channel
可以被Selector
進行使用,而Selector
是根據Channel
的狀態來分配任務的,那麼Channel
應該提供一個註冊到Selector
上的方法,來和Selector
進行繫結。也就是說Channel
的例項要呼叫register(Selector,int,Object)
。注意,因為Selector
是要根據狀態值進行管理的,所以此方法會返回一個SelectionKey
物件來表示這個channel
在selector
上的狀態。關於SelectionKey
,它是包含很多東西的,這裡暫不提。
//java.nio.channels.spi.AbstractSelectableChannel#register
public final SelectionKey register(Selector sel, int ops, Object att)
throws ClosedChannelException
{
if ((ops & ~validOps()) != 0)
throw new IllegalArgumentException();
if (!isOpen())
throw new ClosedChannelException();
synchronized (regLock) {
if (isBlocking())
throw new IllegalBlockingModeException();
synchronized (keyLock) {
// re-check if channel has been closed
if (!isOpen())
throw new ClosedChannelException();
SelectionKey k = findKey(sel);
if (k != null) {
k.attach(att);
k.interestOps(ops);
} else {
// New registration
k = ((AbstractSelector)sel).register(this, ops, att);
addKey(k);
}
return k;
}
}
}
//java.nio.channels.spi.AbstractSelectableChannel#addKey
private void addKey(SelectionKey k) {
assert Thread.holdsLock(keyLock);
int i = 0;
if ((keys != null) && (keyCount < keys.length)) {
// Find empty element of key array
for (i = 0; i < keys.length; i++)
if (keys[i] == null)
break;
} else if (keys == null) {
keys = new SelectionKey[2];
} else {
// Grow key array
int n = keys.length * 2;
SelectionKey[] ks = new SelectionKey[n];
for (i = 0; i < keys.length; i++)
ks[i] = keys[i];
keys = ks;
i = keyCount;
}
keys[i] = k;
keyCount++;
}
複製程式碼
一旦註冊到Selector
上,Channel將一直保持註冊直到其被解除註冊。在解除註冊的時候會解除Selector分配給Channel的所有資源。 也就是Channel並沒有直接提供解除註冊的方法,那我們換一個思路,我們將Selector上代表其註冊的Key取消不就可以了。這裡可以通過呼叫SelectionKey#cancel()
方法來顯式的取消key。然後在Selector
下一次選擇操作期間進行對Channel的取消註冊。
//java.nio.channels.spi.AbstractSelectionKey#cancel
/**
* 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>
*/
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;
//還是呼叫Selector的cancel方法
((AbstractSelector)selector()).cancel(this);
}
}
}
//java.nio.channels.spi.AbstractSelector#cancel
void cancel(SelectionKey k) {
synchronized (cancelledKeys) {
cancelledKeys.add(k);
}
}
//在下一次select操作的時候來解除那些要求cancel的key,即解除Channel註冊
//sun.nio.ch.SelectorImpl#select(long)
@Override
public final int select(long timeout) throws IOException {
if (timeout < 0)
throw new IllegalArgumentException("Negative timeout");
//重點關注此方法
return lockAndDoSelect(null, (timeout == 0) ? -1 : timeout);
}
//sun.nio.ch.SelectorImpl#lockAndDoSelect
private int lockAndDoSelect(Consumer<SelectionKey> action, long timeout)
throws IOException
{
synchronized (this) {
ensureOpen();
if (inSelect)
throw new IllegalStateException("select in progress");
inSelect = true;
try {
synchronized (publicSelectedKeys) {
//重點關注此方法
return doSelect(action, timeout);
}
} finally {
inSelect = false;
}
}
}
//sun.nio.ch.WindowsSelectorImpl#doSelect
protected int doSelect(Consumer<SelectionKey> action, long timeout)
throws IOException
{
assert Thread.holdsLock(this);
this.timeout = timeout; // set selector timeout
processUpdateQueue();
//重點關注此方法
processDeregisterQueue();
if (interruptTriggered) {
resetWakeupSocket();
return 0;
}
...
}
/**
* sun.nio.ch.SelectorImpl#processDeregisterQueue
* Invoked by selection operations to process the cancelled-key set
*/
protected final void processDeregisterQueue() throws IOException {
assert Thread.holdsLock(this);
assert Thread.holdsLock(publicSelectedKeys);
Set<SelectionKey> cks = cancelledKeys();
synchronized (cks) {
if (!cks.isEmpty()) {
Iterator<SelectionKey> i = cks.iterator();
while (i.hasNext()) {
SelectionKeyImpl ski = (SelectionKeyImpl)i.next();
i.remove();
// remove the key from the selector
implDereg(ski);
selectedKeys.remove(ski);
keys.remove(ski);
// remove from channel's key set
deregister(ski);
SelectableChannel ch = ski.channel();
if (!ch.isOpen() && !ch.isRegistered())
((SelChImpl)ch).kill();
}
}
}
}
複製程式碼
這裡,當Channel關閉時,無論是通過呼叫Channel#close
還是通過打斷執行緒的方式來對Channel進行關閉,其都會隱式的取消關於這個Channel的所有的keys,其內部也是呼叫了k.cancel()
。
//java.nio.channels.spi.AbstractInterruptibleChannel#close
/**
* Closes this channel.
*
* <p> If the channel has already been closed then this method returns
* immediately. Otherwise it marks the channel as closed and then invokes
* the {@link #implCloseChannel implCloseChannel} method in order to
* complete the close operation. </p>
*
* @throws IOException
* If an I/O error occurs
*/
public final void close() throws IOException {
synchronized (closeLock) {
if (closed)
return;
closed = true;
implCloseChannel();
}
}
//java.nio.channels.spi.AbstractSelectableChannel#implCloseChannel
protected final void implCloseChannel() throws IOException {
implCloseSelectableChannel();
// clone keys to avoid calling cancel when holding keyLock
SelectionKey[] copyOfKeys = null;
synchronized (keyLock) {
if (keys != null) {
copyOfKeys = keys.clone();
}
}
if (copyOfKeys != null) {
for (SelectionKey k : copyOfKeys) {
if (k != null) {
k.cancel(); // invalidate and adds key to cancelledKey set
}
}
}
}
複製程式碼
如果Selector
自身關閉掉,那麼Channel也會被解除註冊,同時代表Channel註冊的key也將變得無效:
//java.nio.channels.spi.AbstractSelector#close
public final void close() throws IOException {
boolean open = selectorOpen.getAndSet(false);
if (!open)
return;
implCloseSelector();
}
//sun.nio.ch.SelectorImpl#implCloseSelector
@Override
public final void implCloseSelector() throws IOException {
wakeup();
synchronized (this) {
implClose();
synchronized (publicSelectedKeys) {
// Deregister channels
Iterator<SelectionKey> i = keys.iterator();
while (i.hasNext()) {
SelectionKeyImpl ski = (SelectionKeyImpl)i.next();
deregister(ski);
SelectableChannel selch = ski.channel();
if (!selch.isOpen() && !selch.isRegistered())
((SelChImpl)selch).kill();
selectedKeys.remove(ski);
i.remove();
}
assert selectedKeys.isEmpty() && keys.isEmpty();
}
}
}
複製程式碼
一個channel最多可以最多隻能在特定的selector註冊一次。我們可以通過呼叫java.nio.channels.SelectableChannel#isRegistered
的方法來確定是否向一個或多個Selector註冊了channel。
//java.nio.channels.spi.AbstractSelectableChannel#isRegistered
// -- Registration --
public final boolean isRegistered() {
synchronized (keyLock) {
//我們在之前往Selector上註冊的時候呼叫了addKey方法,即每次往//一個Selector註冊一次,keyCount就要自增一次。
return keyCount != 0;
}
}
複製程式碼
至此,繼承了SelectableChannel這個類之後,這個channel就可以安全的由多個併發執行緒來使用。 這裡,要注意的是,繼承了AbstractSelectableChannel
這個類之後,新建立的channel始終處於阻塞模式。然而與Selector
的多路複用有關的操作必須基於非阻塞模式,所以在註冊到Selector
之前,必須將channel
置於非阻塞模式,並且在取消註冊之前,channel
可能不會返回到阻塞模式。 這裡,我們涉及了Channel的阻塞模式與非阻塞模式。在阻塞模式下,在Channel
上呼叫的每個I/O操作都將阻塞,直到完成為止。 在非阻塞模式下,I/O操作永遠不會阻塞,並且可以傳輸比請求的位元組更少的位元組,或者根本不傳輸任何位元組。 我們可以通過呼叫channel的isBlocking方法來確定其是否為阻塞模式。
//java.nio.channels.spi.AbstractSelectableChannel#register
public final SelectionKey register(Selector sel, int ops, Object att)
throws ClosedChannelException
{
if ((ops & ~validOps()) != 0)
throw new IllegalArgumentException();
if (!isOpen())
throw new ClosedChannelException();
synchronized (regLock) {
//此處會做判斷,假如是阻塞模式,則會返回true,然後就會丟擲異常
if (isBlocking())
throw new IllegalBlockingModeException();
synchronized (keyLock) {
// re-check if channel has been closed
if (!isOpen())
throw new ClosedChannelException();
SelectionKey k = findKey(sel);
if (k != null) {
k.attach(att);
k.interestOps(ops);
} else {
// New registration
k = ((AbstractSelector)sel).register(this, ops, att);
addKey(k);
}
return k;
}
}
}
複製程式碼
所以,我們在使用的時候可以基於以下的例子作為參考:
public NIOServerSelectorThread(int port)
{
try {
//開啟ServerSocketChannel,用於監聽客戶端的連線,他是所有客戶端連線的父管道
serverSocketChannel = ServerSocketChannel.open();
//將管道設定為非阻塞模式
serverSocketChannel.configureBlocking(false);
//利用ServerSocketChannel建立一個服務端Socket物件,即ServerSocket
serverSocket = serverSocketChannel.socket();
//為服務端Socket繫結監聽埠
serverSocket.bind(new InetSocketAddress(port));
//建立多路複用器
selector = Selector.open();
//將ServerSocketChannel註冊到Selector多路複用器上,並且監聽ACCEPT事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("The server is start in port: "+port);
} catch (IOException e) {
e.printStackTrace();
}
}
複製程式碼
因時間關係,本篇暫時到這裡,剩下的會在下一篇中進行講解。