自家表兄弟Tomcat和Jetty
- 2020 年 3 月 13 日
- 筆記
Jetty是Eclipse基金會的一個開源項目,是「HTTP伺服器 + Servlet容器」,並且Jetty和Tomcat在架構設計上有不少相似的地方,實在是像一對錶兄弟。
但同時Jetty也有自己的特點,主要是更加小巧,更易於訂製化。Jetty作為一名後起之秀,應用範圍也越來越廣,Jetty來作為Web容器已經在大量CNCF技術體系下發展的紅紅火火。
兩者都是符合Servlet標準,功能上幾乎一樣,但是由於誕生的時代背景不同,所以很多對標的其他解決方案也不同。
Tomcat對標的是Apache,Iis都是五大三粗的全面的web server。Jetty後來居上主要是也站在了Tomcat老大哥的肩膀上,少走了好多彎路。如果非要說兩者有什麼不同,可以認為是不同組織開發的,架構實現上有不同。
Jetty架構上相比於Tomcat更簡單,Jetty架構是基於Handler實現的,擴展也是基於Handler實現,擴展簡單,Tomcat面向的容器設計,如果擴展需要了解整個Tomcat架構,擴展上較為複雜。
Jetty可以同時處理大量連接,且可以保持長時間連接,適用於Web聊天室之類的應用,Tomcat適合處理少數且繁忙的連接,比如一些生命周期較短的,Tomcat有一定的性能優勢。
先看下Jetty的整體架構,Jetty Server就是由多個Connector(連接器)、多個Handler(處理器),以及一個執行緒池組成。

Jetty和Tomcat一樣實現了Servlet容器和Jsp的相應規範。Jetty中的Connector組件和Handler組件分別來實現Http伺服器和Servlet容器的功能,而這兩個組件工作時所需要的執行緒資源都直接從一個全局執行緒池ThreadPool中獲取。

Jetty Server可以有多個Connector在不同的埠上監聽客戶請求,而對於請求處理的Handler組件,也可以根據具體場景使用不同的Handler。
這樣的設計提高了Jetty的靈活性,需要支援Servlet,則可以使用ServletHandler;需要支援Session,則再增加一個SessionHandler。如果不使用Servlet或者Session,只要不配置這個Handler就行了。
Jetty提供了一個Server類創建並初始化Connector、Handler、ThreadPool組件,然後調用start方法啟動它們。
Tomcat和Jetty有什麼相同又有什麼不同呢?
1)第一個區別是Jetty中沒有Service的概念,Tomcat中的Service包裝了多個連接器和一個容器組件,一個Tomcat實例可以配置多個Service,不同的Service通過不同的連接器監聽不同的埠;而Jetty中Connector是被所有Handler共享的。
2)第二個區別是,在Tomcat中每個連接器都有自己的執行緒池,而在Jetty中所有的Connector共享一個全局的執行緒池。
Connector的主要功能是對I/O模型和應用層協議的封裝。I/O模型方面Jetty 9版本只支援NIO,因此Jetty的Connector設計有明顯的Java NIO通訊模型的痕迹。應用層協議方面,跟Tomcat的Processor一樣,Jetty抽象出了Connection組件來封裝應用層協議的差異。
Java NIO的核心組件是Channel、Buffer和Selector。
Channel表示一個連接,可以理解為一個Socket,通過它可以讀取和寫入數據,但是並不能直接操作數據,需要通過Buffer來中轉。
Selector可以用來檢測Channel上的I/O事件,比如讀就緒、寫就緒、連接就緒,一個Selector可以同時處理多個Channel,因此單個執行緒可以監聽多個Channel,這樣會大量減少執行緒上下文切換的開銷。
典型的NIO程式實現邏輯如下:
1.創建服務端Channel,綁定監聽埠並把Channel設置為非阻塞方式。

2.創建Selector,並在Selector中註冊Channel感興趣的事件OP_ACCEPT,告訴Selector如果客戶端有新的連接請求到這個埠就通知我。

3.Selector會在一個死循環里不斷地調用select()去查詢I/O狀態,select()會返回一個SelectionKey列表,Selector會遍歷這個列表,看看是否有「客戶」感興趣的事件,如果有,就採取相應的動作。

上面的例子說明,如果有新的連接請求,就會建立一個新的連接。連接建立後,再註冊Channel的可讀事件到Selector中,告訴Selector我對這個Channel上是否有新的數據到達感興趣。
服務端在I/O通訊上主要完成了三件事情:監聽連接、I/O事件查詢以及數據讀寫。Jetty設計了Acceptor、SelectorManager和Connection來分別做這三件事情,下面我分別來說說這三個組件。
Acceptor用於接受請求,跟Tomcat一樣,Jetty也有獨立的Acceptor執行緒組用於處理連接請求。在Connector的實現類ServerConnector中,有一個_acceptors
的數組,在Connector啟動的時候, 會根據_acceptors
數組的長度創建對應數量的Acceptor,而Acceptor的個數可以配置。

Acceptor是ServerConnector中的一個內部類,同時也是一個Runnable,Acceptor執行緒是通過getExecutor()得到的執行緒池來執行的,前面提到這是一個全局的執行緒池。
Acceptor通過阻塞的方式來接受連接,這一點跟Tomcat也是一樣的。

接受連接成功後會調用accepted()函數,accepted()函數中會將SocketChannel設置為非阻塞模式,然後交給Selector去處理,因此這也就到了Selector的地界了。

Jetty的Selector由SelectorManager類管理,而被管理的Selector叫作ManagedSelector。SelectorManager內部有一個ManagedSelector數組,真正幹活的是ManagedSelector。接著上面分析,看看在SelectorManager在accept方法里做了什麼。

SelectorManager從本身的Selector數組中選擇一個Selector來處理這個Channel,並創建一個任務Accept交給ManagedSelector,ManagedSelector在處理這個任務主要做了兩步:
第一步,調用Selector的register方法把Channel註冊到Selector上,拿到一個SelectionKey。

第二步,創建一個EndPoint和Connection,並跟這個SelectionKey(Channel)綁在一起:

上面這兩個過程是什麼意思呢?打個比方,你到餐廳吃飯,先點菜(註冊I/O事件),服務員(ManagedSelector)給你一個單子(SelectionKey),等菜做好了(I/O事件到了),服務員根據單子就知道是哪桌點了這個菜,於是喊一嗓子某某桌的菜做好了(調用了綁定在SelectionKey上的EndPoint的方法)。
這裡需要你特別注意的是,ManagedSelector並沒有調用直接EndPoint的方法去處理數據,而是通過調用EndPoint的方法返回一個Runnable,然後把這個Runnable扔給執行緒池執行,所以你能猜到,這個Runnable才會去真正讀數據和處理請求。
Connection這個Runnable是EndPoint的一個內部類,它會調用Connection的回調方法來處理請求。Jetty的Connection組件類比就是Tomcat的Processor,負責具體協議的解析,得到Request對象,並調用Handler容器進行處理。下面簡單介紹一下它的具體實現類HttpConnection對請求和響應的處理過程。
請求處理:HttpConnection並不會主動向EndPoint讀取數據,而是向在EndPoint中註冊一堆回調方法:

告訴EndPoint,數據到了你就調我這些回調方法_readCallback吧,有點非同步I/O的感覺,也就是說Jetty在應用層面模擬了非同步I/O模型。
而在回調方法_readCallback里,會調用EndPoint的介面去讀數據,讀完後讓HTTP解析器去解析位元組流,HTTP解析器會將解析後的數據,包括請求行、請求頭相關資訊存到Request對象里。
響應處理:Connection調用Handler進行業務處理,Handler會通過Response對象來操作響應流,向流裡面寫入數據,HttpConnection再通過EndPoint把數據寫到Channel,這樣一次響應就完成了。
再來回顧一下Connector的工作流程。

- step1.Acceptor監聽連接請求,當有連接請求到達時就接受連接,一個連接對應一個Channel,Acceptor將Channel交給ManagedSelector來處理。
- step2.ManagedSelector把Channel註冊到Selector上,並創建一個EndPoint和Connection跟這個Channel綁定,接著就不斷地檢測I/O事件。
- step3.I/O事件到了就調用EndPoint的方法拿到一個Runnable,並扔給執行緒池執行。
- step4.執行緒池中調度某個執行緒執行Runnable。
- step5.Runnable執行時,調用回調函數,這個回調函數是Connection註冊到EndPoint中的。
- step6.回調函數內部實現,其實就是調用EndPoint的介面方法來讀數據。
- step7.Connection解析讀到的數據,生成請求對象並交給Handler組件去處理。
Jetty Server就是由多個Connector、多個Handler,以及一個執行緒池組成,在設計上簡潔明了。
Jetty的Connector只支援NIO模型,跟Tomcat的NioEndpoint組件一樣,它也是通過Java的NIO API實現的。Java NIO編程有三個關鍵組件:Channel、Buffer和Selector,而核心是Selector。為了方便使用,Jetty在原生Selector組件的基礎上做了一些封裝,實現了ManagedSelector組件。
在執行緒模型設計上Tomcat的NioEndpoint跟Jetty的Connector是相似的,都是用一個Acceptor數組監聽連接,用一個Selector數組偵測I/O事件,用一個執行緒池執行請求。它們的不同點在於,Jetty使用了一個全局的執行緒池,所有的執行緒資源都是從執行緒池來分配。
Jetty Connector設計中的一大特點是,使用了回調函數來模擬非同步I/O,比如Connection向EndPoint註冊了一堆回調函數。它的本質將函數當作一個參數來傳遞,告訴對方,你準備好了就調這個回調函數。
Tomcat和Jetty相比,Jetty的I/O執行緒模型更像Netty,Jetty的EatWhatYouKill執行緒策略,其實就是Netty 4.0中的執行緒模型。
Ask:
- Jetty的Connector主要完成了三件事件:接收連接、I/O事件查詢以及數據讀寫。因此Jetty設計了Acceptor、SelectorManager和Connection來做這三件事情。為什麼要把這些組件跑在不同的執行緒里呢?
Ans:
- 反過來想,如果等待連接到達,接收連接、等待數據到達、數據讀取和請求處理(等待應用處理完)都在一個執行緒里,這中間執行緒可能大部分時間都在」等待「,沒有幹活,而執行緒資源是很寶貴的。並且執行緒阻塞會發生執行緒上下文切換,浪費CPU資源。
Connector會將Servlet請求交給Handler去處理,Jetty的Handler在設計上非常有意思,可以說是Jetty的靈魂,Jetty通過Handler實現了高度可訂製化。
Handler就是一個介面,它有一堆實現類,Jetty的Connector組件調用這些介面來處理Servlet請求,先來看看這個介面定義成什麼樣子。

Handler介面的定義非常簡潔,主要就是用handle方法用來處理請求,跟Tomcat容器組件的service方法一樣,它有ServletRequest和ServeletResponse兩個參數。除此之外,這個介面中還有setServer和getServer方法,因為任何一個Handler都需要關聯一個Server組件,也就是說Handler需要被Server組件來管理。一般來說Handler會載入一些資源到記憶體,因此通過設置destroy方法來銷毀。
Handler只是一個介面,完成具體功能的還是它的子類。那麼Handler有哪些子類呢?它們的繼承關係又是怎樣的?這些子類是如何實現Servlet容器功能的呢?
Jetty中定義了一些默認Handler類,並且這些Handler類之間的繼承關係比較複雜,先通過一個全景圖來了解一下。對類圖進行了簡化。

從圖可以看到,Handler的種類和層次關係還是比較複雜的:
Handler介面之下有抽象類AbstractHandler,這一點並不意外,因為有介面一般就有抽象實現類。
在AbstractHandler之下有AbstractHandlerContainer,為什麼需要這個類呢?這其實是個過渡,為了實現鏈式調用,一個Handler內部必然要有其他Handler的引用,所以這個類的名字里才有Container,意思就是這樣的Handler里包含了其他Handler的引用。
理解了上面的AbstractHandlerContainer,就能理解它的兩個子類了:HandlerWrapper和HandlerCollection。簡單來說就是,HandlerWrapper和HandlerCollection都是Handler,但是這些Handler里還包括其他Handler的引用。不同的是,HandlerWrapper只包含一個其他Handler的引用,而HandlerCollection中有一個Handler數組的引用。

接著來看左邊的HandlerWrapper,它有兩個子類:Server和ScopedHandler。Server比較好理解,它本身是Handler模組的入口,必然要將請求傳遞給其他Handler來處理,為了觸發其他Handler的調用,所以它是一個HandlerWrapper。
再看ScopedHandler,它也是一個比較重要的Handler,實現了「具有上下文資訊」的責任鏈調用。為什麼要強調「具有上下文資訊」呢?那是因為Servlet規範規定Servlet在執行過程中是有上下文的。那麼這些Handler在執行過程中如何訪問這個上下文呢?這個上下文又存在什麼地方呢?答案就是通過ScopedHandler來實現的。
而ScopedHandler有一堆的子類,這些子類就是用來實現Servlet規範的,比如ServletHandler、ContextHandler、SessionHandler、ServletContextHandler和WebAppContext。
請看類圖的右邊,跟HandlerWapper對等的還有HandlerCollection,HandlerCollection其實維護了一個Handler數組。為什麼要發明一個這樣的Handler?這是因為Jetty可能需要同時支援多個Web應用,如果每個Web應用有一個Handler入口,那麼多個Web應用的Handler就成了一個數組,比如Server中就有一個HandlerCollection,Server會根據用戶請求的URL從數組中選取相應的Handler來處理,就是選擇特定的Web應用來處理請求。
雖然從類圖上看Handler有很多,但是本質上這些Handler分成三種類型:
- 第一種是協調Handler,這種Handler負責將請求路由到一組Handler中去,比如上圖中的HandlerCollection,它內部持有一個Handler數組,當請求到來時,它負責將請求轉發到數組中的某一個Handler。
- 第二種是過濾器Handler,這種Handler自己會處理請求,處理完了後再把請求轉發到下一個Handler,比如圖上的HandlerWrapper,它內部持有下一個Handler的引用。需要注意的是,所有繼承了HandlerWrapper的Handler都具有了過濾器Handler的特徵,比如ContextHandler、SessionHandler和WebAppContext等。
- 第三種是內容Handler,說白了就是這些Handler會真正調用Servlet來處理請求,生成響應的內容,比如ServletHandler。如果瀏覽器請求的是一個靜態資源,也有相應的ResourceHandler來處理這個請求,返回靜態頁面。
ServletHandler、ContextHandler以及WebAppContext等,它們實現了Servlet規範,那具體是怎麼實現的呢?先來看看如何使用Jetty來啟動一個Web應用。

上面的過程主要分為兩步:
第一步創建一個WebAppContext,接著設置一些參數到這個Handler中,就是告訴WebAppContextWAR包放在哪,Web應用的訪問路徑是什麼。
第二步就是把新創建的WebAppContext添加到Server中,然後啟動Server。
WebAppContext對應一個Web應用。Servlet規範中有Context、Servlet、Filter、Listener和Session等,Jetty要支援Servlet規範,就需要有相應的Handler來分別實現這些功能。因此,Jetty設計了3個組件:ContextHandler、ServletHandler和SessionHandler來實現Servle規範中規定的功能,而WebAppContext本身就是一個ContextHandler,另外它還負責管理ServletHandler和SessionHandler。
再來看一下什麼是ContextHandler。ContextHandler會創建並初始化Servlet規範里的ServletContext對象,同時ContextHandler還包含了一組能夠讓Web應用運行起來的Handler,可以這樣理解,Context本身也是一種Handler,它裡面包含了其他的Handler,這些Handler能處理某個特定URL下的請求。比如,ContextHandler包含了一個或者多個ServletHandler。
再來看ServletHandler,它實現了Servlet規範中的Servlet、Filter和Listener的功能。ServletHandler依賴FilterHolder、ServletHolder、ServletMapping、FilterMapping這四大組件。FilterHolder和ServletHolder分別是Filter和Servlet的包裝類,每一個Servlet與路徑的映射會被封裝成ServletMapping,而Filter與攔截URL的映射會被封裝成FilterMapping。
SessionHandler從名字就知道它的功能,用來管理Session。除此之外WebAppContext還有一些通用功能的Handler,比如SecurityHandler和GzipHandler,同樣從名字可以知道這些Handler的功能分別是安全控制和壓縮/解壓縮。
WebAppContext會將這些Handler構建成一個執行鏈,通過這個鏈會最終調用到業務Servlet。通過一張圖來理解一下。

通過對比Tomcat的架構圖,Jetty的Handler組件和Tomcat中的容器組件是大致是對等的概念,Jetty中的WebAppContext相當於Tomcat的Context組件,都是對應一個Web應用;而Jetty中的ServletHandler對應Tomcat中的Wrapper組件,它負責初始化和調用Servlet,並實現了Filter的功能。
對於一些通用組件,比如安全和解壓縮,在Jetty中都被做成了Handler,這是Jetty Handler架構的特點。
因此對於Jetty來說,請求處理模組就被抽象成Handler,不管是實現了Servlet規範的Handler,還是實現通用功能的Handler,比如安全、解壓縮等,可以任意添加或者裁剪這些「功能模組」,從而實現高度的可訂製化。
Jetty Server就是由多個Connector、多個Handler,以及一個執行緒池組成。
Jetty的Handler設計是它的一大特色,Jetty本質就是一個Handler管理器,Jetty本身就提供了一些默認Handler來實現Servlet容器的功能,你也可以定義自己的Handler來添加到Jetty中,這體現了「微內核 + 插件」的設計思想。