Web容器Jetty

一、Jetty简介

       Jetty 是一个开源的servlet容器,它为基于Java的web容器(例如JSP和servlet)提供运行环境。Jetty是使用Java语言编写的,它的API以一组JAR包的形式发布。

       开发人员可以将Jetty容器实例化成一个对象,可以迅速为一些独立运行(stand-alone)的Java应用提供网络和web连接。

 

二、Jetty特性

       Jetty作为一个Servlet容器,主要有三大特性:易用性、可扩展性、易嵌入性。下面分别简述一下这三大特性:

       易用性:该特性是 Jetty 设计的基本原则,主要体现在以下几个方面:通过 XML 或者 API 来对Jetty进行配置;默认配置可以满足大部分的需求;将 Jetty 嵌入到应用程序当中只需要非常少的代码。

       可扩展性:在使用了 Ajax 的 Web 2.0 的应用程序中,每个连接需要保持更长的时间,这样线程和内存的消耗量会急剧的增加。这就使得我们担心整个程序会因为单个组件陷入瓶颈而影响整个程序的性能。但是有了 Jetty,即使在有大量服务请求的情况下,系统的性能也能保持在一个可以接受的状态。利用 Continuation 机制来处理大量的用户请求以及时间比较长的连接。 另外 Jetty 设计了非常良好的接口,因此在 Jetty 的某种实现无法满足用户的需要时,用户可以非常方便地对 Jetty 的某些实现进行修改,使得 Jetty 适用于特殊的应用程序的需求。

       易嵌入性:Jetty 设计之初就是作为一个优秀的组件来设计的,这也就意味着 Jetty 可以非常容易的嵌入到应用程序当中而不需要程序为了使用 Jetty 做修改。从某种程度上,你也可以把 Jetty 理解为一个嵌入式的Web服务器。Jetty 可以作为嵌入式服务器使用,Jetty的运行速度较快,而且是轻量级的,可以在Java中可以从test case中控制其运行。从而可以使自动化测试不再依赖外部环境,顺利实现自动化测试。

 

三、Jetty工作原理及与Tomcat的比较

       Jetty 目前的是一个比较被看好的 Servlet 引擎,它的架构比较简单,也是一个可扩展性和非常灵活的应用服务器,它有一个基本数据模型,这个数据模型就是 Handler,所有可以被扩展的组件都可以作为一个 Handler,添加到 Server 中,Jetty 就是帮你管理这些 Handler。

       基本架构图:附件中图1是Jetty的基本架构图,整个 Jetty 的核心组件由 Server 和 Connector 两个组件构成,整个 Server 组件是基于 Handler 容器工作的,它类似与 Tomcat 的 Container 容器,Jetty 与 Tomcat 的比较在后面详细介绍。Jetty 中另外一个比不可少的组件是 Connector,它负责接受客户端的连接请求,并将请求分配给一个处理队列去执行。

       主要组件:附件中图2是Jetty的主要组件的类图,从该图可以看出整个 Jetty 的核心是围绕着 Server 类来构建,Server 类继承了 Handler,关联了 Connector 和 Container。Container 是管理 Mbean 的容器。Jetty 的 Server 的扩展主要是实现一个个 Handler 并将 Handler 加到 Server 中,Server 中提供了调用这些 Handler 的访问规则。每个组件都会持有一个观察者(在这里是 Listener 类,这个类通常对应到观察者模式中常用的 Observer 角色,当 start、fail 或 stop 等事件触发时,这些 Listener 将会被调用,这是最简单的一种设计方式,相比 Tomcat 的 LifeCycle 要简单的多。Jetty 中还有一些可有可无的组件,我们可以在它上做扩展。如 JMX,我们可以定义一些 Mbean 把它加到 Server 中,当 Server 启动的时候,这些 Bean 就会一起工作。

      整个 Jetty 的所有组件的生命周期管理是基于观察者模板设计,它和 Tomcat 的管理是类似的。附件中图3是 LifeCycle 的类关系图。

      Jetty主要是基于Handle类设计的,Handler体系影响着Jetty的方方面面,附件中图4总结了Handler的总类及作用。从图4中可以看出,Jetty 主要提供了两种 Handler 类型,一种是 HandlerWrapper,它可以将一个 Handler 委托给另外一个类去执行,如我们要将一个 Handler 加到 Jetty 中,那么就必须将这个 Handler 委托给 Server 去调用。配合 ScopeHandler 类我们可以拦截 Handler 的执行,在调用 Handler 之前或之后,可以做一些另外的事情,类似于 Tomcat 中的 Valve;另外一个 Handler 类型是 HandlerCollection,这个 Handler 类可以将多个 Handler 组装在一起,构成一个 Handler 链,方便我们做扩展。

       Jetty 的启动过程:Jetty 的入口是 Server 类,Server 类启动完成了,就代表 Jetty 能为你提供服务了。它到底能提供哪些服务,就要看 Server 类启动时都调用了其它组件的 start 方法。从 Jetty 的配置文件我们可以发现,配置 Jetty 的过程就是将那些类配置到 Server 的过程。

       附件中图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 容器里。附件中的图6是Jetty 启动创建建立连接的时序图。

       Jetty 创建接受连接环境的三个步骤:

       1、创建一个队列线程池,用于处理每个建立连接产生的任务,这个线程池可以由用户来指定,这个和 Tomcat 是类似的。

       2、创建 ServerSocket,用于准备接受客户端的 socket 请求,以及客户端用来包装这个 socket 的一些辅助类。

       3、创建一个或多个监听线程,用来监听访问端口是否有连接进来。

       相比 Tomcat 创建建立连接的环境,Jetty 的逻辑更加简单,牵涉到的类更少,执行的代码量也更少了。

当建立连接的环境已经准备好了,就可以接受 HTTP 请求了,当 Acceptor 接受到 socket 连接后将转入附件中图7所示的流程执行。图7中的Accetptor 线程将会为这个请求创建 ConnectorEndPoint。HttpConnection 用来表示这个连接是一个 HTTP 协议的连接,它会创建 HttpParse 类解析 HTTP 协议,并且会创建符合 HTTP 协议的 Request 和 Response 对象。接下去就是将这个线程交给队列线程池去执行了。

       基于 AJP 工作:通常一个 web 服务站点的后端服务器不是将 Java 的应用服务器直接暴露给服务访问者,而是在应用服务器,如 Jboss 的前面在加一个 web 服务器,如 Apache 或者 nginx,这是为了如做日志分析、负载均衡、权限控制、防止恶意请求以及静态资源预加载等等。

 

四、Web服务端架构

       附件中图8显示了这种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。附件中图9表示的是Jetty创建连接环境时序图。

       与HTTP方式唯一不同的地方的就是将SocketConnector类替换成了Ajp13SocketConnector。改成Ajp13SocketConnector的目的就是可以创建Ajp13Connection类,表示当前这个连接使用的是AJP协议,所以需要用Ajp13Parser类解析AJP协议,处理连接的逻辑都是一样的,如附件中图10所示。

       基于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处理方式更加高效,附件中的图11是Jetty的NIO处理时序图。

       处理请求:Jetty处理一个HTTP请求的工作方式非常简单,当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请求的流程:

 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();

       创建一个ServletContextHandler并给这个Handler添加一个Servlet,这里的ServletHolder是Servlet的一个装饰类,它十分类似于Tomcat中的StandardWrapper,附件中图12是请求这个Servlet的时序图。从附件中图12可以看出Jetty处理请求的过程就是Handler链上handle方法的执行过程,在这里需要解释的一点是 ScopeHandler的处理规则,ServletContextHandler、SessionHandler 和 ServletHandler都继承了ScopeHandler,那么这三个类组成一个Handler链,它们的执行规则是:ServletContextHandler.handle ServletContextHandler.doScope SessionHandler. doScope ServletHandler.doScope ServletContextHandler. doHandle SessionHandler. doHandle ServletHandler. doHandle,它这种机制使得我们可以在doScope做一些额外工作。

五、与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规范的支持总是要比人们预期的要晚。

猜你喜欢

转载自zh-workhard-java.iteye.com/blog/2241279