如果你對netty的reactor執行緒不瞭解,建議先看下上一篇文章netty原始碼分析之揭開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連結:github.com/netty/netty… 版本中,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中非同步執行任務機制的細節,盡請期待
如果你想系統地學Netty,我的小冊《Netty 入門與實戰:仿寫微信 IM 即時通訊系統》可以幫助你,如果你想系統學習Netty原理,那麼你一定不要錯過我的Netty原始碼分析系列視訊:coding.imooc.com/class/230.h…