1. Tomcat NIO 概述
Tomcat 8.x.x 默認(rèn)的請(qǐng)求處理都是 NIO, 據(jù)說以前處理都是 BIO (PS: 兩者的區(qū)別: 一個(gè)是從IO設(shè)備讀取數(shù)據(jù)到內(nèi)核內(nèi)存, 再?gòu)膬?nèi)核內(nèi)存copy到用戶內(nèi)存, 另一個(gè)是 通過 select 來輪訓(xùn)注冊(cè)是事件, 而且數(shù)據(jù)已經(jīng)從 IO 設(shè)備讀取到了 內(nèi)核內(nèi)存中), 而NIO對(duì)比BIO最大的好處就是少一步從IO設(shè)備讀取數(shù)據(jù) + 程序可以用更少的線程處理更多的請(qǐng)求, 尤其是在開啟 KeepAlive 的情況下
2. Tomcat NIO 組件
我們先來看一下在 NIO 模型下, Connector 的組件:
1. Acceptor : 監(jiān)聽指定的端口, 將接收到的 socket 封裝成 NioChannel 丟給 Poller 線程來進(jìn)行處理
2. NioChannel : SocketChannel 的一個(gè)包裝類, 給 SocketChannel 增加了一個(gè)屬性, 并且代理其做了一個(gè)方法
3. PollerEvent : 其的作用就是在 Poller.Selector 上異步注冊(cè) OP_READ 事件
4. SynchronizedQueue : Poller 每次運(yùn)行都會(huì)執(zhí)行里面的 PollerEvent 事件, 進(jìn)行SocketChannel注冊(cè) selector
5. Poller : 從 SynchronizedQueue 里面 poll 出PollerEvent(在 Selector 上注冊(cè)讀數(shù)據(jù)的時(shí)間)事件, 并進(jìn)行通過 selector.select 來輪訓(xùn)注冊(cè)的讀寫事件
6. SocketProcessor : Tomcat 執(zhí)行 Socket 請(qǐng)求處理的執(zhí)行單元
本質(zhì)上就是一個(gè)線程在端口上等待接收請(qǐng)求, 開啟幾個(gè) Poller 線程(每個(gè)Poller線程有一個(gè)歸屬的 Selector), 通過 round robin 的方式來分派 SocketChannel , 并且注冊(cè)對(duì)應(yīng)的 OP_READ 事件到其 Selector 上, 并且進(jìn)行 SocketChannel 后續(xù)的讀寫處理
3. Tomcat NIO 請(qǐng)求
先來看一下下面一張 UML 圖:
整個(gè)流程就是開啟一個(gè)Acceptor線程來接收請(qǐng)求, 2個(gè)Poller線程(PS: 每個(gè)線程管理一個(gè) Selector) 來處理讀寫事件, 最終真正的邏輯處理交給 Executor 來處理
4. Tomcat NIO 讀取數(shù)據(jù)
先來看一張HTTP協(xié)議的結(jié)構(gòu)圖
在Http頭部中, 每行都是通過 /r/n 字符來進(jìn)行分割, 而header 與 body 之間也是通過一個(gè)單獨(dú)的一行(這一行里面只有 /r/n 字符)來進(jìn)行分割;
Tomcat NIO 讀取數(shù)據(jù)主要在 InternalNioInputBuffer.parseRequestLine() 與 InternalNioInputBuffer.parseHeaders(); 這里對(duì) HTTP header 數(shù)據(jù)的解析也是通過判斷是否有單獨(dú)一行數(shù)據(jù)是 /r/n 來進(jìn)行判斷, 不然就進(jìn)行調(diào)用 InternalNioInputBuffer.fill(boolean timeout, boolean block) 來進(jìn)行再次讀取(為什么要再次讀取呢? 主要還是因?yàn)門CP底層在發(fā)送數(shù)據(jù)包時(shí) 不一定一下子將數(shù)據(jù)發(fā)送過來), 其實(shí)這一步也就是 IM 服務(wù)中的粘包
5. Tomcat NIO 寫數(shù)據(jù)
寫數(shù)據(jù)是通過 org.apache.catalina.connector.Response.finishResponse() 來進(jìn)行觸發(fā)的(具體寫入的步驟與 BIO 差不對(duì), 可參考 Tomcat 源碼分析 一次完整請(qǐng)求), 我這里來點(diǎn)與 BIO 不同的; 先看一下 Tomcat官方中的一張圖:
(PS: 圖片地址)
其中指明了 在NIO模式下, Response 的寫入是 Blocking 的, 而我們?cè)谕ㄟ^ SocketChannel 進(jìn)行寫數(shù)據(jù)時(shí)有可能一次不能完全寫完, 那Tomcat是這么做的呢? 直接看 NioBlockingSelector.write 方法
try {
while ( (!timedout) && buf.hasRemaining()) { // 1. 檢查數(shù)據(jù)是否寫完, 寫操作是否超時(shí)
if (keycount > 0) { //only write if we were registered for a write
int cnt = socket.write(buf); //write the data // 2. 進(jìn)行寫操作
if (cnt == -1) // 3. 寫操作失敗, 直接報(bào)異常 (有可能對(duì)方已經(jīng)關(guān)閉 socket)
throw new EOFException();
written += cnt; // 4. 累加 已經(jīng)寫的數(shù)據(jù)總和
if (cnt > 0) { // 5. 寫數(shù)據(jù)成功, continue 再次寫數(shù)據(jù)
time = System.currentTimeMillis(); //reset our timeout timer
continue; //we successfully wrote, try again without a selector
}
}
try { // 6. 寫入不成功 (cnt == 0)
if ( att.getWriteLatch()==null || att.getWriteLatch().getCount()==0) att.startWriteLatch(1);
poller.add(att,SelectionKey.OP_WRITE,reference); // 7. 通過 BlockPoller 線程將 SocketChannel 的 OP_WRITE 事件 注冊(cè)到 NioSelectorPool 中的 selector 上
if (writeTimeout < 0) { // 8. CountDownLatch 進(jìn)行不限時(shí)的等到 OP_WRITE 事件
att.awaitWriteLatch(Long.MAX_VALUE,TimeUnit.MILLISECONDS);
} else {
att.awaitWriteLatch(writeTimeout,TimeUnit.MILLISECONDS); // 9. CountDownLatch 進(jìn)行限時(shí)的等到 OP_WRITE 事件
}
} catch (InterruptedException ignore) {
// Ignore
}
if ( att.getWriteLatch()!=null && att.getWriteLatch().getCount()> 0) { // 10. 若 CountDownLatch 是被線程 interrupt 喚醒的, 將 keycount 置為 0 (CountDownLatch被 Interrupt 的標(biāo)識(shí)就是程序能繼續(xù)向下執(zhí)行, 但里面的 statue > 0)
//we got interrupted, but we haven't received notification from the poller.
keycount = 0; // 11. keycount 變成 0, 則在第一次進(jìn)入 loop 時(shí)不會(huì)接著寫數(shù)據(jù), 因?yàn)檫@時(shí)還沒有真正的 OP_WRITE 事件過來
}else {
//latch countdown has happened
keycount = 1;
att.resetWriteLatch(); // 12. OP_WRITE 事件過來了, 重置 CountDownLatch 里面的技術(shù)支持
}
if (writeTimeout > 0 && (keycount == 0))
timedout = (System.currentTimeMillis() - time) >= writeTimeout; // 13. 判斷是否寫超時(shí)
} //while
if (timedout)
throw new SocketTimeoutException(); // 14. 若是寫超時(shí)的話, 則直接拋異常
} finally {
poller.remove(att,SelectionKey.OP_WRITE); // 15. Tomcat 寫數(shù)據(jù)到客戶端成功, 移除 SocketChannel 對(duì)應(yīng)的 OP_WRITE 事件
if (timedout && reference.key!=null) {
poller.cancelKey(reference.key);
}
reference.key = null;
keyReferenceStack.push(reference);
}
通過代碼我們知道, 其實(shí)就是在 SocketChannel.write 數(shù)據(jù)的個(gè)數(shù)是0, 則將 SocketChannel的OP_WRITE事件注冊(cè)到 Selector 上(這里的 selector 是通過 NioSelectorPool 獲取的, 有單例, 也有對(duì)象池), 再通過一個(gè) CountDownLatch 來進(jìn)行阻塞, 直到 NioSelectorPool.selector 通知其有對(duì)應(yīng)的 OP_WRITE 事件;
問題來了, 這里怎么又有個(gè) NioSelectorPool, 我們明明可以注冊(cè)到 Poller 中的 selector 上, 干嘛還要注冊(cè)到 NioSelectorPool.selector 上?
補(bǔ)充知識(shí):
selector 內(nèi)部有3 個(gè)SelectionKeys 集合
1. publicKeys : 所有注冊(cè)的 SelectionKeys (PS: 包括部分取消的SelectionKeys)
(通過 selector.keys() 來獲取)
2. publicSelectedKeys :通過底層select獲取到的有觸發(fā)的 SelectionKeys 的集合
(通過 selector.selectedKeys() 來獲取)
3. cancelledKeys : SelectionKey.cancel 來觸發(fā)加入這個(gè)集合中, 或調(diào)用 SocketChannel.close() 也行
4. 調(diào)用 selector.select 或 selector.register 都會(huì)阻塞 publicKeys (通過 Synchronized 關(guān)鍵字, 見代碼 [SelectorImpl](http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/6-b27/sun/nio/ch/SelectorImpl.java?av=f))
上面的第4點(diǎn)其實(shí)已經(jīng)說明 select 或 register 都會(huì)通過 Synchronized 來進(jìn)行阻塞操作, 所以這里也就是增加 selector 來減少 對(duì) selector 操作的并發(fā)度
6. Tomcat NIO 與 BIO 對(duì)比之 KeepAlive
開啟 KeepAlive 功能下
NIOEndPoint.SocketProcessor.doRun(SelectionKey key, KeyAttachment ka) 方法下的 finally 中
getExecutor().execute(new SocketProcessor(socket, SocketStatus.OPEN_READ));
在處理好請(qǐng)求后, 只要再將 SocketChannel 再次注冊(cè)到Selector上就可以
JioEndPoint.SocketProcessor.doRun 方法下的 finally 中
getExecutor().execute(new SocketProcessor(socket, SocketStatus.OPEN_READ));
在處理好請(qǐng)求后, 將 socket 封裝出 SocketProcessor 來接著處理請(qǐng)求
可以看出, BIO對(duì)比NIO 在KeepAlive 的情況下, 需要開啟更多的線程處理 socket, 從而使得系統(tǒng)的壓力更大(PS: Http 的 KeepAlive 是默認(rèn)開啟的, 就因?yàn)檫@個(gè) KeepAlive 的開啟從而使得 NIO 相對(duì)于 BIO 在同等硬件資源下 更能并發(fā)處理請(qǐng)求)
7. 總結(jié):
Reactor 的線程模型其實(shí)大多數(shù)開源項(xiàng)目都是差不多的(主要區(qū)別在是否開啟多個(gè) Selector ), 而這里只是對(duì) NIO 與 BIO 做了一個(gè)簡(jiǎn)單的對(duì)比, 隨著代碼的深入, 越發(fā)覺得 要正真掌握 NIO 是需要 深入理解 TCP/IP + Unix 網(wǎng)絡(luò)編程(PS: 至于粘包拆包 + NIO CPU 100% 的bug 好像 Tomcat 沒做對(duì)應(yīng)的修復(fù), 這兩步其實(shí)在 Netty 里面已經(jīng)做了, 可以參照 Netty 的代碼獲知)
8. 參考
Tomcat源碼分析之:ServletOutputStream的實(shí)現(xiàn)
Tomcat源碼閱讀之底層IO封裝(1)InternalNioInputBuffer的分析
Tomcat 7.0 原理與源碼分析
Tomcat 內(nèi)核設(shè)計(jì)剖析
Tomcat 架構(gòu)解析