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

前言

早在之前寫過一些http玩具服務(wù)器似忧,總感覺無法繼續(xù)前進(jìn)了项钮,期間花了比較多的時間在基礎(chǔ)知識上班眯,前段時間想著直接從用的比較多的服務(wù)器開始希停,對于Java開發(fā)者來說烁巫,自然Tomcat是首選,但有一個比較大的問題是經(jīng)過了近20年的發(fā)展宠能,它已經(jīng)成為了一個十分系統(tǒng)亚隙、復(fù)雜的框架了,讀起來肯定容易陷入泥潭违崇,回想起之前學(xué)其它原理一樣阿弃,一開始就看了比較復(fù)雜的一張架構(gòu)圖诊霹,然后就沒有然后了...,不過還好有《how tomcat works》這種神書渣淳,雖說是講的Tomcat4,5版本的脾还,但關(guān)鍵在于從實際問題出發(fā)描述,尋求解決方式入愧,一步步的將其從百來行代碼的玩具構(gòu)建成了一個功能完整的鄙漏、有著較強(qiáng)擴(kuò)展性的框架。雖然到現(xiàn)在的Tomcat9版本有較大的變化棺蛛,但核心還是沒變怔蚌,了解了早期版本的源碼之后再看現(xiàn)在的版本就不會像無頭蒼蠅一樣亂撞,從一條線出發(fā)旁赊,清楚整個流程桦踊,學(xué)習(xí)設(shè)計,先不管太多細(xì)節(jié)性的問題终畅,這也是我看了一些技術(shù)書籍和文章之后的體會籍胯,發(fā)現(xiàn)很多都是只談概念,不講這樣做的原因声离,就很難讓讀者帶入自己的思考芒炼、融入書本去讀,自然難以閱讀下去术徊,也容易理解不夠深本刽,忘記得也就更加快。用這篇博客主要來解析一下Tomcat的架構(gòu)赠涮,因為Tomcat涉及到的功能模塊比較多子寓,這里只從它的核心功能出發(fā),附加的組件只簡單介紹一下笋除。本來打算直接寫一篇從源碼開始的斜友,最后寫一個簡單的Tomcat的,寫的過程中發(fā)現(xiàn)很多東西都要去解釋垃它,而且難以將前后串起來鲜屏,所以打算把它拆解成架構(gòu)篇,源碼篇国拇,實踐篇一共三篇洛史,基于Tomcat9,這里第一篇主要介紹Tomcat的核心組件酱吝,以及整體的架構(gòu)和運(yùn)作方式也殖,不會涉及過多的源碼,現(xiàn)在開始正文务热。

Tomcat總體架構(gòu)

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

整體架構(gòu)

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

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

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

連接器

連接器(connector) 內(nèi)部持有一個實現(xiàn)了ProtocolHandler接口的對象胳嘲,來看看這個接口具體的實現(xiàn)類的類圖

ProtocolHandler

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


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

EndPoint

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

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

來說說為什么需要這兩個過程,因為網(wǎng)絡(luò)傳輸是基本上都是基于TCP/UDP協(xié)議的渔扎,特別是對于TCP而言夷都,接收到數(shù)據(jù)后還需要發(fā)送相對應(yīng)的ack包倡蝙,表示接收方已經(jīng)接收到數(shù)據(jù)了隘庄,包的傳輸過程不穩(wěn)定踢步,可能會受到各種因素的影響,所以要提高數(shù)據(jù)傳輸?shù)男实脑挸蟛簦捅M量減少網(wǎng)絡(luò)數(shù)據(jù)包的往返的次數(shù)获印,就盡可能的多接收一點數(shù)據(jù)后再進(jìn)行應(yīng)答;同樣街州,寫數(shù)據(jù)也是兼丰,盡量要讓緩沖區(qū)有較多的數(shù)據(jù)后再真正讓網(wǎng)卡進(jìn)行發(fā)送。

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

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

Nio

同步非阻塞IO,具體讀數(shù)據(jù)時還是同步的方式历极,也就是指數(shù)據(jù)從內(nèi)核空間拷貝用戶空間的這段時間一直是阻塞的窄瘟,等數(shù)據(jù)到了用戶空間再將用戶線程喚醒。對于傳統(tǒng)的BIO(同步阻塞)而言趟卸,不管是連接的建立蹄葱,數(shù)據(jù)讀寫的就緒以及數(shù)據(jù)從網(wǎng)卡到內(nèi)核空間氏义,從內(nèi)核空間拷貝到用戶空間都是阻塞的,所以必須用新的線程管理著具體的Socket連接图云,而對于Java來說惯悠,線程是直接映射到操作系統(tǒng)內(nèi)核的線程,所以資源是比較重量級并且是十分有限的的琼稻,而大部分時間線程又在等待,并且線程數(shù)過多會造成大量的線程上下文切換饶囚,為了解決這個問題才產(chǎn)生的新的IO模型帕翻;對于一個連接通道而言阻塞不阻塞其實沒差別,沒數(shù)據(jù)的話萝风,不阻塞因為要保證數(shù)據(jù)同步嘀掸,也要不停的對一個連接做空輪詢,白白消耗CPU時鐘周期规惰,并且也沒辦法做其它的事了睬塌,所以一般具體實現(xiàn)時現(xiàn)時是使用了單獨的Selector(多路復(fù)用器)來管理多個連接,去輪詢多個通道(連接)是否有事件就緒歇万,也就是內(nèi)核已經(jīng)接收到了數(shù)據(jù)并完成了包的拆解揩晴,然后把有就緒事件的通道交給專門進(jìn)行讀寫任務(wù)的線程池來處理,這樣也不會影響到其它有事件就緒的通道贪磺,具體的實現(xiàn)是依賴操作系統(tǒng)底層的epoll(Linux)或iocp(Windows)機(jī)制硫兰,這種IO模型能只用少量的線程管理就能大量的數(shù)據(jù)通道(雖然是同步,但由于CPU將數(shù)據(jù)進(jìn)行拷貝時的速度太快了寒锚,而大多數(shù)情況下數(shù)據(jù)包都比較小劫映,所以在應(yīng)用層也基本無感知),也是目前使用較多的IO模型刹前。

Nio2

AIO泳赋,異步非阻塞IO,就連數(shù)據(jù)的讀寫也無需等待喇喉,只要設(shè)置一個實現(xiàn)回調(diào)的接口的對象祖今,就能在數(shù)據(jù)收發(fā)完成后主動進(jìn)行通知需要回調(diào)的對象,AIO是在后面出來的拣技,一般場景下同步非阻塞IO已經(jīng)完全夠用了衅鹿,在數(shù)據(jù)包量較大時使用AIO就能擁有更好的性能。

Apr

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

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

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

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

首先從JVM談起枪孩,Java對象的實例化憔晒、數(shù)組等,都是JVM給我們在Java堆里面分配的空間蔑舞,而JVM本身其實也只是一個進(jìn)程拒担,所以JVM內(nèi)存也只是進(jìn)程空間的一部分,整個進(jìn)程空間內(nèi)攻询,JVM之外的部分叫本地內(nèi)存澎蛛,看看下面的圖

JVM進(jìn)程

Tomcat的EndPoint組件在接收網(wǎng)絡(luò)數(shù)據(jù)時需要提前分配一個字節(jié)數(shù)組,Java通過JNI調(diào)用將字節(jié)數(shù)組的內(nèi)存地址傳給C代碼蜕窿,C代碼通過操作系統(tǒng)的API讀取Socket谋逻,并把數(shù)據(jù)填充到這個字節(jié)數(shù)組。Java NIO提供了兩種方式來分配字節(jié)數(shù)組: HeapByteBuffer 和 DirectedByteBuffer桐经,對應(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ù)據(jù)拷貝到臨時的本地內(nèi)存茎芭, 然后再從本地內(nèi)存拷貝到Java堆,使用本地內(nèi)存來進(jìn)行中轉(zhuǎn)的目的是為了防止直接從內(nèi)核拷貝到JVM堆時發(fā)生GC誓沸,分配的字節(jié)數(shù)組可能會進(jìn)行移動梅桩,這樣之前的地址空間就會失效,而從本地內(nèi)存拷貝時不滿足JVM可安全的進(jìn)行垃圾回收的條件拜隧,所以不會觸發(fā)GC宿百;使用DirectByteBuffer的方式趁仙,它持有的接收數(shù)據(jù)的字節(jié)數(shù)組所需的內(nèi)存是在本地內(nèi)存進(jìn)行分配的,而這部分內(nèi)存不是由JVM進(jìn)行管理的垦页,在Java堆里面的該對象實例雀费,僅僅保存了該對象所持有的字節(jié)數(shù)組的地址,在真正進(jìn)行數(shù)據(jù)收發(fā)時是把這個內(nèi)存地址傳遞給C代碼痊焊,然后進(jìn)行后續(xù)處理盏袄,用這種方式減少了JVM堆與本地內(nèi)存之間的數(shù)據(jù)拷貝。但由于這部分內(nèi)存不是被JVM所管理薄啥,發(fā)生內(nèi)存泄漏時難以定位辕羽,所以Tomcat的NioEndPoint和Nio2EndPoint都使用了第一種方式,而AprEndPoint使用了第二種方式罪佳,反正具體的管理是交給Apr的C代碼去做的逛漫。實際上很多的網(wǎng)絡(luò)編程框架都使用了第二種方式黑低,比如Netty赘艳,由于本地內(nèi)存不方便JVM進(jìn)行內(nèi)存管理,它就使用了本地內(nèi)存池的方式克握。

3.sendfile

除了前面所說的使用DirectByteBuffer來進(jìn)行優(yōu)化外蕾管,APR在文件發(fā)送的場景也做了比較好的優(yōu)化,使用傳統(tǒng)的方式菩暗,在發(fā)送文件時掰曾,如果使用HeapByteBuffer的方式,首先通過系統(tǒng)調(diào)用需要先將文件讀到內(nèi)核緩沖區(qū)停团,然后再將數(shù)據(jù)拷貝到Java應(yīng)用程序的本地內(nèi)存緩沖區(qū)旷坦,最后再拷貝到JVM堆,此時才可以調(diào)用Socket真正進(jìn)行數(shù)據(jù)的寫佑稠,寫的時候同樣也要先寫到本地內(nèi)存緩沖區(qū)秒梅,然后再拷貝到內(nèi)核的緩沖區(qū),最后再使用網(wǎng)卡發(fā)送具體的數(shù)據(jù)舌胶;而使用APR的方式捆蜀,數(shù)據(jù)不需要拷貝到JVM進(jìn)程相關(guān)的緩沖區(qū),只需要把記錄數(shù)據(jù)位置和長度等相關(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)的方式扬霜,使得其具有了極大的靈活性定鸟,因為這些組件完全可以根據(jù)我們自己的需求去進(jìn)行添加實現(xiàn),又可以直接復(fù)用已有的組件著瓶。Engine表示引擎联予,可以用來管理多個虛擬主機(jī),Host表示虛擬主機(jī)材原,可以管理多個WEB應(yīng)用, Context表示W(wǎng)EB應(yīng)用沸久,可以管理多個Wrapper,Wrapper實際上是對Servlet的封裝余蟹。Container整個組件的通信卷胯,采用了責(zé)任鏈模式,每個組件里面都有一個pipeline用來存放Valve威酒,Valve就是實際請求通過時需要經(jīng)過的節(jié)點, 每層組件pipeline的尾節(jié)點是一個BasicValve, 這也是必須要擁有的一個節(jié)點窑睁,該節(jié)點是直接在當(dāng)前層級的組件對象實例化時在構(gòu)造方法里面進(jìn)行添加的,作用是用來與下一層組件通信,當(dāng)下一層的子組件有多個時葵孤,就需要在BasicValve節(jié)點建立映射担钮,然后就找到下層組件持有的pipeline的第一個valve,以此類推尤仍,直到請求正確的找到最終需要處理它的Servlet箫津。直接按名字來進(jìn)行理解也是非常形象的,pipeline表示管道宰啦,(這里直接把它當(dāng)成入口是開著的苏遥,出口是用一個Basic閥門關(guān)著的),valve表示閥門赡模,每個管道是隸屬于每個單獨的組件的田炭,要讓這些組件連接起來,就直接把出口的閥門對接到下層組件的管道的入口纺裁,數(shù)據(jù)流通時才去開啟閥門诫肠。也就是采用pipeline和valve這么一種方式,可以在任何我們感興趣的地方進(jìn)行攔截欺缘,也就是往管道的任何地方都可以插入一道新的閥門栋豫,Tomcat內(nèi)部的很多擴(kuò)展的插件就是這樣實現(xiàn)的,比如配置不同的訪問認(rèn)證方式谚殊、session管理丧鸯、訪問日志、錯誤記錄嫩絮、SSL/TLS認(rèn)證等丛肢。

JSP文件的解析與處理

還有需要清楚的一點是围肥,對于JSP文件的處理,其實就是使用了上圖的Jasper模塊蜂怎,不過這個模塊不是Tomcat本身自帶的穆刻。 Tomcat有多種啟動方式,為了方便說明杠步,以內(nèi)嵌式(SpringBoot就是使用的這種方式)啟動為例氢伟,對應(yīng)的是org.apache.catalina.startup.Tomcat類,SpringBoot會首先實例化這個對象幽歼,默認(rèn)的web.xml文件沒有找到的情況下朵锣,會去調(diào)用這么一個方法

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");
    }
  .....

邏輯很清楚,它會個Context添加兩個默認(rèn)的Servlet甸私,也就是DefaultServlet和JspServlet诚些,然后通過addServlet()方法,這個方法會把Servlet包裝成一個Wrapper皇型,然后添加到Context的子容器中诬烹,并進(jìn)行相應(yīng)的映射,這樣當(dāng)有JSP頁面的請求到來時犀被,在Context的pipeline的BasicValve里面會直接拿到請求對象request對應(yīng)的Wrapper椅您,現(xiàn)在關(guān)鍵需要知道的是request是怎么和wrapper對應(yīng)起來的外冀,request內(nèi)部有一個MappingData對象實例寡键,該實例里面持有一個Wrapper對象,其實這個對象是連接器(connector)把請求給容器(container)處理之前雪隧,也就是把request對象交給Adapter處理時西轩,如果是jsp則根據(jù)初始化時建立的映射使用的子容器Wrapper就是包裝了JspServlet的這個,把這個直接賦值給MappingData的Wrapper即可脑沿,這個JspServlet會在內(nèi)部把jsp文件進(jìn)行解析處理藕畔,編譯成繼承了HttpJspBase的類,而HttpJspBase又是繼承了HttpServlet類庄拇,最后實例化這個類進(jìn)行最終的處理注服。

兩大核心組件總覽

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


tomcat核心

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

生命周期

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

Lifecycle

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

輔助模塊

除了前文提到的电媳,Tomcat輔助模塊還有一些踏揣,這里簡單的列舉一下。

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

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

基于JMX Bean的管理

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

Session管理

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

Cluster

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

Logging

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

Naming

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

結(jié)尾

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

參考

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)容合作請聯(lián)系作者
  • 序言:七十年代末撬碟,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子莉撇,更是在濱河造成了極大的恐慌呢蛤,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件棍郎,死亡現(xiàn)場離奇詭異其障,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)涂佃,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門励翼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人辜荠,你說我怎么就攤上這事汽抚。” “怎么了伯病?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵造烁,是天一觀的道長。 經(jīng)常有香客問我,道長惭蟋,這世上最難降的妖魔是什么叠纹? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮敞葛,結(jié)果婚禮上誉察,老公的妹妹穿的比我還像新娘。我一直安慰自己惹谐,他們只是感情好持偏,可當(dāng)我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著氨肌,像睡著了一般鸿秆。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上怎囚,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天卿叽,我揣著相機(jī)與錄音,去河邊找鬼恳守。 笑死考婴,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的催烘。 我是一名探鬼主播沥阱,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼伊群!你這毒婦竟也來了考杉?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤舰始,失蹤者是張志新(化名)和其女友劉穎崇棠,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體丸卷,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡枕稀,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了及老。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片抽莱。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖骄恶,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情匕垫,我是刑警寧澤僧鲁,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響寞秃,放射性物質(zhì)發(fā)生泄漏斟叼。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一春寿、第九天 我趴在偏房一處隱蔽的房頂上張望朗涩。 院中可真熱鬧,春花似錦绑改、人聲如沸谢床。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽识腿。三九已至,卻和暖如春造壮,著一層夾襖步出監(jiān)牢的瞬間渡讼,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工耳璧, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留成箫,地道東北人。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓旨枯,卻偏偏與公主長得像伟众,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子召廷,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,916評論 2 344

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