Tomcat剖析之架構(gòu)篇(一)

前言

早在之前寫(xiě)過(guò)一些http玩具服務(wù)器,總感覺(jué)無(wú)法繼續(xù)前進(jìn)了,期間花了比較多的時(shí)間在基礎(chǔ)知識(shí)上,前段時(shí)間想著直接從用的比較多的服務(wù)器開(kāi)始,對(duì)于Java開(kāi)發(fā)者來(lái)說(shuō),自然Tomcat是首選,但有一個(gè)比較大的問(wèn)題是經(jīng)過(guò)了近20年的發(fā)展,它已經(jīng)成為了一個(gè)十分系統(tǒng)、復(fù)雜的框架了,讀起來(lái)肯定容易陷入泥潭,回想起之前學(xué)其它原理一樣,一開(kāi)始就看了比較復(fù)雜的一張架構(gòu)圖,然后就沒(méi)有然后了...,不過(guò)還好有《how tomcat works》這種神書(shū),雖說(shuō)是講的Tomcat4,5版本的,但關(guān)鍵在于從實(shí)際問(wèn)題出發(fā)描述,尋求解決方式,一步步的將其從百來(lái)行代碼的玩具構(gòu)建成了一個(gè)功能完整的、有著較強(qiáng)擴(kuò)展性的框架。雖然到現(xiàn)在的Tomcat9版本有較大的變化,但核心還是沒(méi)變,了解了早期版本的源碼之后再看現(xiàn)在的版本就不會(huì)像無(wú)頭蒼蠅一樣亂撞,從一條線出發(fā),清楚整個(gè)流程,學(xué)習(xí)設(shè)計(jì),先不管太多細(xì)節(jié)性的問(wèn)題,這也是我看了一些技術(shù)書(shū)籍和文章之后的體會(huì),發(fā)現(xiàn)很多都是只談概念,不講這樣做的原因,就很難讓讀者帶入自己的思考、融入書(shū)本去讀,自然難以閱讀下去,也容易理解不夠深,忘記得也就更加快。用這篇博客主要來(lái)解析一下Tomcat的架構(gòu),因?yàn)門omcat涉及到的功能模塊比較多,這里只從它的核心功能出發(fā),附加的組件只簡(jiǎn)單介紹一下。本來(lái)打算直接寫(xiě)一篇從源碼開(kāi)始的,最后寫(xiě)一個(gè)簡(jiǎn)單的Tomcat的,寫(xiě)的過(guò)程中發(fā)現(xiàn)很多東西都要去解釋,而且難以將前后串起來(lái),所以打算把它拆解成架構(gòu)篇,源碼篇,實(shí)踐篇一共三篇,基于Tomcat9,這里第一篇主要介紹Tomcat的核心組件,以及整體的架構(gòu)和運(yùn)作方式,不會(huì)涉及過(guò)多的源碼,現(xiàn)在開(kāi)始正文。

Tomcat總體架構(gòu)

Tomcat本質(zhì)是一個(gè)應(yīng)用服務(wù)器 + Servlet容器, 首先借用一張圖看看它的的整體架構(gòu)

整體架構(gòu)

可以看到 頂層是一個(gè)Server,它是運(yùn)行著的Tomcat服務(wù)器的具體表示,一個(gè)Tomcat只能有一個(gè)Server,而一個(gè)Server可以有多個(gè)Service,Service表示完整的服務(wù),用來(lái)管理tomcat核心的組件,后面再進(jìn)行講述。
所以總的來(lái)說(shuō),Tomcat需要實(shí)現(xiàn)一下兩個(gè)核心功能,(SpringMVC本質(zhì)也是對(duì)Servlet的封裝,將DispatcherServlet加載到tomcat中,將最終請(qǐng)求的處理,使用反射進(jìn)行相應(yīng)參數(shù)的獲取和綁定,然后調(diào)用對(duì)應(yīng)的方法,最后還是由tomcat建立的TCP連接通道的包裝對(duì)象將數(shù)據(jù)發(fā)送出去.)

1. 處理Socket連接, 負(fù)責(zé)網(wǎng)絡(luò)字節(jié)流與Resquest和Response對(duì)象的轉(zhuǎn)換.
2. 加載和管理Servlet,以及請(qǐng)求的具體處理.

因此tomcat設(shè)計(jì)了兩個(gè)核心組件連接器(connector) 和 容器(container), 連接器負(fù)責(zé)對(duì)外交流,容器負(fù)責(zé)內(nèi)部處理,對(duì)應(yīng)著上述兩步。接下來(lái)就從連接器開(kāi)始

連接器

連接器(connector) 內(nèi)部持有一個(gè)實(shí)現(xiàn)了ProtocolHandler接口的對(duì)象,來(lái)看看這個(gè)接口具體的實(shí)現(xiàn)類的類圖

ProtocolHandler

根據(jù)名稱就可以看出ProtocolHandler其實(shí)就是對(duì)協(xié)議的抽象,先用一個(gè)實(shí)現(xiàn)了ProtocolHandler接口的抽象類AbstarctProtocol, 然后有兩類協(xié)議,分別是Ajp和Http1.1協(xié)議,這里就用了兩個(gè)不同的抽象類來(lái)分別表示AbstractAjpProtocol和AbstractHttp11Protocol。對(duì)于AbstractAjpProtocol類,只有三個(gè)子類,剛好分別是使用Nio,Apr,Nio2三種不同IO模型實(shí)現(xiàn)的Ajp協(xié)議;對(duì)于AbstractHttp11Protocol類,也同樣是使用了三種不同的IO模型來(lái)實(shí)現(xiàn)的,不同地方在于對(duì)于Nio和Nio2,不是直接是繼承了AbstractHttp11Protocol,而是通過(guò)一個(gè)繼承了該類的抽象父類AbstractHttp11JsseProtocol,這實(shí)際上就是為了支持傳輸安全的Socket,也就是我們常見(jiàn)的Https協(xié)議(傳輸?shù)募用芘c解密實(shí)際是在應(yīng)用層來(lái)做的,具體使用了HTTP+ TLS協(xié)議來(lái)實(shí)現(xiàn))。Http協(xié)議應(yīng)該都比較熟悉了,這里簡(jiǎn)單介紹一下Ajp協(xié)議,眾所周知,HTTP協(xié)議是基于TCP協(xié)議實(shí)現(xiàn)的純文本的一個(gè)協(xié)議,而Ajp協(xié)議是一個(gè)基于TCP實(shí)現(xiàn)的二進(jìn)制協(xié)議,內(nèi)部做了較多的優(yōu)化,我們平時(shí)使用的基本都是Http協(xié)議,因?yàn)闉g覽器或者操作系統(tǒng),以及各種網(wǎng)絡(luò)編程相關(guān)的庫(kù)內(nèi)部都實(shí)現(xiàn)了Http協(xié)議,所以我們使用起來(lái)都是無(wú)感知的。Tomcat內(nèi)部雖然實(shí)現(xiàn)了Ajp協(xié)議,但我們的瀏覽器等基礎(chǔ)軟件并沒(méi)有實(shí)現(xiàn),所以肯定是無(wú)法直接使用該協(xié)議進(jìn)行數(shù)據(jù)交互的,因此一個(gè)辦法就是在服務(wù)器端做一個(gè)反向代理, 做反向代理的服務(wù)器幫我們實(shí)現(xiàn)了從Http協(xié)議到Ajp的雙向轉(zhuǎn)換即可(實(shí)際情況實(shí)現(xiàn)了Ajp協(xié)議的服務(wù)器較少,所以Ajp相關(guān)的端口默認(rèn)是關(guān)閉的),使用較多的自然就是Apache和Nginx服務(wù)器,Apache是直接支持Ajp協(xié)議的,而Nginx我看了下官網(wǎng),沒(méi)找到相關(guān)的,不過(guò)看到了第三方實(shí)現(xiàn)了Ajp協(xié)議的Nginx反向代理的模塊(關(guān)于Ajp協(xié)議的更多信息可以參考Tomcat官方文檔https://tomcat.apache.org/connectors-doc/ajp/ajpv13a.html)。


ProtocolHandler接口的實(shí)現(xiàn)類里面持有一個(gè)AbstractEndPoint,這就是真正建立,管理連接的地方,每個(gè)EndPoint內(nèi)部使用了多個(gè)Acceptor(每個(gè)都是一個(gè)新啟動(dòng)的線程)來(lái)監(jiān)聽(tīng)新到來(lái)連接請(qǐng)求,建立連接后,會(huì)把連接對(duì)應(yīng)的通道注冊(cè)到一個(gè)Poller(輪詢器)中,EndPoint里面也是持有了多個(gè)Poller(每個(gè)也都是一個(gè)新啟動(dòng)的線程),當(dāng)有讀寫(xiě)事件就緒時(shí)Poller會(huì)把數(shù)據(jù)通道(Channel)交給Processor處理真正的讀寫(xiě),先大概有個(gè)了解,具體的實(shí)現(xiàn)在源碼分析篇里面再進(jìn)行解析。對(duì)與AbstractEndPoint的實(shí)現(xiàn)對(duì)應(yīng)了上述的幾種IO模型,包括Nio, Nio2,Apr,看下類圖,

EndPoint

基本是與上面對(duì)應(yīng)的,然后來(lái)簡(jiǎn)單介紹一下Tomcat中的幾種IO模型,要了解IO模型首先要搞清楚網(wǎng)絡(luò)IO的過(guò)程分為兩步, 用戶線程發(fā)起網(wǎng)絡(luò)IO操作的請(qǐng)求后

1.用戶線程等待內(nèi)核數(shù)據(jù)從網(wǎng)卡緩沖區(qū)拷貝到內(nèi)核空間
2.內(nèi)核將數(shù)據(jù)從內(nèi)核空間拷貝到用戶空間

來(lái)說(shuō)說(shuō)為什么需要這兩個(gè)過(guò)程,因?yàn)榫W(wǎng)絡(luò)傳輸是基本上都是基于TCP/UDP協(xié)議的,特別是對(duì)于TCP而言,接收到數(shù)據(jù)后還需要發(fā)送相對(duì)應(yīng)的ack包,表示接收方已經(jīng)接收到數(shù)據(jù)了,包的傳輸過(guò)程不穩(wěn)定,可能會(huì)受到各種因素的影響,所以要提高數(shù)據(jù)傳輸?shù)男实脑?,就盡量減少網(wǎng)絡(luò)數(shù)據(jù)包的往返的次數(shù),就盡可能的多接收一點(diǎn)數(shù)據(jù)后再進(jìn)行應(yīng)答;同樣,寫(xiě)數(shù)據(jù)也是,盡量要讓緩沖區(qū)有較多的數(shù)據(jù)后再真正讓網(wǎng)卡進(jìn)行發(fā)送。

接收數(shù)據(jù)到應(yīng)用層的過(guò)程

網(wǎng)卡先接收數(shù)據(jù)到它內(nèi)部的緩沖區(qū)隊(duì)列,等網(wǎng)卡的緩沖區(qū)隊(duì)列滿后再通過(guò)發(fā)起一個(gè)硬件中斷,CPU收到該中斷后就會(huì)通知操作系統(tǒng)的內(nèi)核,接著,內(nèi)核會(huì)根據(jù)會(huì)根據(jù)中斷信號(hào)在中斷信號(hào)表里面查找對(duì)應(yīng)的中斷處理程序,接下來(lái)網(wǎng)卡中斷處理程序會(huì)為網(wǎng)絡(luò)幀分配內(nèi)核數(shù)據(jù)結(jié)構(gòu)(sk_buff),此時(shí)CPU再填充接收數(shù)據(jù)需要的一些信息到主板上的DMAC(DMA Controller)芯片,CPU此時(shí)可以去干其它事了,DMAC會(huì)將網(wǎng)卡緩沖區(qū)的數(shù)據(jù)拷貝到內(nèi)核分配的數(shù)據(jù)結(jié)構(gòu),也就是sk_buff 緩沖區(qū)中,這種方式也就是常說(shuō)的DMA;然后再通過(guò)軟中斷(注意軟中斷的發(fā)起很可能不是即刻的),通知內(nèi)核收到了新的網(wǎng)絡(luò)幀。接下來(lái)中斷處理程序就開(kāi)始從下層到上層開(kāi)始依次拆包解析,一直到傳輸層再根據(jù)包的TCP/UDP頭部信息找到對(duì)應(yīng)的Socket,然后將數(shù)據(jù)拷貝到Socket的接收緩沖區(qū),此時(shí)表示數(shù)據(jù)已經(jīng)接收好了,此時(shí)應(yīng)用層就可以使用對(duì)應(yīng)的Socket通道進(jìn)行數(shù)據(jù)讀取了。由于用戶空間是不能直接訪問(wèn)操作系統(tǒng)的內(nèi)核空間的,所以內(nèi)核空間的數(shù)據(jù)必須要拷貝到用戶空間才能進(jìn)行讀取,也就是上述的拷貝到Socket緩沖區(qū)的地方才是將數(shù)據(jù)拷貝到了用戶空間。至于寫(xiě)數(shù)據(jù)剛好是相反的過(guò)程,這里就不多說(shuō)了。

Nio

同步非阻塞IO,具體讀數(shù)據(jù)時(shí)還是同步的方式,也就是指數(shù)據(jù)從內(nèi)核空間拷貝用戶空間的這段時(shí)間一直是阻塞的,等數(shù)據(jù)到了用戶空間再將用戶線程喚醒。對(duì)于傳統(tǒng)的BIO(同步阻塞)而言,不管是連接的建立,數(shù)據(jù)讀寫(xiě)的就緒以及數(shù)據(jù)從網(wǎng)卡到內(nèi)核空間,從內(nèi)核空間拷貝到用戶空間都是阻塞的,所以必須用新的線程管理著具體的Socket連接,而對(duì)于Java來(lái)說(shuō),線程是直接映射到操作系統(tǒng)內(nèi)核的線程,所以資源是比較重量級(jí)并且是十分有限的的,而大部分時(shí)間線程又在等待,并且線程數(shù)過(guò)多會(huì)造成大量的線程上下文切換,為了解決這個(gè)問(wèn)題才產(chǎn)生的新的IO模型;對(duì)于一個(gè)連接通道而言阻塞不阻塞其實(shí)沒(méi)差別,沒(méi)數(shù)據(jù)的話,不阻塞因?yàn)橐WC數(shù)據(jù)同步,也要不停的對(duì)一個(gè)連接做空輪詢,白白消耗CPU時(shí)鐘周期,并且也沒(méi)辦法做其它的事了,所以一般具體實(shí)現(xiàn)時(shí)現(xiàn)時(shí)是使用了單獨(dú)的Selector(多路復(fù)用器)來(lái)管理多個(gè)連接,去輪詢多個(gè)通道(連接)是否有事件就緒,也就是內(nèi)核已經(jīng)接收到了數(shù)據(jù)并完成了包的拆解,然后把有就緒事件的通道交給專門進(jìn)行讀寫(xiě)任務(wù)的線程池來(lái)處理,這樣也不會(huì)影響到其它有事件就緒的通道,具體的實(shí)現(xiàn)是依賴操作系統(tǒng)底層的epoll(Linux)或iocp(Windows)機(jī)制,這種IO模型能只用少量的線程管理就能大量的數(shù)據(jù)通道(雖然是同步,但由于CPU將數(shù)據(jù)進(jìn)行拷貝時(shí)的速度太快了,而大多數(shù)情況下數(shù)據(jù)包都比較小,所以在應(yīng)用層也基本無(wú)感知),也是目前使用較多的IO模型。

Nio2

AIO,異步非阻塞IO,就連數(shù)據(jù)的讀寫(xiě)也無(wú)需等待,只要設(shè)置一個(gè)實(shí)現(xiàn)回調(diào)的接口的對(duì)象,就能在數(shù)據(jù)收發(fā)完成后主動(dòng)進(jìn)行通知需要回調(diào)的對(duì)象,AIO是在后面出來(lái)的,一般場(chǎng)景下同步非阻塞IO已經(jīng)完全夠用了,在數(shù)據(jù)包量較大時(shí)使用AIO就能擁有更好的性能。

Apr

同步非阻塞IO,前面兩者都是JDK自帶的,而Apr(Apache Portable Runtime Libraries)是使用的Apache可移植運(yùn)行時(shí)庫(kù),內(nèi)部是采用C語(yǔ)言實(shí)現(xiàn)的,具體的實(shí)現(xiàn)也是用了操作系統(tǒng)epoll機(jī)制,因?yàn)槭怯肅語(yǔ)言實(shí)現(xiàn),Java層的調(diào)用就是使用JNI的方式進(jìn)行。那么同樣是同步非阻塞IO,為什么Tomcat要多搞這么一個(gè)連接器呢?肯定是在性能上面有了較多的優(yōu)化,不然沒(méi)必要吧,接下來(lái)就闡述一下具體做了哪些的優(yōu)化

1.TCP協(xié)議層的優(yōu)化

AprEndPoint類里面有一個(gè)參數(shù)名為deferAccept,它對(duì)應(yīng)了TCP協(xié)議里面的TCP_DEFER_ACCEPT,表示開(kāi)啟延遲接收,設(shè)置這個(gè)參數(shù)后當(dāng)客戶端有新的連接請(qǐng)求時(shí)服務(wù)器端先不建立連接,而是直到客戶端有數(shù)據(jù)時(shí)再接受連接,這樣的好處是在傳輸層減少了包的往返次數(shù),在應(yīng)用層減少了Selector查詢的連接數(shù)量,減少了CPU的消耗。

2.JVM堆內(nèi)存與本地內(nèi)存

首先從JVM談起,Java對(duì)象的實(shí)例化、數(shù)組等,都是JVM給我們?cè)贘ava堆里面分配的空間,而JVM本身其實(shí)也只是一個(gè)進(jìn)程,所以JVM內(nèi)存也只是進(jìn)程空間的一部分,整個(gè)進(jìn)程空間內(nèi),JVM之外的部分叫本地內(nèi)存,看看下面的圖

JVM進(jìn)程

Tomcat的EndPoint組件在接收網(wǎng)絡(luò)數(shù)據(jù)時(shí)需要提前分配一個(gè)字節(jié)數(shù)組,Java通過(guò)JNI調(diào)用將字節(jié)數(shù)組的內(nèi)存地址傳給C代碼,C代碼通過(guò)操作系統(tǒng)的API讀取Socket,并把數(shù)據(jù)填充到這個(gè)字節(jié)數(shù)組。Java NIO提供了兩種方式來(lái)分配字節(jié)數(shù)組: HeapByteBuffer 和 DirectedByteBuffer,對(duì)應(yīng)下面的代碼

//分配HeapByteBuffer
ByteBuffer buf = ByteBuffer.allocate(1024);

//分配DirectByteBuffer
ByteBuffer buf = ByteBuffer.allocateDirect(1024);

使用HeapByteBuffer的方式,字節(jié)數(shù)組所需的空間是直接在Java堆上面進(jìn)分配的,由虛擬機(jī)所管理,使用這種方式,數(shù)據(jù)到內(nèi)核空間后,具體拷貝數(shù)據(jù)時(shí),得先把數(shù)據(jù)拷貝到臨時(shí)的本地內(nèi)存, 然后再?gòu)谋镜貎?nèi)存拷貝到Java堆,使用本地內(nèi)存來(lái)進(jìn)行中轉(zhuǎn)的目的是為了防止直接從內(nèi)核拷貝到JVM堆時(shí)發(fā)生GC,分配的字節(jié)數(shù)組可能會(huì)進(jìn)行移動(dòng),這樣之前的地址空間就會(huì)失效,而從本地內(nèi)存拷貝時(shí)不滿足JVM可安全的進(jìn)行垃圾回收的條件,所以不會(huì)觸發(fā)GC;使用DirectByteBuffer的方式,它持有的接收數(shù)據(jù)的字節(jié)數(shù)組所需的內(nèi)存是在本地內(nèi)存進(jìn)行分配的,而這部分內(nèi)存不是由JVM進(jìn)行管理的,在Java堆里面的該對(duì)象實(shí)例,僅僅保存了該對(duì)象所持有的字節(jié)數(shù)組的地址,在真正進(jìn)行數(shù)據(jù)收發(fā)時(shí)是把這個(gè)內(nèi)存地址傳遞給C代碼,然后進(jìn)行后續(xù)處理,用這種方式減少了JVM堆與本地內(nèi)存之間的數(shù)據(jù)拷貝。但由于這部分內(nèi)存不是被JVM所管理,發(fā)生內(nèi)存泄漏時(shí)難以定位,所以Tomcat的NioEndPoint和Nio2EndPoint都使用了第一種方式,而AprEndPoint使用了第二種方式,反正具體的管理是交給Apr的C代碼去做的。實(shí)際上很多的網(wǎng)絡(luò)編程框架都使用了第二種方式,比如Netty,由于本地內(nèi)存不方便JVM進(jìn)行內(nèi)存管理,它就使用了本地內(nèi)存池的方式。

3.sendfile

除了前面所說(shuō)的使用DirectByteBuffer來(lái)進(jìn)行優(yōu)化外,APR在文件發(fā)送的場(chǎng)景也做了比較好的優(yōu)化,使用傳統(tǒng)的方式,在發(fā)送文件時(shí),如果使用HeapByteBuffer的方式,首先通過(guò)系統(tǒng)調(diào)用需要先將文件讀到內(nèi)核緩沖區(qū),然后再將數(shù)據(jù)拷貝到Java應(yīng)用程序的本地內(nèi)存緩沖區(qū),最后再拷貝到JVM堆,此時(shí)才可以調(diào)用Socket真正進(jìn)行數(shù)據(jù)的寫(xiě),寫(xiě)的時(shí)候同樣也要先寫(xiě)到本地內(nèi)存緩沖區(qū),然后再拷貝到內(nèi)核的緩沖區(qū),最后再使用網(wǎng)卡發(fā)送具體的數(shù)據(jù);而使用APR的方式,數(shù)據(jù)不需要拷貝到JVM進(jìn)程相關(guān)的緩沖區(qū),只需要把記錄數(shù)據(jù)位置和長(zhǎng)度等相關(guān)的信息填充到Socket緩沖區(qū)中,接著數(shù)據(jù)直接從內(nèi)核緩沖區(qū)傳遞給網(wǎng)卡,這兩種方式可以看看下面的圖


兩種方式比較

很顯然,APR方式文件的數(shù)據(jù)是直接從內(nèi)核區(qū)域進(jìn)行發(fā)送的,一共減少了4次多余的數(shù)據(jù)拷貝,自然大大節(jié)省了CPU以及內(nèi)存的資源。


容器

如下圖所示,容器主要由Engine、Host、Context、Wrapper四部分組成

容器組成結(jié)構(gòu)

Tomcat采用了這種分層架構(gòu)的方式,使得其具有了極大的靈活性,因?yàn)檫@些組件完全可以根據(jù)我們自己的需求去進(jìn)行添加實(shí)現(xiàn),又可以直接復(fù)用已有的組件。Engine表示引擎,可以用來(lái)管理多個(gè)虛擬主機(jī),Host表示虛擬主機(jī),可以管理多個(gè)WEB應(yīng)用, Context表示W(wǎng)EB應(yīng)用,可以管理多個(gè)Wrapper,Wrapper實(shí)際上是對(duì)Servlet的封裝。Container整個(gè)組件的通信,采用了責(zé)任鏈模式,每個(gè)組件里面都有一個(gè)pipeline用來(lái)存放Valve,Valve就是實(shí)際請(qǐng)求通過(guò)時(shí)需要經(jīng)過(guò)的節(jié)點(diǎn), 每層組件pipeline的尾節(jié)點(diǎn)是一個(gè)BasicValve, 這也是必須要擁有的一個(gè)節(jié)點(diǎn),該節(jié)點(diǎn)是直接在當(dāng)前層級(jí)的組件對(duì)象實(shí)例化時(shí)在構(gòu)造方法里面進(jìn)行添加的,作用是用來(lái)與下一層組件通信,當(dāng)下一層的子組件有多個(gè)時(shí),就需要在BasicValve節(jié)點(diǎn)建立映射,然后就找到下層組件持有的pipeline的第一個(gè)valve,以此類推,直到請(qǐng)求正確的找到最終需要處理它的Servlet。直接按名字來(lái)進(jìn)行理解也是非常形象的,pipeline表示管道,(這里直接把它當(dāng)成入口是開(kāi)著的,出口是用一個(gè)Basic閥門關(guān)著的),valve表示閥門,每個(gè)管道是隸屬于每個(gè)單獨(dú)的組件的,要讓這些組件連接起來(lái),就直接把出口的閥門對(duì)接到下層組件的管道的入口,數(shù)據(jù)流通時(shí)才去開(kāi)啟閥門。也就是采用pipeline和valve這么一種方式,可以在任何我們感興趣的地方進(jìn)行攔截,也就是往管道的任何地方都可以插入一道新的閥門,Tomcat內(nèi)部的很多擴(kuò)展的插件就是這樣實(shí)現(xiàn)的,比如配置不同的訪問(wèn)認(rèn)證方式、session管理、訪問(wèn)日志、錯(cuò)誤記錄、SSL/TLS認(rèn)證等。

JSP文件的解析與處理

還有需要清楚的一點(diǎn)是,對(duì)于JSP文件的處理,其實(shí)就是使用了上圖的Jasper模塊,不過(guò)這個(gè)模塊不是Tomcat本身自帶的。 Tomcat有多種啟動(dòng)方式,為了方便說(shuō)明,以內(nèi)嵌式(SpringBoot就是使用的這種方式)啟動(dòng)為例,對(duì)應(yīng)的是org.apache.catalina.startup.Tomcat類,SpringBoot會(huì)首先實(shí)例化這個(gè)對(duì)象,默認(rèn)的web.xml文件沒(méi)有找到的情況下,會(huì)去調(diào)用這么一個(gè)方法

public static void initWebappDefaults(Context ctx) {
        // Default servlet
        Wrapper servlet = addServlet(
                ctx, "default", "org.apache.catalina.servlets.DefaultServlet");
        servlet.setLoadOnStartup(1);
        servlet.setOverridable(true);

        // JSP servlet (by class name - to avoid loading all deps)
        servlet = addServlet(
                ctx, "jsp", "org.apache.jasper.servlet.JspServlet");
        servlet.addInitParameter("fork", "false");
        servlet.setLoadOnStartup(3);
        servlet.setOverridable(true);

        // Servlet mappings
        ctx.addServletMappingDecoded("/", "default");
        ctx.addServletMappingDecoded("*.jsp", "jsp");
        ctx.addServletMappingDecoded("*.jspx", "jsp");
    }
  .....

邏輯很清楚,它會(huì)個(gè)Context添加兩個(gè)默認(rèn)的Servlet,也就是DefaultServlet和JspServlet,然后通過(guò)addServlet()方法,這個(gè)方法會(huì)把Servlet包裝成一個(gè)Wrapper,然后添加到Context的子容器中,并進(jìn)行相應(yīng)的映射,這樣當(dāng)有JSP頁(yè)面的請(qǐng)求到來(lái)時(shí),在Context的pipeline的BasicValve里面會(huì)直接拿到請(qǐng)求對(duì)象request對(duì)應(yīng)的Wrapper,現(xiàn)在關(guān)鍵需要知道的是request是怎么和wrapper對(duì)應(yīng)起來(lái)的,request內(nèi)部有一個(gè)MappingData對(duì)象實(shí)例,該實(shí)例里面持有一個(gè)Wrapper對(duì)象,其實(shí)這個(gè)對(duì)象是連接器(connector)把請(qǐng)求給容器(container)處理之前,也就是把request對(duì)象交給Adapter處理時(shí),如果是jsp則根據(jù)初始化時(shí)建立的映射使用的子容器Wrapper就是包裝了JspServlet的這個(gè),把這個(gè)直接賦值給MappingData的Wrapper即可,這個(gè)JspServlet會(huì)在內(nèi)部把jsp文件進(jìn)行解析處理,編譯成繼承了HttpJspBase的類,而HttpJspBase又是繼承了HttpServlet類,最后實(shí)例化這個(gè)類進(jìn)行最終的處理。

兩大核心組件總覽

如果上述連接器(connector)和容器(container)的作用還沒(méi)有說(shuō)明白的話再看看下面的這張圖,描述了一個(gè)請(qǐng)求從連接器到容器運(yùn)轉(zhuǎn)的流程


tomcat核心

由上圖可知Connector 和 Container組件是被一個(gè)Service來(lái)進(jìn)行管理的,之前不是已經(jīng)有一個(gè)Server對(duì)象了嗎,那么為什么還要搞Service這么一個(gè)對(duì)象來(lái)進(jìn)行管理呢?這里回到Tomcat的設(shè)計(jì),它是把處理Socket連接相關(guān)的組(Connector)和處理Servlet相關(guān)的組件分開(kāi)(Container),也就意味著對(duì)Container來(lái)說(shuō),是不關(guān)心Connector內(nèi)部的處理的,不管是它內(nèi)部的IO模型,還是使用的應(yīng)用層協(xié)議都不關(guān)心,只要你最后給我包裝好的Request和Response對(duì)象即可,這個(gè)適配的操作就是通過(guò)Adapter接口的實(shí)例對(duì)象CoyoteAdapter來(lái)做的,正因?yàn)檫@兩個(gè)核心組件是不同的運(yùn)作方式,所以需要一個(gè)Service對(duì)象進(jìn)行它們生命周期的管理,而Server前面說(shuō)過(guò)是對(duì)Tomcat運(yùn)行著的服務(wù)器的表示,主要作用就是加載一些外部的配置,控制服務(wù)器運(yùn)行的狀態(tài),也能方便的控制多個(gè)Service。Service可以配置多個(gè)連接器,反正最后進(jìn)行了請(qǐng)求和響應(yīng)對(duì)象的的適配操作,直接交給同一個(gè)容器進(jìn)行處理即可,這樣的方式也方便了開(kāi)發(fā)人員進(jìn)行協(xié)議的擴(kuò)展。

生命周期

從上面的結(jié)構(gòu)可知,tomcat實(shí)際運(yùn)行中的輔助組件和容器可能是較多的,那么用什么方式能統(tǒng)一的管理這些輔助組件、容器的生命周期呢?Tomcat內(nèi)部提供了一種優(yōu)雅的方式,
每個(gè)容器或者組件都實(shí)現(xiàn)了Lifecycle接口, 它提供了init()、start()、stop()等方法,只要容器相關(guān)的組件和其子容器都同樣實(shí)現(xiàn)了該方法,就可以方便一鍵式啟動(dòng)/關(guān)閉組件和子容器。在這樣的規(guī)則下,父容器也不需要知道子容器是什么,只要子容器只需要實(shí)現(xiàn)LifeCycle接口即可,也就是說(shuō)子容器的生命周期完全地由父容器來(lái)管理。并且該接口還提供了添加、刪除LifecycleListener的方法,用來(lái)給當(dāng)前的容器注冊(cè)和解除監(jiān)聽(tīng)器,這樣在生命周期的某個(gè)時(shí)刻,可以發(fā)送事件,外部就能監(jiān)聽(tīng)到發(fā)送的事件,并進(jìn)行相應(yīng)的處理,各層容器的一些配置文件實(shí)際上就是實(shí)現(xiàn)了LifecycleListener接口,將容器與它的配置文件的處理進(jìn)行了解耦,其實(shí)這就是使用了觀察者模式, 某個(gè)具體的輔助組件或容器是被觀察者, 而實(shí)現(xiàn)了事件監(jiān)聽(tīng)器接口的對(duì)象是觀察者,只要將觀察者注冊(cè)到被觀察者管理的觀察者列表,這樣當(dāng)有事件到達(dá)時(shí),被觀察者就可以通知到所有觀察者。接下來(lái)看看類圖

Lifecycle

可以清楚的看到,Tomcat的這些組件都是實(shí)現(xiàn)了Lifecycle接口,這些Standardxxx都是Tomcat內(nèi)這些組件的標(biāo)準(zhǔn)實(shí)現(xiàn),不僅僅是核心組件,幾乎所有的輔助組件,也是實(shí)現(xiàn)了這個(gè)接口,這樣的做法極大的方便了全局的資源的統(tǒng)一管理。

輔助模塊

除了前文提到的,Tomcat輔助模塊還有一些,這里簡(jiǎn)單的列舉一下。

基于Realm的權(quán)限認(rèn)證

Realm是用戶名和密碼的“數(shù)據(jù)庫(kù)”,用于標(biāo)示W(wǎng)eb應(yīng)用程序的有效用戶,以及與每個(gè)有效用戶相關(guān)的角色列表,角色類似于Unix操作的系統(tǒng)中的組,因此對(duì)具有特定角色的所有用戶授予對(duì)特定Web應(yīng)用程序資源的訪問(wèn)權(quán)限,使用該組件可以將Servlet容器對(duì)接到某些生產(chǎn)環(huán)境中已經(jīng)存在的認(rèn)證數(shù)據(jù)庫(kù)和機(jī)制。具體查看org.apache.catalina.Realm接口。

基于JMX Bean的管理

JMX(Java Management Extensions) MBean是Java SE定義的技術(shù)規(guī)范,是一個(gè)為設(shè)備、應(yīng)用程序植入可管理功能的框架,通過(guò)JMX可以遠(yuǎn)程監(jiān)控Tomcat各個(gè)組件的運(yùn)行狀態(tài)。前面說(shuō)了Lifecycle接口,其實(shí)Tomcat的各個(gè)組件其實(shí)都是通過(guò)繼承LifecycleMBeanBase,也就是實(shí)現(xiàn)了LifecycleJmxEnabled接口的抽象類。

Session管理

負(fù)責(zé)創(chuàng)建和管理Session,以及Session的持久化處理,支持Session集群。

Cluster

Tomcat的集群,提供了Session,上下文attribute的復(fù)制和集群范圍內(nèi)的WAR文件部署,提供了較多的可配置選項(xiàng),可以對(duì)集群相關(guān)的問(wèn)題進(jìn)行較細(xì)粒度的控制。

Logging

使用Tomcat時(shí)內(nèi)部的日志記錄,這是一個(gè)打包重命名的Apache Commons Logging的分支,使用了java.util.loggin框架進(jìn)行硬編碼實(shí)現(xiàn)的,確保了Tomcat內(nèi)部日志記錄和其它任何Web應(yīng)用程序日志記錄框架保持獨(dú)立。

Naming

命名服務(wù),Tomcat提供了對(duì)Java中JNDI(Java Naming and Directory Interface)的支持,Java應(yīng)用程序使用此API來(lái)訪問(wèn)各種命名和目錄服務(wù),可以使用名字訪問(wèn)對(duì)象以及對(duì)象的資源。Tomcat中使用JNDI定義數(shù)據(jù)源、配置信息,用來(lái)實(shí)現(xiàn)開(kāi)發(fā)與部署的分離。

結(jié)尾

由于篇幅有限,很多東西沒(méi)有涉及,雖說(shuō)是講Tomcat,其實(shí)也還涉及到一些計(jì)算機(jī)網(wǎng)絡(luò)與操作系統(tǒng)相關(guān)的知識(shí),確實(shí)不管是這些基礎(chǔ)知識(shí),還是源碼的設(shè)計(jì)思想,才是真正需要花大量時(shí)間去學(xué)習(xí)的東西,畢竟編程語(yǔ)言,應(yīng)用層的東西只是方便我們用來(lái)干事情的工具,切記不要本末倒置,被工具所主導(dǎo),對(duì)這些通用的知識(shí)的反復(fù)思考,實(shí)踐,總結(jié)才是正確的道路,那些優(yōu)秀的開(kāi)源項(xiàng)目也是如此,沒(méi)有扎實(shí)的地基是不可能建立起高樓的,這篇博客就先到這里了。

參考

https://docs.oracle.com/javase/tutorial/jndi/index.html
http://tomcat.apache.org/tomcat-9.0-doc/index.html
https://time.geekbang.org/column/intro/180
https://juejin.im/post/58eb5fdda0bb9f00692a78fc
http://www.voidcn.com/article/p-cnfwakoo-bma.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容