1. 程式人生 > >沉澱再出發:jetty的架構和本質

沉澱再出發:jetty的架構和本質

沉澱再出發:jetty的架構和本質

一、前言

    我們在使用Tomcat的時候,總是會想到jetty,這兩者的合理選用是和我們專案的型別和大小息息相關的,Tomcat屬於比較重量級的容器,通過很多的容器層層包裹提供了非常強大的web功能,但是可以自我定製的餘地就非常小了,有的時候我們希望自己設計更多的請求接收,處理和返回的環節,就可以用更加輕量級的jetty了。

二、jetty的架構和原理

 2.1、Jetty 的基本架構

    Jetty 是一個Servlet 引擎,它的架構比較簡單,是一個可擴充套件性和非常靈活的應用伺服器,它有一個基本資料模型,這個資料模型就是 Handler,所有可以被擴充套件的元件都可以作為一個 Handler,新增到 Server 中,Jetty 就是幫我們管理這些 Handler。 整個 Jetty 的核心元件由 Server 和 Connector 兩個元件構成,整個 Server 元件是基於 Handler 容器工作的,Jetty 中另外一個比不可少的元件是 Connector,它負責接受客戶端的連線請求,並將請求分配給一個處理佇列去執行。Jetty 中還有一些可有可無的元件,我們可以在它上做擴充套件。如 JMX,我們可以定義一些 Mbean 把它加到 Server 中,當 Server 啟動的時候,這些 Bean 就會一起工作。

     整個 Jetty 的核心是圍繞著 Server 類來構建,Server 類繼承了 Handler,關聯了 Connector 和 Container。Container 是管理 Mbean 的容器。Jetty 的 Server 的擴充套件主要是實現一個個 Handler 並將 Handler 加到 Server 中,Server 中提供了呼叫這些 Handler 的訪問規則。整個 Jetty 的所有元件的生命週期管理是基於觀察者模板設計,實現LifeCycle。

 2.2、Handler 的體系結構

    Jetty 主要是基於 Handler 來設計的,Handler 的體系結構影響著整個 Jetty 的方方面面。下面總結了一下 Handler 的種類及作用:

    Jetty 主要提供了兩種 Handler 型別,一種是 HandlerWrapper,它可以將一個 Handler 委託給另外一個類去執行,如我們要將一個 Handler 加到 Jetty 中,那麼就必須將這個 Handler 委託給 Server 去呼叫。配合 ScopeHandler 類我們可以攔截 Handler 的執行,在呼叫 Handler 之前或之後,可以做一些另外的事情,類似於 Tomcat 中的 Valve;另外一個 Handler 型別是 HandlerCollection,這個 Handler 類可以將多個 Handler 組裝在一起,構成一個 Handler 鏈,方便我們做擴充套件。

 2.3、Jetty 的啟動過程

    Jetty 的入口是 Server 類,Server 類啟動完成了,就代表 Jetty 能為我們提供服務了。它到底能提供哪些服務,就要看 Server 類啟動時都呼叫了其它元件的 start 方法。從 Jetty 的配置檔案我們可以發現,配置 Jetty 的過程就是將那些類配置到 Server 的過程。

 

    因為 Jetty 中所有的元件都會繼承 LifeCycle,所以 Server 的 start 方法呼叫就會呼叫所有已經註冊到 Server 的元件,Server 啟動其它元件的順序是:首先啟動設定到 Server 的 Handler,通常這個 Handler 會有很多子 Handler,這些 Handler 將組成一個 Handler 鏈。Server 會依次啟動這個鏈上的所有 Handler。接著會啟動註冊在 Server 上 JMX 的 Mbean,讓 Mbean 也一起工作起來,最後會啟動 Connector,開啟埠,接受客戶端請求,啟動邏輯非常簡單。

 2.4、接受請求

     Jetty 作為一個獨立的 Servlet 引擎可以獨立提供 Web 服務,也可以與其他 Web 應用伺服器整合,所以它可以提供基於兩種協議工作,一個是 HTTP,一個是 AJP 協議。如果將 Jetty 整合到 Jboss 或者 Apache,那麼就可以讓 Jetty 基於 AJP 模式工作。

 基於 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的方法。
   Jetty 建立接受連線環境需要三個步驟:

1 建立一個佇列執行緒池,用於處理每個建立連線產生的任務,這個執行緒池可以由使用者來指定。
2 建立 ServerSocket,用於準備接受客戶端的 socket 請求,以及客戶端用來包裝這個 socket 的一些輔助類。
3 建立一個或多個監聽執行緒,用來監聽訪問埠是否有連線進來。

    當建立連線的環境已經準備好了,就可以接受 HTTP 請求了,Accetptor 執行緒將會為這個請求建立 ConnectorEndPoint。HttpConnection 用來表示這個連線是一個 HTTP 協議的連線,它會建立 HttpParse 類解析 HTTP 協議,並且會建立符合 HTTP 協議的 Request 和 Response 物件。接下去就是將這個執行緒交給佇列執行緒池去執行了。

 基於 AJP 工作

      通常一個 web 服務站點的後端伺服器不是將 Java 的應用伺服器直接暴露給服務訪問者,而是在應用伺服器,如 Jboss 的前面在加一個 web 伺服器,如 Apache 或者 nginx,用來做日誌分析、負載均衡、許可權控制、防止惡意請求以及靜態資源預載入等等。

      這種架構下 servlet 引擎就不需要解析和封裝返回的 HTTP 協議,因為 HTTP 協議的解析工作已經在 Apache 或 Nginx 伺服器上完成了,Jboss 只要基於更加簡單的 AJP 協議工作就行了,這樣能加快請求的響應速度。對比 HTTP 協議的時序圖可以發現,它們的邏輯幾乎是相同的,不同的是將 HttpParser 替換成了Ajp13Parserer,它定義瞭如何處理 AJP 協議以及需要哪些類來配合。實際上在 AJP 處理請求相比較 HTTP 時唯一的不同就是在讀取到 socket 資料包時,如何來轉換這個資料包,是按照 HTTP 協議的包格式來解析就是 HttpParser,按照 AJP 協議來解析就是 Ajp13Parserer。封裝返回的資料也是如此。讓 Jetty 工作在 AJP 協議下,需要配置 connector 的實現類為 Ajp13SocketConnector,這個類繼承了 SocketConnector 類,覆蓋了父類的 newConnection 方法,為的是建立 Ajp13Connection 物件而不是 HttpConnection。

 2.5、基於 NIO 方式工作

    前面所描述的 Jetty 建立客戶端連線到處理客戶端的連線都是基於 BIO 的方式,它也支援另外一種 NIO 的處理方式,其中 Jetty 的預設 connector 就是 NIO 方式。通常 NIO 的工作原型如下:

1  Selector selector = Selector.open(); 
2  ServerSocketChannel ssc = ServerSocketChannel.open(); 
3  ssc.configureBlocking( false ); 
4  SelectionKey key = ssc.register( selector, SelectionKey.OP_ACCEPT ); 
5  ServerSocketChannel ss = (ServerSocketChannel)key.channel(); 
6  SocketChannel sc = ss.accept(); 
7  sc.configureBlocking( false ); 
8  SelectionKey newKey = sc.register( selector, SelectionKey.OP_READ ); 
9  Set selectedKeys = selector.selectedKeys(); 

     建立一個 Selector 相當於一個觀察者,開啟一個 Server 端通道,把這個 server 通道註冊到觀察者上並且指定監聽的事件。然後遍歷這個觀察者觀察到事件,取出感興趣的事件再處理。這裡有個最核心的地方就是,我們不需要為每個被觀察者建立一個執行緒來監控它隨時發生的事件。而是把這些被觀察者都註冊一個地方統一管理,然後由它把觸發的事件統一發送給感興趣的程式模組。這裡的核心是能夠統一的管理每個被觀察者的事件,所以我們就可以把服務端上每個建立的連線傳送和接受資料作為一個事件統一管理,這樣就不必要每個連線需要一個執行緒來維護了。
     這裡需要注意的地方時,很多人認為監聽 SelectionKey.OP_ACCEPT 事件就已經是非阻塞方式了,其實 Jetty 仍然是用一個執行緒來監聽客戶端的連線請求,當接受到請求後,把這個請求再註冊到 Selector 上,然後才是非阻塞方式執行。這個地方還有一個容易引起誤解的地方是:認為 Jetty 以 NIO 方式工作只會有一個執行緒來處理所有的請求,甚至會認為不同使用者會在服務端共享一個執行緒從而會導致基於 ThreadLocal 的程式會出現問題,其實從 Jetty 的原始碼中能夠發現,真正共享一個執行緒的處理只是在監聽不同連線的資料傳送事件上,比如有多個連線已經建立,傳統方式是當沒有資料傳輸時,執行緒是阻塞的也就是一直在等待下一個資料的到來,而 NIO 的處理方式是隻有一個執行緒在等待所有連線的資料的到來,而當某個連線資料到來時 Jetty 會把它分配給這個連線對應的處理執行緒去處理,所以不同連線的處理執行緒仍然是獨立的。Jetty 的 NIO 處理方式和 Tomcat 的幾乎一樣,唯一不同的地方是在如何把監聽到事件分配給對應的連線的處理方式。從測試效果來看 Jetty 的 NIO 處理方式更加高效。

 2.6、處理請求

    Jetty 的工作方式非常簡單,當 Jetty 接受到一個請求時,Jetty 就把這個請求交給在 Server 中註冊的代理 Handler 去執行,如何執行我們註冊的 Handler,同樣由我們去規定,Jetty 要做的就是呼叫我們註冊的第一個 Handler 的 handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) 方法,接下去要怎麼做,完全由我們決定。要能接受一個 web 請求訪問,首先要建立一個 ContextHandler。

1  Server server = new Server(8080); 
2  ContextHandler context = new ContextHandler(); 
3  context.setContextPath("/"); 
4  context.setResourceBase("."); 
5  context.setClassLoader(Thread.currentThread().getContextClassLoader()); 
6  server.setHandler(context); 
7  context.setHandler(new HelloHandler()); 
8  server.start(); 
9  server.join(); 

     當我們在瀏覽器裡敲入 http://localhost:8080 時的請求將會代理到 Server 類的 handle 方法,Server 的 handle 的方法將請求代理給 ContextHandler 的 handle 方法,在其內部又呼叫 HelloHandler 的 handle 方法。這個呼叫方式和 Servlet 的工作方式類似。雖然 ContextHandler 只是一個 Handler,但是這個 Handler 通常是由 Jetty 幫我們實現了,我們一般只要實現一些我們具體要做的業務邏輯有關的 Handler 就好了,而一些流程性的或某些規範的 Handler,我們直接用就好了。下面是一個簡單的 HTTP 請求的流程:

 1  Server server = new Server(); 
 2  Connector connector = new SelectChannelConnector(); 
 3  connector.setPort(8080); 
 4  server.setConnectors(new Connector[]{ connector }); 
 5  ServletContextHandler root = new 
 6  ServletContextHandler(null,"/",ServletContextHandler.SESSIONS); 
 7  server.setHandler(root); 
 8  root.addServlet(new ServletHolder(new 
 9  org.eclipse.jetty.embedded.HelloServlet("Hello")),"/"); 
10  server.start(); 
11  server.join(); 

    建立一個 ServletContextHandler 並給這個 Handler 新增一個 Servlet,這裡的 ServletHandler 是 Servlet 的一個裝飾類,它十分類似於 Tomcat 中的 StandardWrapper。

     Jetty 處理請求的過程就是 Handler 鏈上 handle 方法的執行過程,在這裡需要解釋的一點是 ScopeHandler 的處理規則,ServletContextHandler、SessionHandler 和 ServletHandler 都繼承了 ScopeHandler,這三個類組成一個 Handler 鏈,它們的執行規則是:

1 ServletContextHandler.handle->ServletContextHandler.doScope ->SessionHandler. doScope->ServletHandler. doScope->ServletContextHandler. doHandle->SessionHandler. doHandle->ServletHandler. doHandle

    這種機制使得我們可以在 doScope 做一些額外工作。

三、Jetty 與 Tomcat 的比較

 3.1、架構比較

    Jetty 的所有元件都是基於 Handler 來實現,當然它也支援 JMX。但是主要的功能擴充套件都可以用 Handler 來實現。可以說 Jetty 是面向 Handler 的架構,就像 Spring 是面向 Bean 的架構,Mybatis是面向 statement 一樣,而 Tomcat 是以多級容器構建起來的
    Jetty從設計模板角度來看 Handler 的設計實際上就是一個責任鏈模式,介面類 HandlerCollection 可以幫助開發者構建一個鏈,而另一個介面類 ScopeHandler 可以幫助控制這個鏈的訪問順序。另外一個用到的設計模板就是觀察者模式,用這個設計模式控制了整個 Jetty 的生命週期,只要繼承了 LifeCycle 介面,我們的物件就可以交給 Jetty 來統一管理了。所以擴充套件 Jetty 非常簡單,也很容易讓人理解,整體架構上的簡單也帶來了無比的好處,Jetty 可以很容易被擴充套件和裁剪
    Tomcat 要臃腫很多Tomcat的整體設計上很複雜,Tomcat的核心是它的容器的設計,從 Server 到 Service 再到 engine 等 container 容器。作為一個應用伺服器這樣設計無口厚非,容器的分層設計也是為了更好的擴充套件,這種擴充套件的方式是將應用伺服器的內部結構暴露給外部使用者,使得如果想擴充套件 Tomcat,開發人員必須要首先了解Tomcat的整體設計結構,然後才能知道如何按照它的規範來做擴充套件。這樣無形就增加了對 Tomcat 的學習成本。不僅僅是容器,實際上 Tomcat 也有基於責任鏈的設計方式,像串聯 Pipeline 的 Valve 設計也是與 Jetty 的 Handler 類似的方式。要自己實現一個 Valve 與寫一個 Handler 的難度不相上下。表面上看,Tomcat 的功能要比 Jetty 強大,因為Tomcat已經幫我們做了很多工作了,而Jetty只告訴我們能怎麼做,如何做,由我們自己去實現。

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

 3.2、效能比較

    單純比較 Tomcat 與 Jetty 的效能意義不是很大,只能說在某種使用場景下,它表現的各有差異。因為它們面向的使用場景不盡相同。從架構上來看 Tomcat 在處理少數非常繁忙的連線上更有優勢,也就是說連線的生命週期如果短的話,Tomcat 的總體效能更高。而 Jetty 剛好相反,Jetty 可以同時處理大量連線而且可以長時間保持這些連線。例如像一些 web 聊天應用非常適合用 Jetty 做伺服器,像淘寶的 web 旺旺就是用 Jetty 作為 Servlet 引擎。另外由於 Jetty 的架構非常簡單,作為伺服器它可以按需載入元件,這樣不需要的元件可以去掉,這樣無形可以減少伺服器本身的記憶體開銷,處理一次請求也是可以減少產生的臨時物件,這樣效能也會提高。另外 Jetty 預設使用的是 NIO 技術在處理 I/O 請求上更佔優勢,Tomcat 預設使用的是 BIO,在處理靜態資源時,Tomcat 的效能不如 Jetty

 3.3、特性比較

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

四、總結

    通過對jetty的學習,我們更加明白了沒有最好的技術,只有在某個行業方面最擅長的技術,jetty和Tomcat就是這樣出現的,對於我們設計一種框架來說也是可以借鑑的,要從多個方面考慮,做出一個功能很強難度很高的定製版,在做出一個可以擴充套件的簡單版。

參考文獻:https://blog.csdn.net/qing_2012/article/details/8276789