Jetty 的工作原理以及與 Tomcat 的比較
Jetty 的基本架構
Jetty 目前的是一個比較被看好的 Servlet 引擎,它的架構比較簡單,也是一個可擴展性和非常靈活的應用服務器,它有一個基本數據模型,這個數據模型就是 Handler,所有可以被擴展的組件都可以作為一個 Handler,添加到 Server 中,Jetty 就是幫你管理這些 Handler。
下圖是 Jetty 的基本架構圖,整個 Jetty 的核心組件由 Server 和 Connector 兩個組件構成,整個 Server 組件是基于 Handler 容器工作的,它類似與 Tomcat 的 Container 容器,Jetty 與 Tomcat 的比較在后面詳細介紹。Jetty 中另外一個比不可少的組件是 Connector,它負責接受客戶端的連接請求,并將請求分配給一個處理隊列去執行。
圖 1. Jetty 的基本架構

Jetty 中還有一些可有可無的組件,我們可以在它上做擴展。如 JMX,我們可以定義一些 Mbean 把它加到 Server 中,當 Server 啟動的時候,這些 Bean 就會一起工作。
圖 2. Jetty 的主要組件的類圖

從上圖可以看出整個 Jetty 的核心是圍繞著 Server 類來構建,Server 類繼承了 Handler,關聯了 Connector 和 Container。Container 是管理 Mbean 的容器。Jetty 的 Server 的擴展主要是實現一個個 Handler 并將 Handler 加到 Server 中,Server 中提供了調用這些 Handler 的訪問規則。
整個 Jetty 的所有組件的生命周期管理是基于觀察者模板設計,它和 Tomcat 的管理是類似的。下面是 LifeCycle 的類關系圖

每個組件都會持有一個觀察者(在這里是 Listener 類,這個類通常對應到觀察者模式中常用的 Observer 角色,關于觀察者模式可以參考 《Tomcat系統架構與設計模式,第2部分:設計模式分析》一文中關于觀察者模式的講解)集合,當 start、fail 或 stop 等事件觸發時,這些 Listener 將會被調用,這是最簡單的一種設計方式,相比 Tomcat 的 LifeCycle 要簡單的多。
前面所述 Jetty 主要是基于 Handler 來設計的,Handler 的體系結構影響著整個 Jetty 的方方面面。下面總結了一下 Handler 的種類及作用:
圖 3. 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 的啟動時序圖:
圖 4. 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 如何基于這兩種協議工作,并且它們如何建立連接和接受請求的。
如果前端沒有其它 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 啟動創建建立連接的時序圖:
圖 5. 建立連接的時序圖

Jetty 創建接受連接環境需要三個步驟:
- 創建一個隊列線程池,用于處理每個建立連接產生的任務,這個線程池可以由用戶來指定,這個和 Tomcat 是類似的。
- 創建 ServerSocket,用于準備接受客戶端的 socket 請求,以及客戶端用來包裝這個 socket 的一些輔助類。
- 創建一個或多個監聽線程,用來監聽訪問端口是否有連接進來。 </ol>
- 查看文章 《 Tomcat 系統架構與設計模式》(developerWorks,2010 年 5 月):了解 Tomcat 中容器的體系結構,基本的工作原理,以及 Tomcat 中使用的經典的設計模式介紹。
- Servlet工作原理解析, (developerWorks,2011 年 2 月):以 Tomcat 為例了解 Servlet 容器是如何工作的?一個 Web 工程在 Servlet 容器中是如何啟動的? Servlet 容器如何解析你在 web.xml 中定義的 Servlet ?用戶的請求是如何被分配給指定的 Servlet 的? Servlet 容器如何管理 Servlet 生命周期?你還將了解到最新的 Servlet 的 API 的類層次結構,以及 Servlet 中一些難點問題的分析。
- Tomcat vs Jetty,對 Tomcat 與 Jetty 的做了比較。
- HTTP 協議,W3C 關于 HTTP 協議的詳細描述。
- developerWorks Java 技術專區:這里有數百篇關于 Java 編程各個方面的文章
相比 Tomcat 創建建立連接的環境,Jetty 的邏輯更加簡單,牽涉到的類更少,執行的代碼量也更少了。
當建立連接的環境已經準備好了,就可以接受 HTTP 請求了,當 Acceptor 接受到 socket 連接后將轉入下圖所示流程執行:
圖 6. 處理連接時序圖

Accetptor 線程將會為這個請求創建 ConnectorEndPoint。HttpConnection 用來表示這個連接是一個 HTTP 協議的連接,它會創建 HttpParse 類解析 HTTP 協議,并且會創建符合 HTTP 協議的 Request 和 Response 對象。接下去就是將這個線程交給隊列線程池去執行了。
通常一個 web 服務站點的后端服務器不是將 Java 的應用服務器直接暴露給服務訪問者,而是在應用服務器,如 Jboss 的前面在加一個 web 服務器,如 Apache 或者 nginx,我想這個原因大家應該很容易理解,如做日志分析、負載均衡、權限控制、防止惡意請求以及靜態資源預加載等等。
下圖是通常的 web 服務端的架構圖:
圖 7. Web 服務端架構( 查看大圖)

這種架構下 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 創建連接環境時序圖:

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

前面所描述的 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(); |
</tr>
</tbody>
</table>
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(); |
</tr>
</tbody>
</table>
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(); server.join(); |
</tr>
</tbody>
</table>