Tomcat 源碼分析 NIO (基于8.0.5)

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 圖:


Tomcat_nio.png

整個(gè)流程就是開啟一個(gè)Acceptor線程來接收請(qǐng)求, 2個(gè)Poller線程(PS: 每個(gè)線程管理一個(gè) Selector) 來處理讀寫事件, 最終真正的邏輯處理交給 Executor 來處理

4. Tomcat NIO 讀取數(shù)據(jù)

先來看一張HTTP協(xié)議的結(jié)構(gòu)圖


image.png

在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官方中的一張圖:

image.png

(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)解析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末凤价,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子鹏控,更是在濱河造成了極大的恐慌威始,老刑警劉巖枢纠,帶你破解...
    沈念sama閱讀 211,639評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異黎棠,居然都是意外死亡晋渺,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門脓斩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來木西,“玉大人,你說我怎么就攤上這事俭厚』海” “怎么了?”我有些...
    開封第一講書人閱讀 157,221評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵挪挤,是天一觀的道長(zhǎng)叼丑。 經(jīng)常有香客問我,道長(zhǎng)扛门,這世上最難降的妖魔是什么鸠信? 我笑而不...
    開封第一講書人閱讀 56,474評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮论寨,結(jié)果婚禮上星立,老公的妹妹穿的比我還像新娘。我一直安慰自己葬凳,他們只是感情好绰垂,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,570評(píng)論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著火焰,像睡著了一般劲装。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,816評(píng)論 1 290
  • 那天占业,我揣著相機(jī)與錄音绒怨,去河邊找鬼。 笑死谦疾,一個(gè)胖子當(dāng)著我的面吹牛南蹂,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播念恍,決...
    沈念sama閱讀 38,957評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼六剥,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了樊诺?” 一聲冷哼從身側(cè)響起仗考,我...
    開封第一講書人閱讀 37,718評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎词爬,沒想到半個(gè)月后秃嗜,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,176評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡顿膨,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,511評(píng)論 2 327
  • 正文 我和宋清朗相戀三年锅锨,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片恋沃。...
    茶點(diǎn)故事閱讀 38,646評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡必搞,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出囊咏,到底是詐尸還是另有隱情恕洲,我是刑警寧澤,帶...
    沈念sama閱讀 34,322評(píng)論 4 330
  • 正文 年R本政府宣布梅割,位于F島的核電站霜第,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏户辞。R本人自食惡果不足惜泌类,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,934評(píng)論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望底燎。 院中可真熱鬧刃榨,春花似錦、人聲如沸双仍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)朱沃。三九已至晴玖,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背呕屎。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評(píng)論 1 266
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留敬察,地道東北人秀睛。 一個(gè)月前我還...
    沈念sama閱讀 46,358評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像莲祸,于是被迫代替她去往敵國(guó)和親蹂安。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,514評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容

  • 1.Tomcat總體架構(gòu) Tomcat有Connector和Container兩大核心組件锐帜,Connector組件...
    monkey01閱讀 11,949評(píng)論 6 23
  • Java NIO(New IO)是從Java 1.4版本開始引入的一個(gè)新的IO API田盈,可以替代標(biāo)準(zhǔn)的Java I...
    JackChen1024閱讀 7,547評(píng)論 1 143
  • 常見的http服務(wù)器有apache,nginx缴阎,iis允瞧,tomcat等。HTTP服務(wù)器本質(zhì)上也是一種應(yīng)用程序——它...
    可樂愛上咖啡閱讀 3,974評(píng)論 1 49
  • 概述 Tomcat是一個(gè)JSP/Servlet容器蛮拔。其作為Servlet容器述暂,有三種工作模式:獨(dú)立的Servlet...
    jiangmo閱讀 2,227評(píng)論 0 13
  • 簡(jiǎn)介 Java NIO 是由 Java 1.4 引進(jìn)的異步 IO.Java NIO 由以下幾個(gè)核心部分組成: Ch...
    永順閱讀 1,787評(píng)論 0 15