前言
早在之前寫過一些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)
可以看到 頂層是一個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)類的類圖
根據(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渴邦,看下類圖,
基本是與上面對應(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)存澎蛛,看看下面的圖
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四部分組成
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)的流程
由上圖可知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á)時暮现,被觀察者就可以通知到所有觀察者还绘。接下來看看類圖
可以清楚的看到,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)了Lifecycle和JmxEnabled接口的抽象類犬第。
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