Netty是互聯(lián)網(wǎng)中間件領(lǐng)域使用最廣泛最核心的網(wǎng)絡(luò)通信框架,幾乎所有互聯(lián)網(wǎng)中間件或者大數(shù)據(jù)領(lǐng)域均離不開Netty。
轉(zhuǎn)載https://www.cnblogs.com/chenmo-xpw/p/3938304.html
Netty、NIO
理清NIO與Netty的關(guān)系之前,我們必須先要來看看Reactor模式。Netty是一個典型的多線程的Reactor模式的使用,理解了這部分,在宏觀上理解Netty的NIO及多線程部分就不會有什么困難了。
Reactor模式
Reactor的由來
Reactor模式究竟是個什么東西呢?這要從事件驅(qū)動的開發(fā)方式說起。我們知道,對于應(yīng)用服務(wù)器,一個主要規(guī)律就是,CPU的處理速度是要遠遠快于IO速度的,如果CPU為了IO操作(例如從Socket讀取一段數(shù)據(jù))而阻塞顯然是不劃算的。好一點的方法是分為多進程或者線程去進行處理,但是這樣會帶來一些進程切換的開銷,試想一個進程一個數(shù)據(jù)讀了500ms,期間進程切換到它3次,但是CPU卻什么都不能干,就這么切換走了,是不是也不劃算?
這時先驅(qū)們找到了事件驅(qū)動,或者叫回調(diào)的方式,來完成這件事情。這種方式就是,應(yīng)用業(yè)務(wù)向一個中間人注冊一個回調(diào)(event handler),當(dāng)IO就緒后,就這個中間人產(chǎn)生一個事件,并通知此handler進行處理。這種回調(diào)的方式,也體現(xiàn)了“好萊塢原則”(Hollywood principle)-“Don't call us, we'll call you”,在我們熟悉的IoC中也有用到??磥碥浖_發(fā)真是互通的!
好了,我們現(xiàn)在來看Reactor模式。在前面事件驅(qū)動的例子里有個問題:我們?nèi)绾沃繧O就緒這個事件,誰來充當(dāng)這個中間人?Reactor模式的答案是:由一個不斷等待和循環(huán)的單獨進程(線程)來做這件事,它接受所有handler的注冊,并負責(zé)先操作系統(tǒng)查詢IO是否就緒,在就緒后就調(diào)用指定handler進行處理,這個角色的名字就叫做Reactor。
Reactor與NIO
Java中的NIO可以很好的和Reactor模式結(jié)合。關(guān)于NIO中的Reactor模式,我想沒有什么資料能比Doug Lea大神(不知道Doug Lea?看看JDK集合包和并發(fā)包的作者吧)在《Scalable IO in Java》解釋的更簡潔和全面了。NIO中Reactor的核心是Selector,我寫了一個簡單的Reactor示例,這里我貼一個核心的Reactor的循環(huán)(這種循環(huán)結(jié)構(gòu)又叫做EventLoop),剩余代碼在learning-src目錄下。
public void run() {
try {
while (!Thread.interrupted()) {
selector.select();
Set selected = selector.selectedKeys();
Iterator it = selected.iterator();
while (it.hasNext())
dispatch((SelectionKey) (it.next()));
selected.clear();
}
} catch (IOException ex) { /* ... */
}
}
與Reactor相關(guān)的其他概念
前面提到了Proactor模式,這又是什么呢?簡單來說,Reactor模式里,操作系統(tǒng)只負責(zé)通知IO就緒,具體的IO操作(例如讀寫)仍然是要在業(yè)務(wù)進程里阻塞的去做的,而Proactor模式則更進一步,由操作系統(tǒng)將IO操作執(zhí)行好(例如讀取,會將數(shù)據(jù)直接讀到內(nèi)存buffer中),而handler只負責(zé)處理自己的邏輯,真正做到了IO與程序處理異步執(zhí)行。所以我們一般又說Reactor是同步IO,Proactor是異步IO。
關(guān)于阻塞和非阻塞、異步和非異步,以及UNIX底層的機制,大家可以看看這篇文章IO - 同步,異步,阻塞,非阻塞 (亡羊補牢篇),以及陶輝(《深入理解nginx》的作者)《高性能網(wǎng)絡(luò)編程》的系列。
由Reactor出發(fā)來理解Netty
多線程下的Reactor
講了一堆Reactor,我們回到Netty。在《Scalable IO in Java》中講到了一種多線程下的Reactor模式。在這個模式里,mainReactor只有一個,負責(zé)響應(yīng)client的連接請求,并建立連接,它使用一個NIO Selector;subReactor可以有一個或者多個,每個subReactor都會在一個獨立線程中執(zhí)行,并且維護一個獨立的NIO Selector。
這樣的好處很明顯,因為subReactor也會執(zhí)行一些比較耗時的IO操作,例如消息的讀寫,使用多個線程去執(zhí)行,則更加有利于發(fā)揮CPU的運算能力,減少IO等待時間。
Netty中的Reactor與NIO
了解了多線程下的Reactor模式,我們來看看Netty吧(以下部分主要針對NIO,OIO部分更加簡單一點,不重復(fù)介紹了)。Netty里對應(yīng)mainReactor的角色叫做“Boss”,而對應(yīng)subReactor的角色叫做"Worker"。Boss負責(zé)分配請求,Worker負責(zé)執(zhí)行,好像也很貼切!以TCP的Server端為例,這兩個對應(yīng)的實現(xiàn)類分別為NioServerBoss和NioWorker(Server和Client的Worker沒有區(qū)別,因為建立連接之后,雙方就是對等的進行傳輸了)。
Netty 3.7中Reactor的EventLoop在AbstractNioSelector.run()中,它實現(xiàn)了Runnable接口。這個類是Netty NIO部分的核心。它的邏輯非常復(fù)雜,其中還包括一些對JDK Bug的處理(例如rebuildSelector),剛開始讀的時候不需要深入那么細節(jié)。我精簡了大部分代碼,保留主干如下:
abstract class AbstractNioSelector implements NioSelector {
//NIO Selector
protected volatile Selector selector;
//內(nèi)部任務(wù)隊列
private final Queue<Runnable> taskQueue = new ConcurrentLinkedQueue<Runnable>();
//selector循環(huán)
public void run() {
for (;;) {
try {
//處理內(nèi)部任務(wù)隊列
processTaskQueue();
//處理selector事件對應(yīng)邏輯
process(selector);
} catch (Throwable t) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// Ignore.
}
}
}
}
private void processTaskQueue() {
for (;;) {
final Runnable task = taskQueue.poll();
if (task == null) {
break;
}
task.run();
}
}
protected abstract void process(Selector selector) throws IOException;
}
其中process是主要的處理事件的邏輯,例如在AbstractNioWorker中,處理邏輯如下:
protected void process(Selector selector) throws IOException {
Set<SelectionKey> selectedKeys = selector.selectedKeys();
if (selectedKeys.isEmpty()) {
return;
}
for (Iterator<SelectionKey> i = selectedKeys.iterator(); i.hasNext();) {
SelectionKey k = i.next();
i.remove();
try {
int readyOps = k.readyOps();
if ((readyOps & SelectionKey.OP_READ) != 0 || readyOps == 0) {
if (!read(k)) {
// Connection already closed - no need to handle write.
continue;
}
}
if ((readyOps & SelectionKey.OP_WRITE) != 0) {
writeFromSelectorLoop(k);
}
} catch (CancelledKeyException e) {
close(k);
}
if (cleanUpCancelledKeys()) {
break; // break the loop to avoid ConcurrentModificationException
}
}
}
Netty中的多線程
下面我們來看Netty的多線程部分。一旦對應(yīng)的Boss或者Worker啟動,就會分配給它們一個線程去一直執(zhí)行。對應(yīng)的概念為BossPool和WorkerPool。對于每個NioServerSocketChannel,Boss的Reactor有一個線程,而Worker的線程數(shù)由Worker線程池大小決定,但是默認最大不會超過CPU核數(shù)*2,當(dāng)然,這個參數(shù)可以通過NioServerSocketChannelFactory構(gòu)造函數(shù)的參數(shù)來設(shè)置。
public NioServerSocketChannelFactory(
Executor bossExecutor, Executor workerExecutor,
int workerCount) {
this(bossExecutor, 1, workerExecutor, workerCount);
}
最后我們比較關(guān)心一個問題,我們之前ChannlePipeline中的ChannleHandler是在哪個線程執(zhí)行的呢?答案是在Worker線程里執(zhí)行的,并且會阻塞Worker的EventLoop。例如,在NioWorker中,讀取消息完畢之后,會觸發(fā)MessageReceived事件,這會使得Pipeline中的handler都得到執(zhí)行。
protected boolean read(SelectionKey k) {
....
if (readBytes > 0) {
// Fire the event.
fireMessageReceived(channel, buffer);
}
return true;
}
可以看到,對于處理事件較長的業(yè)務(wù),并不太適合直接放到ChannelHandler中執(zhí)行。那么怎么處理呢?我們在Handler部分會進行介紹。
參考資料:
- Scalable IO in Java http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf
- Netty5.0架構(gòu)剖析和源碼解讀 http://vdisk.weibo.com/s/C9LV9iVqH13rW/1391437855
- Reactor pattern http://en.wikipedia.org/wiki/Reactor_pattern
- Reactor - An Object Behavioral Pattern for Demultiplexing and Dispatching Handles for Synchronous Eventshttp://www.cs.wustl.edu/~schmidt/PDF/reactor-siemens.pdf
- 高性能網(wǎng)絡(luò)編程6--reactor反應(yīng)堆與定時器管理 http://blog.csdn.net/russell_tao/article/details/17452997
- IO - 同步,異步,阻塞,非阻塞 (亡羊補牢篇)http://blog.csdn.net/historyasamirror/article/details/5778378