Jetty基本介紹 及 與tomcat對比

一、Jetty目錄剖析

bin:可執行腳本文件 demo- base: etc:Jetty模塊定義的XML配置文件的目錄 lib:Jetty依賴的庫文件 logs:Jetty的日誌目錄 modules:Jetty的模塊 resources:外部資源配置文件的目錄 webapps:項目WAR文件的目錄還需要關心根目錄下的一個文件:start.d(Wondows系統是start.ini文件),它定義了Jetty的活動模塊。

二、基本配置

1、修改Jetty的端口

Jetty默認使用8080端口,要讓它使用其他端口(如7070),那麼編輯start.d(Wondows系統是start.ini文件),找到jetty.http.port行,修改爲:

## Connector port to listen on jetty.http.port=7070

保存並退出,再重啓Jetty。

2、修改webapps目錄

Jetty下的webapps是默認的Web項目的部署目錄,如果想修改此目錄,可修改start.d配置文件(start.ini),移除以下行的註釋符號“#”

# jetty.deploy.monitoredDir=webapps

並把內容修改到你指定的目錄。保存並退出,再重啓Jetty。

三、Jetty的模塊化架構

Jetty運行於模塊化的架構之上,這意味着Jetty的功能是以模塊的方式運行的,比如HTTP、HTTPS、SSL、日誌logging、JMX、JNDI、WebSocket等模塊。常用的模塊如HTTP、JSP和WebSocket模塊都是默認就激活的,而其他如HTTPS、JMX等模塊則需要手動激活。

1、單個模塊的剖析

Jetty的modules子目錄列出了所有的模塊,這些模塊是擴展名爲.mod的文件,它聲明瞭要被激活的JAR文件(在Jetty的lib子目錄下)和XML配置文件(在Jetty的etc子目錄下),以及其他要作爲模塊被激活的資源。比如,可以查看modules子目錄的logging.mod文件的內容,可以看到,它聲明瞭配置文件是etc/jetty-logging.xml,所需的JAR包在lib/logging處,另外logs目錄是必須的。

[ xml]
etc/jetty-logging.xml
[files]
logs/
[lib]
lib/logging/**.jar
resources/

2、通過命令行激活模塊

激活Jetty的模塊有兩種方式。第一種方式是通過命令行激活:

java -jar start.jar --add-to-startd=logging

上面的命令會在Jetty目錄下創建logging.ini文件,相關的配置可以在此文件中查到。配置日誌後,可以再次啓動Jetty,並可以查看到日誌模塊是激活了的。

3、通過配置文件start.ini激活模塊

第二種方式是通過配置文件start.ini激活模塊

--module=logging

這種方式和前一種相似,且更常用。

4、配置模塊

正如上面提到的,mod文件聲明瞭相關的XML配置文件,在Jetty的etc子目錄下,可以通過這些配置文件來配置模塊。比如日誌模塊聲明瞭相關的配置文件是jetty-logging.xml,可以通過修改此配置文件來調整日誌。

四、接受請求

Jetty 作爲一個獨立的 Servlet 引擎可以獨立提供 Web 服務,但是它也可以與其他 Web 應用服務器集成,所以它可以提供基於兩種協議工作,一個是 HTTP,一個是 AJP 協議。如果將 Jetty 集成到 Jboss 或者 Apache,那麼就可以讓 Jetty 基於 AJP 模式工作。下面分別介紹 Jetty 如何基於這兩種協議工作,並且它們如何建立連接和接受請求的。

1、基於HTTP

如果前端沒有其它 web 服務器,那麼 Jetty 應該是基於 HTTP 協議工作。也就是當 Jetty 接收到一個請求時,必須要按照 HTTP 協議解析請求和封裝返回的數據。那麼 Jetty 是如何接受一個連接又如何處理這個連接呢?

我們設置 Jetty 的 Connector 實現類爲

org.eclipse.jetty.server.bi.SocketConnector 讓 Jetty 以 BIO 的方式工作,Jetty 在啓動時將會創建 BIO 的工作環境,它會創建 HttpConnection 類用來解析和封裝 HTTP1.1 的協議,ConnectorEndPoint 類是以 BIO 的處理方式處理連接請求,ServerSocket 是建立 socket 連接接受和傳送數據,Executor 是處理連接的線程池,它負責處理每一個請求隊列中任務。acceptorThread 是監聽連接請求,一有 socket 連接,它將進入下面的處理流程。

當 socket 被真正執行時,HttpConnection 將被調用,這裏定義瞭如何將請求傳遞到 servlet 容器裏,有如何將請求最終路由到目的 servlet,關於這個細節可以參考《 servlet 工作原理解析》一文。 下圖是 Jetty 啓動創建建立連接的時序圖:

image.png

Jetty 創建接受連接環境需要三個步驟:

  1. 創建一個隊列線程池,用於處理每個建立連接產生的任務,這個線程池可以由用戶來指定,這個和 Tomcat 是類似的。
  2. 創建 ServerSocket,用於準備接受客戶端的 socket 請求,以及客戶端用來包裝這個 socket 的一些輔助類。
  3. 創建一個或多個監聽線程,用來監聽訪問端口是否有連接進來。 相比 Tomcat 創建建立連接的環境,Jetty 的邏輯更加簡單,牽涉到的類更少,執行的代碼量也更少了。

當建立連接的環境已經準備好了,就可以接受 HTTP 請求了,當 Acceptor 接受到 socket 連接後將轉入下圖所示流程執行:

image.png

Accetptor 線程將會爲這個請求創建 ConnectorEndPoint。HttpConnection 用來表示這個連接是一個 HTTP 協議的連接,它會創建 HttpParse 類解析 HTTP 協議,並且會創建符合 HTTP 協議的 Request 和 Response 對象。接下去就是將這個線程交給隊列線程池去執行了。

2、基於AJP

通常一個 web 服務站點的後端服務器不是將 Java 的應用服務器直接暴露給服務訪問者,而是在應用服務器,如 Jboss 的前面在加一個 web 服務器,如 Apache 或者 nginx,我想這個原因大家應該很容易理解,如做日誌分析、負載均衡、權限控制、防止惡意請求以及靜態資源預加載等等。

下圖是通常的 web 服務端的架構圖:

image.png

這種架構下 servlet 引擎就不需要解析和封裝返回的 HTTP 協議,因爲 HTTP 協議的解析工作已經在 Apache 或 Nginx 服務器上完成了,Jboss 只要基於更加簡單的 AJP 協議工作就行了,這樣能加快請求的響應速度。

對比 HTTP 協議的時序圖可以發現,它們的邏輯幾乎是相同的,不同的是替換了一個類 Ajp13Parserer 而不是 HttpParser,它定義瞭如何處理 AJP 協議以及需要哪些類來配合。

實際上在 AJP 處理請求相比較 HTTP 時唯一的不同就是在讀取到 socket 數據包時,如何來轉換這個數據包,是按照 HTTP 協議的包格式來解析就是 HttpParser,按照 AJP 協議來解析就是 Ajp13Parserer。封裝返回的數據也是如此。

讓 Jetty 工作在 AJP 協議下,需要配置 connector 的實現類爲 Ajp13SocketConnector,這個類繼承了 SocketConnector 類,覆蓋了父類的 newConnection 方法,爲的是創建 Ajp13Connection 對象而不是 HttpConnection。如下圖表示的是 Jetty 創建連接環境時序圖:

image.png

與 HTTP 方式唯一不同的地方的就是將 SocketConnector 類替換成了 Ajp13SocketConnector。改成 Ajp13SocketConnector 的目的就是可以創建 Ajp13Connection 類,表示當前這個連接使用的是 AJP 協議,所以需要用 Ajp13Parser 類解析 AJP 協議,處理連接的邏輯都是一樣的。如下時序圖所示:

image.png

3、NIO處理方式

Jetty 建立客戶端連接到處理客戶端的連接也支持 NIO 的處理方式,其中 Jetty 的默認 connector 就是 NIO 方式。

關於 NIO 的工作原理可以參考 developerworks 上關於 NIO 的文章,通常 NIO 的工作原型如下:

Selector selector = Selector.open(); 
ServerSocketChannel ssc = ServerSocketChannel.open(); 
ssc.configureBlocking( false ); 
SelectionKey key = ssc.register( selector, SelectionKey.OP_ACCEPT ); 
ServerSocketChannel ss = (ServerSocketChannel)key.channel(); 
SocketChannel sc = ss.accept(); 
sc.configureBlocking( false );
SelectionKey newKey = sc.register( selector, SelectionKey.OP_READ ); 
Set selectedKeys = selector.selectedKeys();

創建一個 Selector 相當於一個觀察者,打開一個 Server 端通道,把這個 server 通道註冊到觀察者上並且指定監聽的事件。然後遍歷這個觀察者觀察到事件,取出感興趣的事件再處理。這裏有個最核心的地方就是,我們不需要爲每個被觀察者創建一個線程來監控它隨時發生的事件。而是把這些被觀察者都註冊一個地方統一管理,然後由它把觸發的事件統一發送給感興趣的程序模塊。這裏的核心是能夠統一的管理每個被觀察者的事件,所以我們就可以把服務端上每個建立的連接傳送和接受數據作爲一個事件統一管理,這樣就不必要每個連接需要一個線程來維護了。

這裏需要注意的地方時,很多人認爲監聽 SelectionKey.OP_ACCEPT 事件就已經是非阻塞方式了,其實 Jetty 仍然是用一個線程來監聽客戶端的連接請求,當接受到請求後,把這個請求再註冊到 Selector 上,然後纔是非阻塞方式執行。這個地方還有一個容易引起誤解的地方是:認爲 Jetty 以 NIO 方式工作只會有一個線程來處理所有的請求,甚至會認爲不同用戶會在服務端共享一個線程從而會導致基於 ThreadLocal 的程序會出現問題,其實從 Jetty 的源碼中能夠發現,真正共享一個線程的處理只是在監聽不同連接的數據傳送事件上,比如有多個連接已經建立,傳統方式是當沒有數據傳輸時,線程是阻塞的也就是一直在等待下一個數據的到來,而 NIO 的處理方式是隻有一個線程在等待所有連接的數據的到來,而當某個連接數據到來時 Jetty 會把它分配給這個連接對應的處理線程去處理,所以不同連接的處理線程仍然是獨立的。

Jetty 的 NIO 處理方式和 Tomcat 的幾乎一樣,唯一不同的地方是在如何把監聽到事件分配給對應的連接的處理方式。從測試效果來看 Jetty 的 NIO 處理方式更加高效。下面是 Jetty 的 NIO 處理時序圖:

image.png

五、與 Tomcat 的比較

Tomcat 和 Jetty 都是作爲一個 Servlet 引擎應用的比較廣泛,可以將它們比作爲中國與美國的關係,雖然 Jetty 正常成長爲一個優秀的 Servlet 引擎,但是目前的 Tomcat 的地位仍然難以撼動。相比較來看,它們都有各自的優點與缺點。

Tomcat 經過長時間的發展,它已經廣泛的被市場接受和認可,相對 Jetty 來說 Tomcat 還是比較穩定和成熟,尤其在企業級應用方面,Tomcat 仍然是第一選擇。但是隨着 Jetty 的發展,Jetty 的市場份額也在不斷提高,至於原因就要歸功與 Jetty 的很多優點了,而這些優點也是因爲 Jetty 在技術上的優勢體現出來的。

架構比較

從架構上來說,顯然 Jetty 比 Tomcat 更加簡單,如果你對 Tomcat 的架構還不是很瞭解的話,建議你先看一下 《Tomcat系統架構與設計模式》這篇文章。

Jetty 的架構從前面的分析可知,它的所有組件都是基於 Handler 來實現,當然它也支持 JMX。但是主要的功能擴展都可以用 Handler 來實現。可以說 Jetty 是面向 Handler 的架構,就像 Spring 是面向 Bean 的架構,iBATIS 是面向 statement 一樣,而 Tomcat 是以多級容器構建起來的,它們的架構設計必然都有一個“元神”,所有以這個“元神“構建的其它組件都是肉身。

從設計模板角度來看 Handler 的設計實際上就是一個責任鏈模式,接口類 HandlerCollection 可以幫助開發者構建一個鏈,而另一個接口類 ScopeHandler 可以幫助你控制這個鏈的訪問順序。另外一個用到的設計模板就是觀察者模式,用這個設計模式控制了整個 Jetty 的生命週期,只要繼承了 LifeCycle 接口,你的對象就可以交給 Jetty 來統一管理了。所以擴展 Jetty 非常簡單,也很容易讓人理解,整體架構上的簡單也帶來了無比的好處,Jetty 可以很容易被擴展和裁剪。

相比之下,Tomcat 要臃腫很多,Tomcat 的整體設計上很複雜,前面說了 Tomcat 的核心是它的容器的設計,從 Server 到 Service 再到 engine 等 container 容器。作爲一個應用服務器這樣設計無口厚非,容器的分層設計也是爲了更好的擴展,這是這種擴展的方式是將應用服務器的內部結構暴露給外部使用者,使得如果想擴展 Tomcat,開發人員必須要首先了解 Tomcat 的整體設計結構,然後才能知道如何按照它的規範來做擴展。這樣無形就增加了對 Tomcat 的學習成本。不僅僅是容器,實際上 Tomcat 也有基於責任鏈的設計方式,像串聯 Pipeline 的 Vavle 設計也是與 Jetty 的 Handler 類似的方式。要自己實現一個 Vavle 與寫一個 Handler 的難度不相上下。表面上看,Tomcat 的功能要比 Jetty 強大,因爲 Tomcat 已經幫你做了很多工作了,而 Jetty 只告訴,你能怎麼做,如何做,有你去實現。

打個比方,就像小孩子學數學,Tomcat 告訴你 1+1=2,1+2=3,2+2=4 這個結果,然後你可以根據這個方式得出 1+1+2=4,你要計算其它數必須根據它給你的公式才能計算,而 Jetty 是告訴你加減乘除的算法規則,然後你就可以根據這個規則自己做運算了。所以你一旦掌握了 Jetty,Jetty 將變得異常強大。

性能比較

單純比較 Tomcat 與 Jetty 的性能意義不是很大,只能說在某種使用場景下,它表現的各有差異。因爲它們面向的使用場景不盡相同。從架構上來看 Tomcat 在處理少數非常繁忙的連接上更有優勢,也就是說連接的生命週期如果短的話,Tomcat 的總體性能更高。

而 Jetty 剛好相反,Jetty 可以同時處理大量連接而且可以長時間保持這些連接。例如像一些 web 聊天應用非常適合用 Jetty 做服務器,像淘寶的 web 旺旺就是用 Jetty 作爲 Servlet 引擎。

另外由於 Jetty 的架構非常簡單,作爲服務器它可以按需加載組件,這樣不需要的組件可以去掉,這樣無形可以減少服務器本身的內存開銷,處理一次請求也是可以減少產生的臨時對象,這樣性能也會提高。另外 Jetty 默認使用的是 NIO 技術在處理 I/O 請求上更佔優勢,Tomcat 默認使用的是 BIO,在處理靜態資源時,Tomcat 的性能不如 Jetty。

特性比較

作爲一個標準的 Servlet 引擎,它們都支持標準的 Servlet 規範,還有 Java EE 的規範也都支持,由於 Tomcat 的使用的更加廣泛,它對這些支持的更加全面一些,有很多特性 Tomcat 都直接集成進來了。但是 Jetty 的應變更加快速,這一方面是因爲 Jetty 的開發社區更加活躍,另一方面也是因爲 Jetty 的修改更加簡單,它只要把相應的組件替換就好了,而 Tomcat 的整體結構上要複雜很多,修改功能比較緩慢。所以 Tomcat 對最新的 Servlet 規範的支持總是要比人們預期的要晚。

參考資料

官方文檔 Jetty 的工作原理以及與 Tomcat 的比較

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章