如果你對netty的reactor線程不瞭解,建議先看下上一篇文章netty源碼分析之揭開reactor線程的面紗(一),這裏再把reactor中的三個步驟的圖貼一下
reactor線程
我們已經瞭解到netty reactor線程的第一步是輪詢出注冊在selector上面的IO事件(select),那麼接下來就要處理這些IO事件(process selected keys),本篇文章我們將一起來探討netty處理IO事件的細節
我們進入到reactor線程的 run
方法,找到處理IO事件的代碼,如下
processSelectedKeys();
跟進去
private void processSelectedKeys() {
if (selectedKeys != null) {
processSelectedKeysOptimized(selectedKeys.flip());
} else {
processSelectedKeysPlain(selector.selectedKeys());
}
}
我們發現處理IO事件,netty有兩種選擇,從名字上看,一種是處理優化過的selectedKeys,一種是正常的處理
我們對優化過的selectedKeys的處理稍微展開一下,看看netty是如何優化的,我們查看 selectedKeys
被引用過的地方,有如下代碼
private SelectedSelectionKeySet selectedKeys;
private Selector NioEventLoop.openSelector() {
//...
final SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet();
// selectorImplClass -> sun.nio.ch.SelectorImpl
Field selectedKeysField = selectorImplClass.getDeclaredField("selectedKeys");
Field publicSelectedKeysField = selectorImplClass.getDeclaredField("publicSelectedKeys");
selectedKeysField.setAccessible(true);
publicSelectedKeysField.setAccessible(true);
selectedKeysField.set(selector, selectedKeySet);
publicSelectedKeysField.set(selector, selectedKeySet);
//...
selectedKeys = selectedKeySet;
}
首先,selectedKeys是一個 SelectedSelectionKeySet
類對象,在NioEventLoop
的 openSelector
方法中創建,之後就通過反射將selectedKeys與 sun.nio.ch.SelectorImpl
中的兩個field綁定
sun.nio.ch.SelectorImpl
中我們可以看到,這兩個field其實是兩個HashSet
// Public views of the key sets
private Set<SelectionKey> publicKeys; // Immutable
private Set<SelectionKey> publicSelectedKeys; // Removal allowed, but not addition
protected SelectorImpl(SelectorProvider sp) {
super(sp);
keys = new HashSet<SelectionKey>();
selectedKeys = new HashSet<SelectionKey>();
if (Util.atBugLevel("1.4")) {
publicKeys = keys;
publicSelectedKeys = selectedKeys;
} else {
publicKeys = Collections.unmodifiableSet(keys);
publicSelectedKeys = Util.ungrowableSet(selectedKeys);
}
}
selector在調用select()
族方法的時候,如果有IO事件發生,就會往裏面的兩個field中塞相應的selectionKey
(具體怎麼塞有待研究),即相當於往一個hashSet中add元素,既然netty通過反射將jdk中的兩個field替換掉,那我們就應該意識到是不是netty自定義的SelectedSelectionKeySet
在add
方法做了某些優化呢?
帶着這個疑問,我們進入到 SelectedSelectionKeySet
類中探個究竟
final class SelectedSelectionKeySet extends AbstractSet<SelectionKey> {
private SelectionKey[] keysA;
private int keysASize;
private SelectionKey[] keysB;
private int keysBSize;
private boolean isA = true;
SelectedSelectionKeySet() {
keysA = new SelectionKey[1024];
keysB = keysA.clone();
}
@Override
public boolean add(SelectionKey o) {
if (o == null) {
return false;
}
if (isA) {
int size = keysASize;
keysA[size ++] = o;
keysASize = size;
if (size == keysA.length) {
doubleCapacityA();
}
} else {
int size = keysBSize;
keysB[size ++] = o;
keysBSize = size;
if (size == keysB.length) {
doubleCapacityB();
}
}
return true;
}
private void doubleCapacityA() {
SelectionKey[] newKeysA = new SelectionKey[keysA.length << 1];
System.arraycopy(keysA, 0, newKeysA, 0, keysASize);
keysA = newKeysA;
}
private void doubleCapacityB() {
SelectionKey[] newKeysB = new SelectionKey[keysB.length << 1];
System.arraycopy(keysB, 0, newKeysB, 0, keysBSize);
keysB = newKeysB;
}
SelectionKey[] flip() {
if (isA) {
isA = false;
keysA[keysASize] = null;
keysBSize = 0;
return keysA;
} else {
isA = true;
keysB[keysBSize] = null;
keysASize = 0;
return keysB;
}
}
@Override
public int size() {
if (isA) {
return keysASize;
} else {
return keysBSize;
}
}
@Override
public boolean remove(Object o) {
return false;
}
@Override
public boolean contains(Object o) {
return false;
}
@Override
public Iterator<SelectionKey> iterator() {
throw new UnsupportedOperationException();
}
}
該類其實很簡單,繼承了 AbstractSet
,說明該類可以當作一個set來用,但是底層使用兩個數組來交替使用,在add
方法中,判斷當前使用哪個數組,找到對應的數組,然後經歷下面三個步驟
1.將SelectionKey塞到該數組的邏輯尾部
2.更新該數組的邏輯長度+1
3.如果該數組的邏輯長度等於數組的物理長度,就將該數組擴容
我們可以看到,待程序跑過一段時間,等數組的長度足夠長,每次在輪詢到nio事件的時候,netty只需要O(1)的時間複雜度就能將 SelectionKey
塞到 set中去,而jdk底層使用的hashSet需要O(lgn)的時間複雜度
這裏關於爲何使用兩個數組循環交替使用,其實我也是很費解,思考了很久,查找SelectedSelectionKeySet
所有使用的地方,我覺得使用一個數組就能夠達到優化目的,並且不用每次都判斷使用哪個數組,所以對於該問題,我提了一個issue給netty官方,官方也給出了答覆說會跟進,issue鏈接:https://github.com/netty/netty/issues/6058#,目前在4.1.9.Final 版本中,netty已經將SelectedSelectionKeySet.java
底層使用一個數組了,鏈接
關於netty對SelectionKeySet
的優化我們暫時就跟這麼多,下面我們繼續跟netty對IO事件的處理,轉到processSelectedKeysOptimized
private void processSelectedKeysOptimized(SelectionKey[] selectedKeys) {
for (int i = 0;; i ++) {
// 1.取出IO事件以及對應的channel
final SelectionKey k = selectedKeys[i];
if (k == null) {
break;
}
selectedKeys[i] = null;
final Object a = k.attachment();
// 2.處理該channel
if (a instanceof AbstractNioChannel) {
processSelectedKey(k, (AbstractNioChannel) a);
} else {
NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
processSelectedKey(k, task);
}
// 3.判斷是否該再來次輪詢
if (needsToSelectAgain) {
for (;;) {
i++;
if (selectedKeys[i] == null) {
break;
}
selectedKeys[i] = null;
}
selectAgain();
selectedKeys = this.selectedKeys.flip();
i = -1;
}
}
}
我們可以將該過程分爲以下三個步驟
1.取出IO事件以及對應的netty channel類
這裏其實也能體會到優化過的 SelectedSelectionKeySet
的好處,遍歷的時候遍歷的是數組,相對jdk原生的HashSet
效率有所提高
拿到當前SelectionKey之後,將selectedKeys[i]
置爲null,這裏簡單解釋一下這麼做的理由:想象一下這種場景,假設一個NioEventLoop平均每次輪詢出N個IO事件,高峯期輪詢出3N個事件,那麼selectedKeys
的物理長度要大於等於3N,如果每次處理這些key,不置selectedKeys[i]
爲空,那麼高峯期一過,這些保存在數組尾部的selectedKeys[i]
對應的SelectionKey
將一直無法被回收,SelectionKey
對應的對象可能不大,但是要知道,它可是有attachment的,這裏的attachment具體是什麼下面會講到,但是有一點我們必須清楚,attachment可能很大,這樣一來,這些元素是GC root可達的,很容易造成gc不掉,內存泄漏就發生了
這個bug在 4.0.19.Final
版本中被修復,建議使用netty的項目升級到最新版本^^
2.處理該channel
拿到對應的attachment之後,netty做了如下判斷
if (a instanceof AbstractNioChannel) {
processSelectedKey(k, (AbstractNioChannel) a);
}
源碼讀到這,我們需要思考爲啥會有這麼一條判斷,憑什麼說attachment可能會是 AbstractNioChannel
對象?
我們的思路應該是找到底層selector, 然後在selector調用register方法的時候,看一下注冊到selector上的對象到底是什麼鬼,我們使用intellij的全局搜索引用功能,最終在 AbstractNioChannel
中搜索到如下方法
protected void doRegister() throws Exception {
// ...
selectionKey = javaChannel().register(eventLoop().selector, 0, this);
// ...
}
javaChannel()
返回netty類AbstractChannel
對應的jdk底層channel對象
protected SelectableChannel javaChannel() {
return ch;
}
我們查看到SelectableChannel方法,結合netty的 doRegister()
方法,我們不難推論出,netty的輪詢註冊機制其實是將AbstractNioChannel
內部的jdk類SelectableChannel
對象註冊到jdk類Selctor
對象上去,並且將AbstractNioChannel
作爲SelectableChannel
對象的一個attachment附屬上,這樣再jdk輪詢出某條SelectableChannel
有IO事件發生時,就可以直接取出AbstractNioChannel
進行後續操作
下面是jdk中的register方法
//*
//* @param sel
//* The selector with which this channel is to be registered
//*
//* @param ops
//* The interest set for the resulting key
//*
//* @param att
//* The attachment for the resulting key; may be <tt>null</tt>
public abstract SelectionKey register(Selector sel, int ops, Object att)
throws ClosedChannelException;
由於篇幅原因,詳細的 processSelectedKey(SelectionKey k, AbstractNioChannel ch)
過程我們單獨寫一篇文章來詳細展開,這裏就簡單說一下
1.對於boss NioEventLoop來說,輪詢到的是基本上就是連接事件,後續的事情就通過他的pipeline將連接扔給一個worker NioEventLoop處理
2.對於worker NioEventLoop來說,輪詢到的基本上都是io讀寫事件,後續的事情就是通過他的pipeline將讀取到的字節流傳遞給每個channelHandler來處理
上面處理attachment的時候,還有個else分支,我們也來分析一下
else部分的代碼如下
NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
processSelectedKey(k, task);
說明註冊到selctor上的attachment還有另外一中類型,就是 NioTask
,NioTask主要是用於當一個 SelectableChannel
註冊到selector的時候,執行一些任務
NioTask的定義
public interface NioTask<C extends SelectableChannel> {
void channelReady(C ch, SelectionKey key) throws Exception;
void channelUnregistered(C ch, Throwable cause) throws Exception;
}
由於NioTask
在netty內部沒有使用的地方,這裏不過多展開
3.判斷是否該再來次輪詢
if (needsToSelectAgain) {
for (;;) {
i++;
if (selectedKeys[i] == null) {
break;
}
selectedKeys[i] = null;
}
selectAgain();
selectedKeys = this.selectedKeys.flip();
i = -1;
}
我們回憶一下netty的reactor線程經歷前兩個步驟,分別是抓取產生過的IO事件以及處理IO事件,每次在抓到IO事件之後,都會將 needsToSelectAgain 重置爲false,那麼什麼時候needsToSelectAgain會重新被設置成true呢?
還是和前面一樣的思路,我們使用intellij來幫助我們查看needsToSelectAgain被使用的地方,在NioEventLoop類中,只有下面一處將needsToSelectAgain設置爲true
NioEventLoop.java
void cancel(SelectionKey key) {
key.cancel();
cancelledKeys ++;
if (cancelledKeys >= CLEANUP_INTERVAL) {
cancelledKeys = 0;
needsToSelectAgain = true;
}
}
繼續查看 cancel
函數被調用的地方
AbstractChannel.java
@Override
protected void doDeregister() throws Exception {
eventLoop().cancel(selectionKey());
}
不難看出,在channel從selector上移除的時候,調用cancel函數將key取消,並且當被去掉的key到達 CLEANUP_INTERVAL
的時候,設置needsToSelectAgain爲true,CLEANUP_INTERVAL
默認值爲256
private static final int CLEANUP_INTERVAL = 256;
也就是說,對於每個NioEventLoop而言,每隔256個channel從selector上移除的時候,就標記 needsToSelectAgain 爲true,我們還是跳回到上面這段代碼
if (needsToSelectAgain) {
for (;;) {
i++;
if (selectedKeys[i] == null) {
break;
}
selectedKeys[i] = null;
}
selectAgain();
selectedKeys = this.selectedKeys.flip();
i = -1;
}
每滿256次,就會進入到if的代碼塊,首先,將selectedKeys的內部數組全部清空,方便被jvm垃圾回收,然後重新調用selectAgain
重新填裝一下 selectionKey
private void selectAgain() {
needsToSelectAgain = false;
try {
selector.selectNow();
} catch (Throwable t) {
logger.warn("Failed to update SelectionKeys.", t);
}
}
netty這麼做的目的我想應該是每隔256次channel斷線,重新清理一下selectionKey,保證現存的SelectionKey及時有效
到這裏,我們初次閱讀源碼的時候對reactor的第二個步驟的瞭解已經足夠了。總結一下:netty的reactor線程第二步做的事情爲處理IO事件,netty使用數組替換掉jdk原生的HashSet來保證IO事件的高效處理,每個SelectionKey上綁定了netty類AbstractChannel
對象作爲attachment,在處理每個SelectionKey的時候,就可以找到AbstractChannel
,然後通過pipeline的方式將處理串行到ChannelHandler,回調到用戶方法
下一篇文章,我們將一起來看下netty中reactor線程中最後一步,runTasks
,你將瞭解到netty中異步執行任務機制的細節,盡請期待