Jetty目前的是一個比較被看好的 Servlet 引擎,它的架構比較簡單,也是一個可擴展性和很是靈活的應用服務器。它有一個基本數據模型,這個數據模型就是 Handler(處理器),全部能夠被擴展的組件均可以做爲一個 Handler,添加到 Server 中,Jetty 就是幫你管理這些 Handler。Jetty 中另一個比不可少的組件是 Connector,它負責接受客戶端的鏈接請求,並將請求分配給一個處理隊列去執行。html
下圖是 Jetty 的基本架構圖,整個 Jetty 的核心組件由 Server 和 Connector 兩個組件構成,整個 Server 組件是基於 Handler 容器工做的,它相似與 Tomcat 的 Container 容器。前端
圖 1. Jetty 的基本架構java
Jetty 中還有一些無關緊要的組件,咱們能夠在它上作擴展。如 JMX,咱們能夠定義一些 Mbean 把它加到 Server 中,當 Server 啓動的時候,這些 Bean 就會一塊兒工做。nginx
圖 2. Jetty 的主要組件的類圖web
從上圖能夠看出整個 Jetty 的核心是圍繞着 Server 類來構建,Server 類繼承了 Handler,關聯了 Connector 和 Container,Container 是管理 Mbean 的容器。算法
Jetty 的 Server 的擴展主要是實現一個個 Handler 並將 Handler 加到 Server 中,Server 中提供了調用這些 Handler 的訪問規則。後端
整個 Jetty 的全部組件的生命週期管理是基於觀察者模板設計,它和 Tomcat 的管理是相似的。設計模式
圖 3. LifeCycle 的類關係圖瀏覽器
每一個組件都會持有一個觀察者(在這裏是 Listener 類,這個類一般對應到觀察者模式中經常使用的 Observer 角色,關於觀察者模式能夠參考 《Tomcat系統架構與設計模式,第2部分:設計模式分析》一文中關於觀察者模式的講解)集合,當 start、fail 或 stop 等事件觸發時,這些 Listener 將會被調用,這是最簡單的一種設計方式,相比 Tomcat 的 LifeCycle 要簡單的多。tomcat
體系結構
前面所述 Jetty 主要是基於 Handler 來設計的,Handler 的體系結構影響着整個 Jetty 的方方面面。下面總結了一下 Handler 的種類及做用:
Jetty 主要提供了兩種 Handler 類型,一種是 HandlerWrapper,它能夠將一個 Handler 委託給另一個類去執行,如咱們要將一個 Handler 加到 Jetty 中,那麼就必須將這個 Handler 委託給 Server 去調用。配合 ScopeHandler 類咱們能夠攔截 Handler 的執行,在調用 Handler 以前或以後,能夠作一些另外的事情,相似於 Tomcat 中的 Valve(閥門);另一個 Handler 類型是 HandlerCollection,這個 Handler 類能夠將多個 Handler 組裝在一塊兒,構成一個 Handler 鏈,方便咱們作擴展。
啓動過程
Jetty 的入口是 Server 類,Server 類啓動完成了,就表明 Jetty 能爲你提供服務了。
它到底能提供哪些服務,就要看 Server 類啓動時都調用了哪些組件的 start 方法。
從 Jetty 的配置文件咱們能夠發現,配置 Jetty 的過程就是將那些類配置到 Server 的過程。下面是 Jetty 的啓動時序圖:
圖5. Jetty啓動時序圖
由於 Jetty 中全部的組件都會繼承 LifeCycle,因此 Server 的 start 方法調用就會調用全部已經註冊到 Server 的組件,Server 啓動其它組件的順序是:
- 啓動設置到 Server 的 Handler(一般這個 Handler 會有不少子 Handler,這些 Handler 將組成一個 Handler 鏈,Server 會依次啓動這個鏈上的全部 Handler);
- 接着會啓動註冊在 Server 上 JMX 的 Mbean,讓 Mbean 也一塊兒工做起來;
- 最後會啓動 Connector,打開端口,接受客戶端請求。
接受請求
Jetty 做爲一個獨立的 Servlet 引擎能夠獨立提供 Web 服務,可是它也能夠與其餘 Web 應用服務器集成,因此它能夠提供基於兩種協議工做,一個是 HTTP,一個是 AJP 協議。
若是將 Jetty 集成到 Jboss 或者 Apache,那麼就可讓 Jetty 基於 AJP 模式工做。
下面分別介紹 Jetty 如何基於這兩種協議工做,以及如何創建鏈接和接受請求的。
基於 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 啓動建立創建鏈接的時序圖:
圖 6. 創建鏈接的時序圖
Jetty 建立接受鏈接環境須要三個步驟:
- 建立一個隊列線程池,用於處理每一個創建鏈接產生的任務,這個線程池能夠由用戶來指定,這個和 Tomcat 是相似的。
- 建立 ServerSocket,用於準備接受客戶端的 socket 請求,以及客戶端用來包裝這個 socket 的一些輔助類。
- 建立一個或多個監聽線程,用來監聽訪問端口是否有鏈接進來。
相比 Tomcat 建立創建鏈接的環境,Jetty 的邏輯更加簡單,牽涉到的類更少,執行的代碼量也更少了。
當創建鏈接的環境已經準備好了,就能夠接受 HTTP 請求了,當 Acceptor 接受到 socket 鏈接後將轉入下圖所示流程執行:
圖 7. 處理鏈接時序圖
Acceptor 線程將會爲這個請求建立 ConnectorEndPoint。HttpConnection 用來表示這個鏈接是一個 HTTP 協議的鏈接,它會建立 HttpParse 類解析 HTTP 協議,而且會建立符合 HTTP 協議的 Request 和 Response 對象。接下去就是將這個線程交給隊列線程池去執行了。
基於 AJP 工做
一般一個 web 站點的後端服務器不會將 Java 的應用服務器直接暴露給服務訪問者,而是在應用服務器(如Jboss)的前面再加一個 web 服務器(如 Apache 或者 nginx),我想這個緣由你們應該很容易理解,如作日誌分析、負載均衡、權限控制、防止惡意請求以及靜態資源預加載等等。
下圖是一般的 web 服務端的架構圖:
這種架構下 servlet 引擎就不須要解析和封裝返回的 HTTP 協議,由於 HTTP 協議的解析工做已經在 Apache 或 Nginx 服務器上完成了,Jboss 只要基於更加簡單的 AJP 協議工做就好了,這樣能加快請求的響應速度。
對比 HTTP 協議的時序圖能夠發現,它們的邏輯幾乎是相同的,不一樣的是替換了一個類 Ajp13Parser 而不是 HttpParser,它定義瞭如何處理 AJP 協議以及須要哪些類來配合。
實際上在 AJP 處理請求相比較 HTTP 時惟一的不一樣就是在讀取到 socket 數據包時,如何來轉換這個數據包。若是按照 HTTP 協議的包格式來解析就是 HttpParser,若是按照 AJP 協議來解析就是 Ajp13Parser。封裝返回的數據也是如此。
讓 Jetty 工做在 AJP 協議下,須要配置 connector 的實現類爲 Ajp13SocketConnector,這個類繼承了 SocketConnector 類,覆蓋了父類的 newConnection 方法,爲的是建立 Ajp13Connection 對象而不是 HttpConnection。以下圖表示的是 Jetty 建立鏈接環境時序圖:
圖9. Jetty 建立鏈接環境時序圖
與 HTTP 方式惟一不一樣的地方的就是將 SocketConnector 類替換成了 Ajp13SocketConnector。改爲 Ajp13SocketConnector 的目的就是能夠建立 Ajp13Connection 類,表示當前這個鏈接使用的是 AJP 協議,因此須要用 Ajp13Parser 類解析 AJP 協議,處理鏈接的邏輯都是同樣的。以下時序圖所示:
基於 NIO 方式工做
前面所描述的 Jetty 創建客戶端鏈接處處理客戶端的鏈接都是基於 BIO 的方式,它也支持另一種 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 處理時序圖:
處理請求
下面看一下 Jetty 是如何處理一個 HTTP 請求的。
實際上 Jetty 的工做方式很是簡單,當 Jetty 接受到一個請求時,Jetty 就把這個請求交給在 Server 中註冊的代理 Handler 去執行,如何執行你註冊的 Handler,一樣由你去規定,Jetty 要作的就是調用你註冊的第一個 Handler 的 handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) 方法,接下去要怎麼作,徹底由你決定。
要能接受一個 web 請求訪問,首先要建立一個 ContextHandler,以下代碼所示:
Server server = new Server(8080);
ContextHandler context = new ContextHandler();
context.setContextPath("/");
context.setResourceBase(".");
context.setClassLoader(Thread.currentThread().getContextClassLoader());
server.setHandler(context);
context.setHandler(new HelloHandler());
server.start();
server.join();
當咱們在瀏覽器裏敲入 http://localhost:8080 時的請求將會代理到 Server 類的 handle 方法,Server 的 handle 的方法將請求代理給 ContextHandler 的 handle 方法,ContextHandler 又調用 HelloHandler 的 handle 方法。這個調用方式是否是和 Servlet 的工做方式相似,在啓動以前初始化,而後建立對象後調用 Servlet 的 service 方法。在 Servlet 的 API 中我一般也只實現它的一個包裝好的類,在 Jetty 中也是如此,雖然 ContextHandler 也只是一個 Handler,可是這個 Handler 一般是由 Jetty 幫你實現了,咱們通常只要實現一些咱們具體要作的業務邏輯有關的 Handler 就行了,而一些流程性的或某些規範的 Handler,咱們直接用就行了,以下面的關於 Jetty 支持 Servlet 的規範的 Handler 就有多種實現,下面是一個簡單的 HTTP 請求的流程。
訪問一個 Servlet 的代碼:
Server server = new Server();
Connector connector = new SelectChannelConnector();
connector.setPort(8080);
server.setConnectors(new Connector[]{ connector });
ServletContextHandler root = new
ServletContextHandler(null,"/",ServletContextHandler.SESSIONS);
server.setHandler(root);
root.addServlet(new ServletHolder(new
org.eclipse.jetty.embedded.HelloServlet("Hello")),"/");
server.start();
建立一個 ServletContextHandler 並給這個 Handler 添加一個 Servlet,這裏的 ServletHolder 是 Servlet 的一個裝飾類,它十分相似於 Tomcat 中的 StandardWrapper。下面是請求這個 Servlet 的時序圖:
圖 12. Jetty 處理請求的時序圖
上圖能夠看出 Jetty 處理請求的過程就是 Handler 鏈上 handle 方法的執行過程,在這裏須要解釋的一點是 ScopeHandler 的處理規則,ServletContextHandler、SessionHandler 和 ServletHandler 都繼承了 ScopeHandler,那麼這三個類組成一個 Handler 鏈,它們的執行規則是:ServletContextHandler.handleServletContextHandler.doScope SessionHandler. doScopeServletHandler. doScopeServletContextHandler. doHandleSessionHandler. doHandleServletHandler. doHandle,它這種機制使得咱們能夠在 doScope 作一些額外工做。
與 Jboss 集成
前面介紹了 Jetty 能夠基於 AJP 協議工做,在正常的企業級應用中,Jetty 做爲一個 Servlet 引擎都是基於 AJP 協議工做的,因此它前面必然有一個服務器,一般狀況下與 Jboss 集成的可能性很是大,這裏介紹一下如何與 Jboss 集成。
Jboss 是基於 JMX 的架構,那麼只要符合 JMX 規範的系統或框架均可以做爲一個組件加到 Jboss 中,擴展 Jboss 的功能。Jetty 做爲主要的 Servlet 引擎固然支持與 Jboss 集成。具體集成方式以下:
Jetty 做爲一個獨立的 Servlet 引擎集成到 Jboss 須要繼承 Jboss 的 AbstractWebContainer 類,這個類實現的是模板模式,其中有一個抽象方法須要子類去實現,它是 getDeployer,能夠指定建立 web 服務的 Deployer。Jetty 工程中有個 jetty-jboss 模塊,編譯這個模塊就會產生一個 SAR 包,或者能夠直接從官網下載一個 SAR 包。解壓後以下圖:
圖 13. jboss-jetty 目錄
在 jboss-jetty-6.1.9 目錄下有一個 webdefault.xml 配置文件,這個文件是 Jetty 的默認 web.xml 配置,在 META-INF 目錄發下發現 jboss-service.xml 文件,這個文件配置了 MBean,以下:
<mbean code="org.jboss.jetty.JettyService" name="jboss.web:service=WebServer" xmbean-dd="META-INF/webserver-xmbean.xml">
一樣這個 org.jboss.jetty.JettyService 類也是繼成 org.jboss.web.AbstractWebContainer 類,覆蓋了父類的 startService 方法,這個方法直接調用 jetty.start 啓動 Jetty。
與 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 規範的支持老是要比人們預期的要晚。
總結
本文介紹了目前 Java 服務端中一個比較流行應用服務器 Jetty,介紹了它的基本架構和工做原理以及如何和 Jboss 工做,最後與 Tomcat 作了比較。在看這篇文章的時候最好是結合這兩篇文章《 Tomcat 系統架構與設計模式》和《 Servlet 工做原理解析》以及這些系統的源代碼,耐心的都看一下會讓你對 Java 服務端有個整體的瞭解。