编者注：本文为历史博文归档；涉及 JDK、框架与工具链版本请以当前官方文档为准。引用外链图片可能失效，阅读时请注意时效性。如何正确使用 NIO 构建网络服务器一直是值得深入探讨的问题。为此，我们分析了 Jetty、Tomcat 和 Mina 有关 NIO 的源码，发现三者基于类似的方式实现。这应该算是 NIO 构架网络服务器的经典模式。基于这种模式，我们编写了一个小型网络服务器，经过压力测试，效果良好。下文将具体分析这三者是如何使用 NIO 的。Jetty Connector 的实现先看看有关类图：其中各组件职责如下：SelectChannelConnector：负责组装各组件SelectSet：负责侦听客户端请求SelectChannelEndPoint：负责 IO 的读和写HttpConnection：负责逻辑处理在整个服务端处理请求的过程可以分为三个阶段，时序图如下所示：阶段一：监听并建立连接这一过程主要是启动一个线程负责 accept 新连接，监听到后分配给相应的 SelectSet，分配的策略就是轮询。阶段二：监听客户端的请求这一过程主要是启动多个线程（线程数一般为服务器 CPU 的个数），让 SelectSet 监听所管辖的 channel 队列。每个 SelectSet 维护一个 Selector，这个 Selector 监听队列里所有的 channel，一旦有读事件，从线程池里拿线程去做处理请求。阶段三：处理请求这一过程就是每次客户端请求的数据处理过程。值得注意的是，为了不让后端的业务处理阻碍 Selector 监听新的请求，采用多线程来分隔开监听请求和处理请求两个阶段。由此可以大致总结出 Jetty 有关 NIO 使用的模式，如下图所示：最核心的设计就是把三件不同的事情隔离开，并用不同规模的线程去处理，最大限度地利用 NIO 的异步和通知特性。Tomcat Connector 的实现下面再来看看 Tomcat 是如何使用 NIO 来构架 Connector 这块的。先看看 Tomcat Connector 这块的类图：其中各组件职责如下：NioEndpoint：负责组装各部件Acceptor：负责监听新连接，并把连接交给 PollerPoller：负责监听所管辖的 channel 队列，并把请求交给 SocketProcessor 处理SocketProcessor：负责数据处理，并把请求传递给后端业务处理模块在整个服务端处理请求的过程可以分为三个阶段，时序图如下所示：阶段一：监听并建立连接这一阶段主要是 Acceptor 监听新连接，并轮询取一个 Poller，把连接交付给 Poller。阶段二：监听客户端的请求这一过程主要是让每个 Poller 监听所管辖的 channel 队列，select 到新请求后交付给 SocketProcessor 处理。阶段三：处理请求这一过程就是从多线程执行 SocketProcessor，做数据和业务处理。于是我们发现，抛开具体代码细节，Tomcat 和 Jetty 在 NIO 的使用方面是非常一致的，采用的模式依然是下图：Mina 的实现最后我们再看看 NIO 方面最著名的框架 Mina。抛开 Mina 有关 session 和处理链条等方面的设计，单单挑出前端网络层处理来看，也采用的是与 Jetty 和 Tomcat 类似的模式。只不过它做了些简化，没有隔开请求侦听和请求处理两个阶段，因此宏观上看它只分为两个阶段。先看看它的类图：其中各组件职责如下：SocketAcceptor：起线程调用 SocketAcceptor.Work 负责新连接侦听，并交给 SocketIoProcessor 处理SocketIoProcessor：起线程调用 SocketIoProcessor.Work 负责侦听所管辖的 channel 队列，select 到新请求后交给 IoFilterChain 处理IoFilterChain：组装了 Mina 的处理链条在整个服务端处理请求的过程可以分为两个阶段，时序图如下所示：阶段一：监听并建立连接阶段二：监听并处理客户端的请求总结与实践总结来看 Jetty、Tomcat 和 Mina，我们大概清楚了该如何基于 NIO 来构架网络服务器。通过这个提炼出来的模式，我们写了个很简单的 NIO Server。在保持连接的情况下，可以很轻松地保持 6 万连接（由于有 65535 连接限制），并能在负载只有 3 左右的情况下（4 核），承担 3 到 4 万的 TPS 请求（当然做的事情很简单，仅仅是把 buffer 转化为自定义协议的包，然后再把包转为 buffer 写到客户端）。简单地实践一下可以证明这个模式的有效性。不妨再看看这个图，希望对大伙以后写 server 有用：说明：本文内容基于 2011 年左右的技术背景整理（参考图片链接时间戳），文中涉及的 Jetty、Tomcat 及 Mina 版本架构可能已与当前最新版本存在差异。现代 NIO 框架（如 Netty）及 JDK NIO.2 已有更多演进，实际开发请以官方最新文档为准。
门面设计模式门面设计模式（Facade Pattern）在 Tomcat 中有多处应用，例如 Request 和 Response 对象的封装、Standard Wrapper 到 ServletConfig 的封装、以及 ApplicationContext 到 ServletContext 的封装等。门面设计模式的原理门面设计模式的核心作用在于封装。顾名思义，就是将复杂的内部结构封装成一个统一的“门面”，以便与其他模块更便捷地进行交互，类似于国家外交部统一对外接口的角色。该模式主要应用于由多个子系统组成的大型系统中。子系统之间必然涉及相互通信，但每个子系统不应将内部数据过多地暴露给其他系统，否则划分子系统的意义将大打折扣。因此，每个子系统都会设计一个门面，将外部感兴趣的数据封装起来，并通过这个门面提供访问接口。这就是门面设计模式存在的意义。门面设计模式示意图如下：客户端（Client）仅能访问 Facade 提供的数据，这是门面设计模式的关键所在。至于 Client 如何访问 Facade，以及 Subsystem 如何提供 Facade，该模式并没有严格规定。Tomcat 的门面设计模式示例Tomcat 中包含众多不同组件，组件间需要相互交互数据，使用门面模式隔离数据是一种有效的方法。以下是 Request 对象上使用的门面设计模式示例：从类图可以看出，HttpRequestFacade 类封装了 HttpRequest 接口能够提供的数据。通过 HttpRequestFacade 访问到的数据都被代理到 HttpRequest 中。通常被封装的对象会被设为 Private 或者 Protected 访问修饰符，以防止在 Facade 之外被直接访问。观察者设计模式观察者设计模式（Observer Pattern）是一种常用的设计方法，通常也称为发布 - 订阅模式（Publish-Subscribe Pattern）或事件监听机制。它通常在某个事件发生的前后触发一些特定操作。观察者模式的原理观察者模式的原理相对简单：当主体（Subject）状态发生变化时，依赖该主体的观察者（Observer）会收到通知并执行相应操作。观察者必须向主体注册（登记），否则主体无法通知它。观察者模式通常包含以下几个角色：Subject（抽象主题）：负责管理所有观察者的引用，同时定义主要的事件操作。ConcreteSubject（具体主题）：实现抽象主题定义的所有接口，当自身状态发生变化时，通知所有观察者。Observer（观察者）：定义监听主题发生变化时的相应操作接口。Tomcat 的观察者模式示例Tomcat 中多处使用了观察者模式。前面提到的控制组件生命周期的 Lifecycle 接口就是这种模式的体现，此外 Servlet 实例的创建、Session 的管理、Container 的管理等都采用了同样的原理。下面主要看一下 Lifecycle 的具体实现。Lifecycle 的观察者模式结构图如下：在上述结构图中：LifecycleListener 代表抽象观察者，它定义了一个 lifecycleEvent 方法，这是当主题变化时要执行的方法。ServerLifecycleListener 代表具体观察者，它实现了 LifecycleListener 接口的方法。Lifecycle 接口代表抽象主题，它定义了管理观察者的方法以及其他相关方法。StandardServer 代表具体主题，它实现了抽象主题的所有方法。Tomcat 对观察者模式做了扩展，增加了两个辅助类：LifecycleSupport 和 LifecycleEvent。LifecycleEvent 使得可以定义事件类别，不同的事件可区别处理，更加灵活。LifecycleSupport 类代理了主题对多观察者的管理，将这个管理逻辑抽出来统一实现。以后如果需要修改，只需修改 LifecycleSupport 类，而不需要修改所有具体主题，因为所有具体主题对观察者的操作都被代理给了 LifecycleSupport 类。这可以认为是观察者模式的一种改进版。LifecycleSupport 调用观察者的方法代码如下：public void fireLifecycleEvent(String type, Object data) { LifecycleEvent event = new LifecycleEvent(lifecycle, type, data); LifecycleListener interested[] = null; synchronized (listeners) { interested = (LifecycleListener[]) listeners.clone(); } for (int i = 0; i < interested.length; i++) { interested[i].lifecycleEvent(event); } }主题是如何通知观察者的呢？请看以下代码示例：public void start() throws LifecycleException { lifecycle.fireLifecycleEvent(BEFORE_START_EVENT, null); lifecycle.fireLifecycleEvent(START_EVENT, null); started = true; synchronized (services) { for (int i = 0; i < services.length; i++) { if (services[i] instanceof Lifecycle) { ((Lifecycle) services[i]).start(); } } } lifecycle.fireLifecycleEvent(AFTER_START_EVENT, null); }命令设计模式在 Tomcat 架构中，核心组件 Connector 和 Container 的交互体现了命令设计模式。Connector 将接收到的请求封装为命令，传递给 Container 进行处理。命令模式的原理命令模式（Command Pattern）的主要作用是将命令封装，把发出命令的责任和执行命令的责任分离开，实现功能分工。不同的模块可以对同一个命令做出不同的解释。命令模式通常包含以下几个角色：Client（客户端）：创建一个命令，并决定接受者。Command（命令）：命令接口，定义一个抽象方法。ConcreteCommand（具体命令）：负责调用接受者的相应操作。Invoker（请求者）：负责调用命令对象执行请求。Receiver（接受者）：负责具体实施和执行一次请求。Tomcat 中的命令模式示例Tomcat 作为应用服务器，会接收到很多请求，如何分配和执行这些请求是核心功能之一。命令模式在 Connector 和 Container 组件之间得到了体现。以下是 Tomcat 命令模式的结构图：在此结构中：Connector 作为抽象请求者，HttpConnector 作为具体请求者。HttpProcessor 作为命令。Container 作为命令的抽象接受者，ContainerBase 作为具体的接受者。客户端即应用服务器 Server 组件。Server 首先创建命令请求者 HttpConnector 对象，然后创建命令 HttpProcessor 对象，再把命令对象交给命令接受者 ContainerBase 容器来处理。命令最终是被 Tomcat 的 Container 执行的。命令可以以队列的方式进入，Container 也可以以不同的方式来处理请求，例如 HTTP/1.0 协议和 HTTP/1.1 的处理方式会有所不同。责任链模式责任链模式（Chain of Responsibility Pattern）是 Tomcat 中最容易发现的设计模式之一，也是 Tomcat 中 Container 设计的基础。整个容器通过一条链连接在一起，这条链一直将请求正确地传递给最终处理请求的 Servlet。责任链模式的原理责任链模式将很多对象通过每个对象对其下家的引用连接起来形成一条链。请求在这条链上传递，直到链上的某个对象处理此请求；或者每个对象都可以处理请求，并传给下一家，直到最终链上每个对象都处理完。这样可以在不影响客户端的情况下，在链上增加任意的处理节点。通常责任链模式包含以下几个角色：Handler（抽象处理者）：定义一个处理请求的接口。ConcreteHandler（具体处理者）：处理请求的具体类，或者传给下家。Tomcat 中责任链模式示例在 Tomcat 中，这种设计模式几乎被完整地使用。Tomcat 的容器设置就是责任链模式，从 Engine 到 Host，再到 Context，一直到 Wrapper，都是通过一个链传递请求。Tomcat 中责任链模式的类结构图如下：上图基本描述了四个子容器使用责任链模式的类结构。对应的责任链模式角色中，Container 扮演抽象处理者角色，具体处理者由 StandardEngine 等子容器扮演。与标准的责任链不同的是，这里引入了 Pipeline 和 Valve 接口。它们有什么作用呢？实际上，Pipeline 和 Valve 扩展了这个链的功能，使得在链往下传递过程中，能够接受外界的干预。Pipeline 如同连接每个子容器的管道，里面传递的 Request 和 Response 对象好比管子里流的水；而 Valve 则是这个管子上开的一个个小口子（阀门），让你有机会能够接触到里面的水，做一些额外的事情。为了防止水被引出来而不能流到下一个容器中，每一段管子最后总有一个节点保证它一定能流到下一个子容器。所以每个容器都有一个 StandardXXXValve。只要涉及到这种链式处理流程，这是一个非常值得借鉴的模式。说明：本文涉及的部分类名（如 HttpConnector、HttpProcessor）主要基于 Tomcat 5/6 版本架构。在 Tomcat 7 及更高版本中，Connector 和 Container 的内部实现有所调整（例如移除了 HttpConnector），但整体设计模式思想（门面、观察者、命令、责任链）依然适用。
引言在前端优化的第一部分中，主要讲解了对静态资源的优化措施，包括图片压缩、CSS Sprites 技术、GZIP 压缩等。本文作为系列的第二部分，将深入讲解前端优化中关键的 Flush 机制、动静分离、HTTP 持久连接、HTTP 协议的灵活应用以及 CDN 等技术。结合这些技术策略，旨在进一步提升 Java Web 应用程序的整体性能。Flush 机制的使用在 Web 技术中，Flush 机制并非新概念。其核心思想是无需等待网页内容全部加载完毕再一次性写回客户端，而是可以分部分逐次返回。对于内容较大的网页，一次性写回全部内容会导致用户长时间面对空白页面，体验较差。Flush 机制允许开发人员将网页内容按文档流顺序逐步返回给客户端，使用户感知到系统正在工作，从而提升等待过程中的用户体验。这是一个经典的优化手段，至今仍在广泛使用。在 Java Web 技术中，实现 Flush 非常简单。只需调用 HttpServletResponse.getWriter() 输出流的 flush() 方法，即可将已经加载完成的内容写回给客户端。然而，是否每个网页都需要使用该技术？并不建议。将网页内容加载完毕后一次性返回客户端也有其优势。网络传输存在最大传输单元（MTU），内容加载完毕后一次性输出可以最大程度地利用传输带宽，减少分块和传输次数。换言之，Flush 机制可能会略微增加用户等待时间和浏览器渲染时间。但对于大型网页而言，牺牲这点效率来换取用户体验的提升，通常是值得的。动静分离所谓的动静分离，就是将 Web 应用程序中的静态内容和动态内容分别部署在不同的 Web 服务器上，针对性地处理不同类型的请求，从而达到性能提升的目的。本文基于 Java Web 环境讲解优化，主流服务器软件为 Tomcat。需要注意的是，Tomcat 在高并发和静态资源处理能力上相对较弱。既然选择了 Java Web，我们就应通过架构优化来弥补不足。动静分离便是其中一种有效方法：将静态资源的处理任务交给更擅长的软件，而让 Tomcat 专注于处理 JSP/Servlet 动态请求。服务器软件选择对于静态资源处理，可以选择 Nginx。这是一款高性能的 HTTP 和反向代理服务器，支持高并发，对静态资源的处理能力较强。事实上，动静分离的方案多种多样：有人采用 Apache+Tomcat 组合，也有人使用 Tomcat+Tomcat 组合（部署于不同主机或域名）。其中 Apache+Tomcat 方案与 Nginx 方案原理相似，均基于反向代理。相比之下，Nginx 配置动静分离更为简洁，而 Apache 则需要配置 mod_proxy 模块。Apache 配置示例在 Apache 中，mod_proxy 模块负责反向代理的实现。核心配置内容如清单 1所示（该配置源自实际项目经验）。清单 1. 动静分离的 Apache 核心配置<Proxy balancer://proxy> BalancerMember http://192.168.1.178:8080 loadfactor=1 BalancerMember http://192.168.1.145:8080 loadfactor=1 </Proxy> NameVirtualHost *:80 <VirtualHost *:80> ServerAdmin service@xuanli365.com ServerName www.xuanli365.com DocumentRoot /www DirectoryIndex index.shtml <Directory /www> AllowOverride All AddType text/html .shtml AddType application/x-rar .rar AddHandler server-parsed .shtml Options +IncludesNOEXEC </Directory> RewriteEngine on ProxyRequests Off ProxyPass /static/! ProxyPass / balancer://proxy/ ProxyPassReverse / balancer://proxy/ ProxyPreserveHost on </VirtualHost>根据 Apache 官方对 mod_proxy 模块的介绍，ProxyPass 属性可以将远端服务器映射到本地服务器的 URL 空间中，实现地址映射。在清单 1的配置中：当访问路径不在 /static/ 下时（! 表示非），请求转发给后端服务器（即 Tomcat）。当访问路径在 /static/ 下时，直接访问本机。例如，访问 www.xuanli365.com/static/css/index.css 时，实际处理请求的是 Apache 服务器；而访问 www.xuanli365.com/index.jsp 时，Apache 会将请求转发到后端的 Tomcat 服务器（如 http://192.168.1.178:8080/index.jsp）。这就实现了动静分离。此外，该配置中也包含了简单的负载均衡（通过 loadfactor 因子）。大型网站案例分析打开任意大型门户网站（如腾讯网站），查看图片资源地址，可能会发现类似以下域名：http://mat1.gtimg.com/www/iskin960/qqcomlogo.pnghttp://img1.gtimg.com/v/pics/hv1/95/225/832/54158270.jpg可见该网站存放图片资源使用了多个域名。使用 Linux 的 host 命令查看这两个域名的 IP 地址，结果如图 1所示。图 1. 某网站的动静分离通过查看 IP 地址可以发现，这些图片很可能存放在不同的主机上（注：一个主机可以拥有多个 IP）。图片内容和网页的动态内容不在同一 IP 下，这也是动静分离的体现。使用多个域名可以增加浏览器的并发下载数，从而提高下载效率。本文演示架构本文采用另一种策略对动静分离进行演示，大致结构如图 2所示。图 2. 本文设计的动静分离结构在该架构中：A 主机：存放静态资源，安装 Nginx。B 主机：存放动态程序，安装 Tomcat。配置 Nginx 后，当请求的是 html、jpg 等静态资源时，直接访问 A 主机上的静态资源目录；当用户提出动态资源请求时，则将请求转发到后端的 B 服务器，交由 Tomcat 处理，再由 Nginx 将结果返回给客户端。可能会有疑问：动态请求要先访问 A，A 转发访问 B，再由 B 返回结果给 A，A 最后又将结果返回给客户端，是否多余？初看的确如此，但这种结构至少有两点好处：为负载均衡做准备：随着系统发展，单台 B 主机处理动态请求可能不足。基于图 2 的结构，可以直接扩展 B1、B2 等节点，只需修改 Nginx 配置即可实现负载均衡。便于程序开发与维护：基于该结构，程序代码中只需写相对路径（如 ），无需关心具体资源服务器的地址。若静态资源扩展到其他服务器，只需调整 Nginx 配置，无需修改代码。Nginx 配置示例按照图 2 所示的架构图，安装好 Nginx 和 Tomcat 后，对 nginx.conf 进行配置。与本文该部分相关的配置如清单 2所示。清单 2. 动静分离的 Nginx 配置# 转发的服务器，upstream 为负载均衡做准备 upstream tomcat_server { server 192.168.1.117:8080; } server { listen 9090; server_name localhost; index index.html index.htm index.jsp; charset koi8-r; # 静态资源存放目录 root /home/wq243221863/Desktop/ROOT; access_log logs/host.access.log main; # 动态请求的转发 location ~ .*.jsp$ { proxy_pass http://tomcat_server; proxy_set_header Host $host; } # 静态请求直接读取 location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|css)$ { expires 30d; } …… }清单 2配置简洁明了：动态请求（以 .jsp 结尾）发到 B 主机（192.168.1.117:8080），静态请求（gif|jpg 等）则直接访问定义的 root 目录。测试环境搭建接下来在 Tomcat 中新建 Web 项目，添加一个 test.jsp 文件，目录结构如图 3所示。图 3. B 上的测试项目结构定义一张测试用的静态图片，放置在 A 主机的桌面 ROOT/seperate 目录下。结构如图 4所示。图 4. A 上的静态资源文件夹结构注意：这里的 separate 目录名是与 B 的项目文件夹同名的。查看图 3 中 test.jsp 的源码，如清单 3所示。清单 3. test.jsp 源码<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ page import="java.util.Date" %> <%@ page import="java.text.SimpleDateFormat" %> 动静分离的测试 这是动态脚本处理的结果 <% // 这是一段测试的动态脚本 Date now=new Date(); SimpleDateFormat f=new SimpleDateFormat("现在是"+"yyyy 年 MM 月 dd 日 E kk 点 mm 分"); %> <%=f.format(now)%> 这是静态资源的请求结果 清单 3是一个非常简单的 JSP 页面，主要使用 img 标签来访问 jquery.gif。已知 test.jsp 在 B 服务器上，而 jquery.gif 在 A 服务器上。用于访问 jquery.gif 的代码里不需要指定 A 的地址，而是直接使用相对路径，就好像该图片也在 B 上一样，这就是本结构的优点。在 A 上访问 test.jsp 文件，结果如图 5所示。图 5. test.jsp 的结果配置顺利，完全按照预期实现了动静分离。性能测试对比初步完成动静分离配置后，我们需要验证其性能提升效果。将 Tomcat 服务器也迁移到 A 服务器上，同时将 jquery.gif 拷贝一份到 separate 项目目录下，图 3 的结构变为图 6所示。图 6. 拷贝 jquery.gif 的 separate 项目将 Tomcat 端口设置为 8080，Nginx 端口依然是 9090。现在：访问 http://localhost:9090/separate/test.jsp（未使用动静分离，静态资源由 Tomcat 处理）。访问 http://localhost:8080/separate/test.jsp（使用了动静分离，静态资源由 Nginx 处理）。注：原文此处端口描述可能存在笔误，根据上下文逻辑，9090 应为 Nginx 代理端口，8080 为 Tomcat 直连端口。测试时分别对比 Nginx 代理下的静态资源访问与 Tomcat 直连的静态资源访问。使用 Apache 的 AB 压力测试工具，对相关 URL 分别进行压力和吞吐率测试。1. 静态资源测试首先，对静态资源（jquery.gif）的处理结果如清单 4所示。清单 4. 静态资源的 AB 测试测试脚本：ab -c 100 -n 1000 http://localhost:{port}/seperate/jquery.gif 9090 端口，也就是 Nginx 的测试结果： Concurrency Level: 100 Time taken for tests: 0.441 seconds Complete requests: 1000 Failed requests: 0 Write errors: 0 Total transferred: 4497000 bytes HTML transferred: 4213000 bytes Requests per second: 2267.92 [#/sec] (mean) Time per request: 44.093 [ms] (mean) Time per request: 0.441 [ms] (mean, across all concurrent requests) Transfer rate: 9959.82 [Kbytes/sec] received 8080 端口，也就是 Tomcat 的测试结果： Concurrency Level: 100 Time taken for tests: 1.869 seconds Complete requests: 1000 Failed requests: 0 Write errors: 0 Total transferred: 4460000 bytes HTML transferred: 4213000 bytes Requests per second: 535.12 [#/sec] (mean) Time per request: 186.875 [ms] (mean) Time per request: 1.869 [ms] (mean, across all concurrent requests) Transfer rate: 2330.69 [Kbytes/sec] received清单 4的测试脚本代表同时处理 100 个请求并下载 1000 次 jquery.gif 文件。关注 Requests per second（吞吐率）指标可以看出 Nginx 实现动静分离的优势：动静分离每秒可以处理 2267 个请求，而不使用则只可以处理 535 个请求。由此可见，动静分离后效率提升显著。2. 动态脚本测试动态请求的转发会导致动态脚本的处理效率降低吗？降低多少？再用 AB 工具对 test.jsp 进行测试，结果如清单 5所示。清单 5. 动态脚本的 AB 测试测试脚本：ab -c 1000 -n 1000 http://localhost:{port}/seperate/test.jsp 9090 端口，也就是 Nginx 的测试结果： Concurrency Level: 100 Time taken for tests: 0.420 seconds Complete requests: 1000 Failed requests: 0 Write errors: 0 Total transferred: 709000 bytes HTML transferred: 469000 bytes Requests per second: 2380.97 [#/sec] (mean) Time per request: 42.000 [ms] (mean) Time per request: 0.420 [ms] (mean, across all concurrent requests) Transfer rate: 1648.54 [Kbytes/sec] received 8080 端口，也就是 Tomcat 的测试结果： Concurrency Level: 100 Time taken for tests: 0.376 seconds Complete requests: 1000 Failed requests: 0 Write errors: 0 Total transferred: 714000 bytes HTML transferred: 469000 bytes Requests per second: 2660.06 [#/sec] (mean) Time per request: 37.593 [ms] (mean) Time per request: 0.376 [ms] (mean, across all concurrent requests) Transfer rate: 1854.77 [Kbytes/sec] received经过多次测试，得出清单 5的较为稳定的结果。可以看到在使用 Nginx 实现动静分离以后，动态请求的吞吐率的确会有所下降（因为多了一层转发）。然而，对于网站整体性能来说，静态资源的高吞吐率，以及未来可以实现的负载均衡、可扩展性、高可用性等优势，该牺牲是值得的。任何技术都是有利有弊，动静分离也不例外。选择了动静分离，就意味着选择了更为复杂的系统架构，维护成本在一定程度上会增加。但鉴于其带来的性能提升，这仍是很多系统架构师会选择的一种解决方案。HTTP 持久连接持久连接（Keep-Alive）也叫做长连接，它是一种 TCP 连接方式。连接会被浏览器和服务器所缓存，在下次连接同一服务器时，缓存的连接被重新使用。由于 HTTP 的无状态性，传统的 HTTP 通信往往是“一次性”的。持久连接减少了创建连接的开销，提高了性能。HTTP/1.1 已经支持长连接，大部分浏览器和服务器也提供了支持。要想发起长连接，服务器和浏览器必须共同合作。一方面浏览器要保持连接，另一方面服务器也不会断开连接。也就是说，双方需要进行协商，而协商机制依赖于 HTTP 协议。协商结构如图 7所示。图 7. 长连接协商浏览器在请求头部添加 Connection: Keep-Alive，告诉服务器“我支持长连接”。倘若服务器支持，则在响应头部添加 Connection: Keep-Alive，确认建立长连接。服务器还可以通过 Keep-Alive: timeout=10, max=100 头部告诉浏览器超时时间和最大请求数。Tomcat 配置在 Tomcat 里允许配置长连接。编辑 conf/server.xml 文件，配置 Connector 节点。该节点负责控制浏览器与 Tomcat 的连接，其中与长连接直接相关的有两个属性：keepAliveTimeout：表示在 Connector 关闭连接前，为另外一个请求 Keep Alive 所等待的毫秒数。默认值和 connectionTimeout 一样。maxKeepAliveRequests：表示 HTTP/1.0 Keep Alive 和 HTTP/1.1 Keep Alive / Pipeline 的最大请求数目。如果设置为 1，将会禁用 Keep Alive 和 Pipeline；如果设置为小于 0 的数，Keep Alive 的最大请求数将没有限制。也就是说在 Tomcat 里，默认长连接是打开的。当我们想关闭长连接时，只要将 maxKeepAliveRequests 设置为 1 即可。配置验证首先将 maxKeepAliveRequests 设置为 20，keepAliveTimeout 为 10000，通过 Firefox 查看请求头部（访问上面提到的 test.jsp）。结果如图 8所示。图 8. 服务器打开长连接接下来，将 maxKeepAliveRequests 设置为 1，并且重启服务器，再次请求网页后查看的结果如图 9所示。图 9. 服务器关闭长连接对比可以发现，Tomcat 关闭长连接后，在服务器的请求响应中明确标识了：Connection: close，告诉浏览器服务器并不支持长连接。性能测试长连接究竟可以带来怎样的性能提升？我们用数据说话。依然使用 AB 工具，它可以使用 -k 参数模拟浏览器使用 HTTP 的 Keep-Alive 特性。对 http://localhost:8080/seperate/jquery.gif 进行测试，结果如清单 6所示。清单 6. AB 测试长连接测试脚本：ab -k -c 1000 -n 10000 http://localhost:8080/seperate/jquery.gif 关闭长连接时： Concurrency Level: 1000 Time taken for tests: 5.067 seconds Complete requests: 10000 Failed requests: 0 Write errors: 0 Keep-Alive requests: 0 Total transferred: 44600000 bytes HTML transferred: 42130000 bytes Requests per second: 1973.64 [#/sec] (mean) Time per request: 506.678 [ms] (mean) Time per request: 0.507 [ms] (mean, across all concurrent requests) Transfer rate: 8596.13 [Kbytes/sec] received 打开长连接时，maxKeepAliveRequests 设置为 50： Concurrency Level: 1000 Time taken for tests: 1.671 seconds Complete requests: 10000 Failed requests: 0 Write errors: 0 Keep-Alive requests: 10000 Total transferred: 44650000 bytes HTML transferred: 42130000 bytes Requests per second: 5983.77 [#/sec] (mean) Time per request: 167.119 [ms] (mean) Time per request: 0.167 [ms] (mean, across all concurrent requests) Transfer rate: 26091.33 [Kbytes/sec] received结果令人惊讶。使用长连接和不使用长连接的性能对比，对于 Tomcat 配置的 maxKeepAliveRequests 为 50 来说，性能提升了将近 5 倍。可见服务器默认打开长连接是有原因的。HTTP 协议的合理使用很多程序员将精力专注在了技术实现上，认为性能的高低完全取决于代码的实现，却忽略了已经成型的规范、协议和工具。最典型的就是在 Web 开发上，部分开发人员没有意识到 HTTP 协议的重要性。其实，HTTP 协议可以提供另一条性能优化之路。通过简单地在 JSP 的 request 对象中添加响应头部，往往可以迅速提升程序性能。本系列文章的宗旨也在于让程序员编最少的代码，提升最大的性能。本文提出一个需求：在前面部分提到的 test.jsp 中，它的一部分功能是显示服务器的当前时间。现在我们希望这个动态网页允许被浏览器缓存。这似乎有点不合理，但在很多场景下，虽然是动态网页，却只执行一次（例如网页的主菜单存入数据库，不希望每次加载菜单都去读数据库）。浏览器缓存带来的性能提升众人皆知，而缓存过期时间、缓存删除、什么页面可以缓存等，都可以由程序员控制。只要熟悉 HTTP 协议，就可以轻松控制浏览器。访问上面提及的 test.jsp，用 Firebug 查看请求情况，发现每次请求都会重新到服务器下载内容。这不难理解，因为 test.jsp 是动态内容，每次服务器必须执行后才可以返回结果。图 10是访问当前的 test.jsp 的头部情况。图 10. 修改 test.jsp 前的访问头部情况现在我们往 test.jsp 添加清单 7的内容。清单 7. 在 test.jsp 的首部添加的代码<% SimpleDateFormat f2=new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss"); String ims = request.getHeader("If-Modified-Since"); if (ims != null) { try { Date dt = f2.parse(ims.substring(0, ims.length()-4)); if (dt.after(new Date(2009, 1, 1))) { response.setStatus(304); return; } } catch(Exception e) { } } response.setHeader("Last-Modified", f2.format(new Date(2010, 5, 5)) + " GMT"); %>上述代码的意图是：服务器获得浏览器请求头部中的 If-Modified-Since 时间。这个时间是浏览器询问服务器，它所请求的资源是否过期。如果没过期就返回 304 状态码，告诉浏览器直接使用本地的缓存。修改完 test.jsp 代码后，使用鼠标激活浏览器地址栏，按下回车刷新页面。这次的结果如图 11所示。图 11. 修改 test.jsp 后的首次访问可以看到图 11和图 10的请求报头没有区别，而在服务器的响应中，图 11增加了 Last-Modified 头部，这个头部告诉浏览器可以将此页面缓存。按下 F5（必须是 F5 刷新），F5 会强制 Firefox 加载服务器内容，并且发出 If-Modified-Since 头部。得到的报头结果如图 12所示。图 12. 修改 test.jsp 后的再次访问可以看到，图 12的底部已经提示所有内容都来自缓存。浏览器的请求头部多出了 If-Modified-Since，以此询问服务器从缓存时间起，服务器是否对资源进行了修改。服务器判断后发现没有对此资源（test.jsp）修改，就返回 304 状态码，告诉浏览器可以使用缓存。我们在上面的实验中，用到了 HTTP 协议的相关知识，其中涉及了 If-Modified-Since、Last-Modified、304 状态码等。事实上与缓存相关的 HTTP 头部还有许多，诸如过期设置的头部等。熟悉了 HTTP 头部，就如同学会了如何与用户的浏览器交谈，也可以利用协议提升您的程序性能。那么对于 test.jsp 这个小网页来说，基于缓存的方案提升了多少性能呢？我们用 AB 给您答案。AB 是个很强大的工具，它提供了 -H 参数，允许测试人员手动添加 HTTP 请求头部。测试结果如清单 8所示。清单 8. AB 测试 HTTP 缓存测试脚本：ab -c 1000 – n 10000 – H ' If-Modified-Since: Sun, 05 Jun 3910 00:00:00 GMT ' http://localhost:8080/seperate/test.jsp 未修改 test.jsp 前 : Document Path: /seperate/test.jsp Document Length: 362 bytes Concurrency Level: 1000 Time taken for tests: 10.467 seconds Complete requests: 10000 Failed requests: 0 Write errors: 0 Total transferred: 6080000 bytes HTML transferred: 3630000 bytes Requests per second: 955.42 [#/sec] (mean) Time per request: 1046.665 [ms] (mean) Time per request: 1.047 [ms] (mean, across all concurrent requests) Transfer rate: 567.28 [Kbytes/sec] received 修改 test.jsp 后： Document Path: /seperate/test.jsp Document Length: 0 bytes Concurrency Level: 1000 Time taken for tests: 3.535 seconds Complete requests: 10000 Failed requests: 0 Write errors: 0 Non-2xx responses: 10000 Total transferred: 1950000 bytes HTML transferred: 0 bytes Requests per second: 2829.20 [#/sec] (mean) Time per request: 353.457 [ms] (mean) Time per request: 0.353 [ms] (mean, across all concurrent requests) Transfer rate: 538.76 [Kbytes/sec] received分别对比 Document Length、Requests per second 以及 Transfer rate 这三个指标。可以发现没使用缓存的 Document Length（下载内容的长度）是 362 字节，而使用了缓存的长度为 0。在吞吐率方面，使用缓存是不使用缓存的 3 倍左右。同时在传输率方面，缓存的传输率比没缓存的小。这些都是用到了客户端缓存的缘故。CDN 的使用CDN（Content Delivery Network，内容分发网络）也是笔者最近才了解和接触到的技术。在淘宝的前端技术报告上、在一个好朋友的创新工场创业之路上，我都听到了这个词，因此有必要对此技术了解一下。所谓的 CDN，就是一种内容分发网络。它采用智能路由和流量管理技术，及时发现能够给访问者提供最快响应的加速节点，并将访问者的请求导向到该加速节点，由该加速节点提供内容服务。利用内容分发与复制机制，CDN 客户不需要改动原来的网站结构，只需修改少量的 DNS 配置，就可以加速网络的响应速度。当用户访问了使用 CDN 服务的网站时，DNS 域名服务器通过 CNAME 方式将最终域名请求重定向到 CDN 系统中的智能 DNS 负载均衡系统。智能 DNS 负载均衡系统通过一组预先定义好的策略（如内容类型、地理区域、网络负载状况等），将当时能够最快响应用户的节点地址提供给用户，使用户可以得到快速的服务。同时，它还与分布在不同地点的所有 CDN 节点保持通信，搜集各节点的健康状态，确保不将用户的请求分配到任何一个已经不可用的节点上。而我们的 CDN 还具有在网络拥塞和失效情况下，能拥有自适应调整路由的能力。由于笔者对 CDN 没有亲身实践，不便多加讲解，但是各大网站都在一定程度使用到了 CDN，淘宝的前端技术演讲中就提及了 CDN，可见 CDN 的威力不一般。图 13. 淘宝的 CDN 前端优化因此 CDN 也是不得不提的一项技术。国内有免费提供 CDN 服务的网站：http://www.webluker.com/，它需要您有备案的域名，感兴趣的您可以去试试。小结本文总结了 HTTP 长连接、动静分离、HTTP 协议等等。在您需要的时候，可以查看本文的内容，相信按照本文的方法，可以辅助您进行前端的高性能优化。笔者将继续写后续的部分，包括数据库的优化、负载均衡、反向代理等。由于笔者水平有限，如有错误，请联系我批评指正。接下来在第三部分文章中，我将介绍服务器端缓存、静态化与伪静态化、分布式缓存等，并且将它们应用到 Java Web 的开发中。使用这些技术可以帮助提高 Java Web 应用程序的性能。*相关主题查看本系列的 第 1 部分：本系列文章将讲述如何提高 Java Web 应用的性能，同时用实际的工具、实际的数据来对比被优化前后的 Java Web 程序。本文主要讲解网页前端的性能优化，重点放在了对于图片使用的优化方面，如图片压缩、CSS Sprites 等方法的介绍。查看"深入理解 HTTP 协议"文章，了解 HTTP 协议，更加理解 HTTP 的含义。查看"利用 Nginx 实现动静分离和负载均衡"，了解如何实现动静分离和负载均衡。查看"developerWorks 的 Web 开发专题"，可以看看顶尖程序员写的文章。查看"Content delivery network"，维基百科对 CDN 的定义。查看"JSP 的缓存技术"，了解 JSP 技术对缓存的多种控制方法。查看 "Apache 反向代理 Mod_Proxy", 了解 Apache 对反向代理的支持。说明：本文部分内容源自 IBM DeveloperWorks 早期文章，部分外部链接可能已失效或内容过时（如 Tomcat 版本配置、CDN 服务提供商等）。文中涉及的技术原理（如动静分离、HTTP 缓存、Keep-Alive）依然适用，但具体配置参数请参考当前使用的软件版本官方文档。
Tomcat 中 Servlet 的创建初始化体系结构及工作一个 Web 应用对应一个 Context 容器。在 Tomcat 中，Context 容器是管理 Servlet 的核心，也是 Servlet 运行时的容器。当添加一个 Web 应用时，Tomcat 会创建一个 StandardContext 容器，并为其设置必要的参数。其中，url 和 path 分别代表该应用在 Tomcat 中的访问路径和实际的物理路径。Web 应用的初始化工作Tomcat 中 Web 应用的初始化工作主要在 ContextConfig 的 configureStart 方法中实现。初始化的核心任务是解析 web.xml 文件，该文件描述了 Web 应用的关键信息，是应用的入口。解析过程如下：查找全局配置：Tomcat 首先查找 globalWebXml，该文件位于 org/apache/catalina/startup/NO_DEFAULT_XML 或 conf/web.xml 中的任一个。查找宿主配置：接着查找 hostWebXml，即应用的配置文件（例如 examples/WEB-INF/web.xml）。解析与保存：web.xml 文件的各个配置项会被解析成相应的属性，保存在 WebXml 对象中。设置容器属性：随后将 WebXml 对象中的属性设置到 Context 容器中，包括创建 Servlet 对象、Filter、Listener 等。最后，Servlet 会被包装成 StandardWrapper 并作为子容器添加到 Context 中。因此，Context 容器才是真正运行 Servlet 的容器，其配置属性由应用的 web.xml 指定。创建 Servlet 对象如果 Servlet 的 load-on-startup 配置项大于 0，那么在 Context 容器启动时就会被实例化。前文提到，在解析配置文件时会读取默认的 globalWebXml。在 conf 下的 web.xml 文件中定义了一些默认配置项，其中定义了两个 Servlet：org.apache.catalina.servlets.DefaultServletorg.apache.jasper.servlet.JspServlet它们的 load-on-startup 分别是 1 和 3。这意味着当 Tomcat 启动时，这两个 Servlet 会被优先启动。JspServlet 主要用于将 JSP 页面转换成 Servlet，它会拦截所有以 .jsp 为后缀的文件。创建 Servlet 实例的方法是从 Wrapper.loadServlet 开始的。loadServlet 方法的主要任务是获取 servletClass，然后将其交给 InstanceManager 去创建一个基于 servletClass.class 的对象。初始化 Servlet由于 Servlet 在 Web 初始化时已经包装在 StandardWrapper 中，因此 Servlet 的初始化是在 StandardWrapper 的 initServlet 方法中进行的。该方法逻辑简单，主要是调用 Servlet 的 init 方法，同时将包装了 StandardWrapper 对象的 StandardWrapperFacade 作为 ServletConfig 传给 Servlet。如果该 Servlet 关联的是一个 JSP 文件，那么前面初始化的就是 JspServlet。接下来会模拟一次简单请求，调用这个 JSP 文件，以便将其编译为 class 文件，并初始化这个 class。Servlet 体系结构Java Web 应用是基于 Servlet 规范运转的。下面介绍一下 Servlet 的体系结构。顶层类关联从上图可以看出，Servlet 规范主要基于几个核心类运转。与 Servlet 主动关联的是三个类，分别是 ServletConfig、ServletRequest 和 ServletResponse。这三个类都是通过容器传递给 Servlet 的：ServletConfig：在 Servlet 初始化时传给 Servlet，主要用于获取 Servlet 的配置属性，这些属性可能在 Servlet 运行时被用到。ServletRequest 和 ServletResponse：在请求到达调用 Servlet 时传递过来。它们代表了请求和响应的具体对象，通常作为运输工具来传递交互结果。Servlet 的运行模式是一个典型的“握手型的交互式”运行模式。所谓“握手型的交互式”，是指两个模块为了交换数据通常都会准备一个交易场景，这个场景一直跟随交易过程直到交易完成。交易场景的初始化是根据这次交易对象指定的参数来定制的，这些指定参数通常是一个配置类。交易场景由 ServletContext 来描述。定制的参数集合由 ServletConfig 来描述。容器中的类关系下图是 ServletConfig 和 ServletContext 在 Tomcat 容器中的类关系图。上图可以看出，StandardWrapper 和 StandardWrapperFacade 都实现了 ServletConfig 接口，而 StandardWrapperFacade 是 StandardWrapper 的门面类。因此，传给 Servlet 的是 StandardWrapperFacade 对象。这个类能够保证从 StandardWrapper 中拿到 ServletConfig 所规定的数据，而又不把 ServletConfig 不关心的数据暴露给 Servlet。同样，ServletContext 也与 ServletConfig 有类似的结构。Servlet 中能拿到的 ServletContext 的实际对象也是 ApplicationContextFacade 对象。ApplicationContextFacade 同样保证 ServletContext 只能从容器中拿到它该拿的数据。它们都起到了对数据的封装作用，使用的都是门面设计模式（Facade Pattern）。通过 ServletContext 可以拿到 Context 容器中的一些必要信息，比如应用的工作路径、容器支持的 Servlet 最小版本等。Request 相关类结构我们在创建自己的 Servlet 类时，通常使用的是 HttpServletRequest 和 HttpServletResponse，它们继承了 ServletRequest 和 ServletResponse。为何 Context 容器传过来的 ServletRequest、ServletResponse 可以被转化为 HttpServletRequest 和 HttpServletResponse 呢？Tomcat 一接收到请求，首先将会创建 org.apache.coyote.Request 和 org.apache.coyote.Response。这两个类是 Tomcat 内部使用的描述一次请求和相应信息的类，它们是轻量级的类。作用是在服务器接收到请求后，经过简单解析将这个请求快速地分配给后续线程去处理，所以它们的对象很小，很容易被 JVM 回收。接下来，当交给一个用户线程去处理这个请求时，又创建 org.apache.catalina.connector.Request 和 org.apache.catalina.connector.Response 对象。这两个对象一直穿越整个 Servlet 容器，直到要传给 Servlet。传给 Servlet 的是 Request 和 Response 的门面类 RequestFacade 和 ResponseFacade。这里使用门面模式与前面一样，都是基于同样的目的——封装容器中的数据。一次请求对应的 Request 和 Response 的类转化如下图所示：Servlet 如何工作当用户从浏览器向服务器发起一个请求，通常会包含如下信息：http://hostname:port/contextpath/servletpath。hostname 和 port：用来与服务器建立 TCP 连接。后面的 URL：用来选择服务器中那个子容器服务用户的请求。在 Tomcat 7.0 中，这种映射工作用一个专门的类来完成，即 org.apache.tomcat.util.http.mapper。这个类保存了 Tomcat 的 Container 容器中的所有子容器的信息。当 org.apache.catalina.connector.Request 类进入 Container 容器之前，mapper 将会根据这次请求的 hostname 和 contextpath 将 host 和 context 容器设置到 Request 的 mappingData 属性中。因此，当 Request 进入 Container 容器之前，它要访问哪个子容器就已经确定了。当 Request 请求到达了最终的 Wrapper 容器后，此时还需要真正的到达最终的 Servlet，这里还需要一些步骤：必须执行 Filter 链。通知你在 web.xml 中定义的 Listener。接下来就要执行 Servlet 的 service 方法了。通常情况下，我们自己定义的 Servlet 并不是直接去实现 javax.servlet.Servlet 接口，而是去继承更简单的 HttpServlet 类或者 GenericServlet 类，我们可以有选择地覆盖相应方法去实现我们要完成的工作。Servlet 的确已经能够帮我们完成所有的工作了，但是现在的 Web 应用很少有直接将交互全部页面都用 Servlet 来实现，而是采用更加高效的 MVC 框架 来实现。这些 MVC 框架基本的原理都是将所有的请求都映射到一个 Servlet，然后去实现 service 方法，这个方法也就是 MVC 框架的入口。当 Servlet 从 Servlet 容器中移除时，也就表明该 Servlet 的生命周期结束了。这时 Servlet 的 destroy 方法将被调用，做一些扫尾工作。说明：本文内容基于 Tomcat 7.0 版本架构整理，部分类名与内部实现机制在不同版本（如 Tomcat 8/9/10）中可能有所调整，仅供参考学习 Servlet 容器基本原理。
许多开源应用服务器都集成 Tomcat 作为 Web 容器（Web Container），且对于 Tomcat 的 Servlet 容器部分代码很少改动。因此，这些应用服务器的性能基本上取决于 Tomcat 处理 HTTP 请求的 Connector 模块的性能。本文首先从应用层次分析了 Tomcat 所有的 Connector 种类及用法，接着从架构上分析了 Connector 模块在整个 Tomcat 中所处的位置，最后对 Connector 做了详细的源代码分析。并且我们以 Http11NioProtocol 为例，详细说明了 Tomcat 是如何通过实现 ProtocolHandler 接口而构建 Connector 的。通过本文的学习，读者应该可以轻松做到将 Tomcat 作为 Web 容器集成到第三方系统，并且自定义任何想要的高性能 HTTP 连接器。1 Connector 介绍1.1 Connector 的种类Tomcat 源码中与 Connector 相关的类位于 org.apache.coyote 包中，Connector 分为以下几类：Http Connector：基于 HTTP 协议，负责建立 HTTP 连接。它又分为 BIO Http Connector 与 NIO Http Connector 两种，后者提供非阻塞 IO 与长连接 Comet 支持。AJP Connector：基于 AJP 协议。AJP 是专门设计用来为 Tomcat 与 HTTP 服务器之间通信定制的协议，能提供较高的通信速度和效率。如与 Apache 服务器集成时，采用这个协议。APR HTTP Connector：用 C 实现，通过 JNI 调用。主要提升对静态资源（如 HTML、图片、CSS、JS 等）的访问性能。现在这个库已独立出来可用在任何项目中。Tomcat 在配置 APR 之后性能非常强劲。1.2 Connector 的配置对 Connector 的配置位于 conf/server.xml 文件中。1.2.1 BIO HTTP/1.1 Connector 配置一个典型的配置如下：<Connector port="8080" protocol="HTTP/1.1" maxThreads="150" connectionTimeout="20000" redirectPort="8443" />其它一些重要属性如下：acceptCount：接受连接 request 的最大连接数目，默认值是 10。address：绑定 IP 地址，如果不绑定，默认将绑定任何 IP 地址。allowTrace：如果是 true，将允许 TRACE HTTP 方法。compressibleMimeTypes：各个 mimeType，以逗号分隔，如 text/html,text/xml。compression：如果带宽有限的话，可以用 GZIP 压缩。connectionTimeout：超时时间，默认为 60000ms (60s)。maxKeepAliveRequest：默认值是 100。maxThreads：处理请求的 Connector 的线程数目，默认值为 200。如果是 SSL 配置，如下：<Connector port="8181" protocol="HTTP/1.1" SSLEnabled="true" maxThreads="150" scheme="https" secure="true" clientAuth="false" sslProtocol="TLS" address="0.0.0.0" keystoreFile="E:/java/jonas-full-5.1.0-RC3/conf/keystore.jks" keystorePass="changeit" />其中，keystoreFile 为证书位置，keystorePass 为证书密码。1.2.2 NIO HTTP/1.1 Connector 配置<Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol" maxThreads="150" connectionTimeout="20000" redirectPort="8443" />1.2.3 Native APR Connector 配置APR 是用 C/C++ 写的，对静态资源（HTML，图片等）进行了优化。所以要下载本地库 tcnative-1.dll 与 openssl.exe，将其放在 %tomcat%\bin 目录下。下载地址是：http://tomcat.heanet.ie/native/1.1.10/binaries/win32/在 server.xml 中要配置一个 Listener，如下图。这个配置 Tomcat 是默认配好的。 <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />配置使用 APR connector<Connector port="8080" protocol="org.apache.coyote.http11.Http11AprProtocol" maxThreads="150" connectionTimeout="20000" redirectPort="8443" />如果配置成功，启动 Tomcat，会看到如下信息：org.apache.coyote.http11.Http11AprProtocol init2 Connector 在 Tomcat 中所处的位置2.1 Tomcat 架构图 2-1 Tomcat 架构Server (服务器)：是 Tomcat 构成的顶级构成元素，所有一切均包含在 Server 中，Server 的实现类 StandardServer 可以包含一个到多个 Services。Service：次顶级元素 Service 的实现类为 StandardService，调用了容器 (Container) 接口，其实是调用了 Servlet Engine (引擎)，而且 StandardService 类中也指明了该 Service 归属的 Server。Container (容器)：接下来次级的构成元素就是容器，主机 (Host)、上下文 (Context) 和引擎 (Engine) 均继承自 Container 接口，所以它们都是容器。但是，它们是有父子关系的。在主机 (Host)、上下文 (Context) 和引擎 (Engine) 这三类容器中，引擎是顶级容器，直接包含是主机容器，而主机容器又包含上下文容器。所以引擎、主机和上下文从大小上来说又构成父子关系，虽然它们都继承自 Container 接口。Connector (连接器)：将 Service 和 Container 连接起来。首先它需要注册到一个 Service，它的作用就是把来自客户端的请求转发到 Container (容器)，这就是它为什么称作连接器的原因。故我们从功能的角度将 Tomcat 源代码分成 5 个子模块，它们分别是：Jasper 子模块：这个子模块负责 JSP 页面的解析、JSP 属性的验证，同时也负责将 JSP 页面动态转换为 Java 代码并编译成 class 文件。在 Tomcat 源代码中，凡是属于 org.apache.jasper 包及其子包中的源代码都属于这个子模块。Servlet 和 JSP 规范的实现模块：这个子模块的源代码属于 javax.servlet 包及其子包，如我们非常熟悉的 javax.servlet.Servlet 接口、javax.servlet.http.HttpServlet 类及 javax.servlet.jsp.HttpJspPage 就位于这个子模块中。Catalina 子模块：这个子模块包含了所有以 org.apache.catalina 开头的 Java 源代码。该子模块的任务是规范了 Tomcat 的总体架构，定义了 Server、Service、Host、Connector、Context、Session 及 Cluster 等关键组件及这些组件的实现。这个子模块大量运用了 Composite 设计模式，同时也规范了 Catalina 的启动及停止等事件的执行流程。从代码阅读的角度看，这个子模块应该是我们阅读和学习的重点。Connectors 子模块：如果说上面三个子模块实现了 Tomcat 应用服务器的话，那么这个子模块就是 Web 服务器的实现。所谓连接器 (Connector) 就是一个连接客户和应用服务器的桥梁，它接收用户的请求，并把用户请求包装成标准的 Http 请求 (包含协议名称，请求头 Head，请求方法是 Get 还是 Post 等等)。同时，这个子模块还按照标准的 Http 协议，负责给客户端发送响应页面，比如在请求页面未发现时，Connector 就会给客户端浏览器发送标准的 Http 404 错误响应页面。Resource 子模块：这个子模块包含一些资源文件，如 Server.xml 及 Web.xml 配置文件。严格说来，这个子模块不包含 Java 源代码，但是它还是 Tomcat 编译运行所必需的。2.2 Tomcat 运行流程图 2-2 Tomcat 运行流程假设来自客户的请求为：http://localhost:8080/test/index.jsp请求被发送到本机端口 8080，被在那里侦听的 Coyote HTTP/1.1 Connector 获得。Connector 把该请求交给它所在的 Service 的 Engine 来处理，并等待 Engine 的回应。Engine 获得请求 localhost:8080/test/index.jsp，匹配它所有虚拟主机 Host。Engine 匹配到名为 localhost 的 Host（即使匹配不到也把请求交给该 Host 处理，因为该 Host 被定义为该 Engine 的默认主机）。localhost Host 获得请求 /test/index.jsp，匹配它所拥有的所有 Context。Host 匹配到路径为 /test 的 Context（如果匹配不到就把该请求交给路径名为 "" 的 Context 去处理）。path="/test" 的 Context 获得请求 /index.jsp，在它的 mapping table 中寻找对应的 Servlet。Context 匹配到 URL PATTERN 为 *.jsp 的 Servlet，对应于 JspServlet 类。构造 HttpServletRequest 对象和 HttpServletResponse 对象，作为参数调用 JspServlet 的 doGet 或 doPost 方法。Context 把执行完了之后的 HttpServletResponse 对象返回给 Host。Host 把 HttpServletResponse 对象返回给 Engine。Engine 把 HttpServletResponse 对象返回给 Connector。Connector 把 HttpServletResponse 对象返回给客户 Browser。3 Connector 源码分析3.1 Tomcat 的启动分析与集成设想我们知道，启动 Tomcat 有两种方式：双击 bin/startup.bat运行 bin/catalina.bat run它们对应于 Bootstrap 与 Catalina 两个类。我们现在只关心 Catalina 这个类，这个类使用 Apache Digester 解析 conf/server.xml 文件生成 Tomcat 组件，然后再调用 Embedded 类的 start 方法启动 Tomcat。所以，集成 Tomcat 的方式就有以下两种了：沿用 Tomcat 自身的 server.xml。自己定义一个 XML 格式来配置 Tomcat 的各参数，自己再写解析这段 XML，然后使用 Tomcat 提供的 API 根据这些 XML 来生成 Tomcat 组件，最后调用 Embedded 类的 start 方法启动 Tomcat。个人觉得第一种方式要优越，给开发者比较好的用户体验。如果使用这种，直接模仿 Catalina 类的方法即可实现集成。目前，JOnAS 就使用了这种集成方式，JBoss、GlassFish 使用的第二种自定义 XML 的方式。3.2 Connector 类图与顺序图图 3-1 Connector 相关类图图 3-2 Connector 工作流程顺序图从上面二图中我们可以得到如下信息：Tomcat 中有四种容器 (Context、Engine、Host、Wrapper)，前三者常见，第四个不常见但它也是实现了 Container 接口的容器。如果要自定义一个 Connector 的话，只需要实现 ProtocolHandler 接口，该接口定义如下：图 3-3 自定义 Connector 时需实现的 ProtocolHandler 接口Tomcat 以 HTTP（包括 BIO 与 NIO）、AJP、APR、内存四种协议实现了该接口（它们分别是：AjpAprProtocol、AjpProtocol、Http11AprProtocol、Http11NioProtocol、Http11Protocol、JkCoyoteHandler、MemoryProtocolHandler）。要使用哪种 Connector 就在 conf/server.xml 中配置，在 Connector 的构造函数中会通过反射实例化所配置的实现类：<Connector port="8181" protocol="org.apache.coyote.http11.Http11AprProtocol" />3.3 Connector 的工作流程下面我们以 Http11AprProtocol 为例说明 Connector 的工作流程。它将工作委托给 NioEndpoint 类。在 NioEndpoint 类的 init 方法中构建一个 SocketServer (当然，不同的实现类会有一些微小的变化，例如如果是 NIO，它构建的就是 SocketServerChannel)。在 NioEndpoint.Acceptor 类中会接收一个客户端新的连接请求，如下图：在 NioEndpoint 类中，有一个内部接口 Handler，该接口定义如下：在 Http11NioProtocol 类中实现了 Handler 这个内部接口，并调用 Http11NioProcessor 类 (该类实现了 ActionHook 回调接口)。在 Response 类中会调用 ActionHook 实现类的相关方法的，Response 类的 action 方法如下：Http11NioProcessor 的 process 实现方法中，会通过 Adapter 来调用 Servlet 容器生成响应结果。4 如何实现 Connector由上面的介绍我们可以知道，实现 Connector 就是实现 ProtocolHandler 接口的过程。AjpAprProtocol、AjpProtocol、Http11AprProtocol、Http11Protocol、JkCoyoteHandler、MemoryProtocolHandler 这些实现类的实现流程与 Http11NioProtocol 相同。下面我们以 Http11NioProtocol 为类重点说明 Tomcat 中如何实现 ProtocolHandler 接口的。Http11NioProtocol 实现了 ProtocolHandler 接口，它将所有的操作委托给 NioEndpoint 类去做，如下图：NioEndpoint 类中的 init 方法中首先以普通阻塞方式启动了 SocketServer：NioEndpoint 类的 start 方法是关键，如下：可以看出，在 start 方法中启动了两个线程和一个线程池：Acceptor 线程：该线程以普通阻塞方式接收客户端请求（socket.accept()），将客户 Socket 交由线程池处理。线程池要将该 Socket 配置成非阻塞模式（socket.configureBlocking(false)），并且向 Selector 注册 READ 事件。该线程数目可配置，默认为 1 个。Poller 线程：由于 Acceptor 委托线程为客户端 Socket 注册了 READ 事件，当 READ 准备好时，就会进入 Poller 线程的循环。Poller 线程也是委托线程池去做，线程池将 NioChannel 加入到 ConcurrentLinkedQueue<NioChannel> 队列中。该线程数目可配置，默认为 1 个。线程池：就是上面说的做 Acceptor 与 Poller 线程委托要做的事情。4.1 Init 接口实现方法中阻塞方式启动 ServerSocketChannel在 Init 接口实现方法中阻塞方式启动 ServerSocketChannel。4.2 Start 接口实现方法中启动所有线程Start 方法中启动了线程池、Acceptor 线程与 Poller 线程。其中 Acceptor 与 Poller 线程一般数目为 1，当然，数目也可配置。可以看出，线程池有两种实现方式：普通 queue + wait + notify 方式，默认使用的方式，据说实际测试这种比下种效率高。JDK1.5 自带的线程池方式。4.3 Acceptor 线程接收客户请求、注册 READ 事件在 Acceptor 线程中接收了客户请求，同时委托线程池注册 READ 事件。在 Acceptor 线程中接收了客户请求（serverSock.accept()）委托线程池处理在线程池的 Worker 线程的 run 方法中有这么几句：在 setSocketOptions 方法中，首先将 socket 配置成非阻塞模式：在 setSocketOptions 方法中，最后调用 getPoller0().register(channel); 一句为 SocketChannel 注册 READ 事件，register 方法代码如下 (注意：这是 Poller 线程的方法)：其中 attachment 的结构如下，它可以看做是一个共享的数据结构：4.4 Poller 线程读请求、生成响应数据、注册 WRITE 事件在上面说的 setSocketOptions 方法中调用 Poller 线程的 register 方法注册读事件之后，当 READ 准备就绪之后，就开始读了。下面代码位于 Poller 线程的 run 方法之中：可以看到，可读之后调用 processSocket 方法，该方法将读处理操作委托给线程池处理 (注意此时加入到线程池的是 NioChannel，不是 SocketChannel)：线程池的 Worker 线程中的 run 方法中的部分代码如下（请注意 handler.process(socket) 这一句）：注意：调用了 handler.process(socket) 来生成响应数据。数据生成完之后，注册 WRITE 事件的，代码如下：4.5 Handler 接口实现类通过 Adapter 调用 Servlet 容器生成响应数据NioEndpoint 类中的 Handler 接口定义如下：其中 process 方法通过 Adapter 来调用 Servlet Container 生成返回结果。Adapter 接口定义如下：4.6 小结实现一个 Tomcat 连接器 Connector 就是实现 ProtocolHandler 接口的过程。Connector 用来接收 Socket Client 端的请求，通过内置的线程池去调用 Servlet Container 生成响应结果，并将响应结果同步或异步的返回给 Socket Client。在第三方应用集成 Tomcat 作为 Web 容器时，一般不会动 Servlet Container 端的代码，那么 Connector 的性能将是整个 Web 容器性能的关键。说明：本文基于 Tomcat 早期版本（如 Tomcat 7 或 Tomcat 8 初期）的架构进行分析，其中涉及到的 BIO、NIO、APR 连接器分类及 Http11NioProtocol 等实现细节在后续版本（如 Tomcat 8.5+）中有所调整（例如 BIO 连接器已被移除，NIO 成为默认实现）。文中源码分析逻辑仍具有参考价值，但具体类名或配置方式请以实际使用的 Tomcat 版本为准。
Tomcat 系统架构与设计模式，第 1 部分: 工作原理
Tomcat 工作原理本文以 Tomcat 5 为基础，同时兼顾 Tomcat 6 和 Tomcat 4 的相关特性。Tomcat 的基本设计思路和架构具有较好的连续性。Tomcat 总体结构Tomcat 的结构虽然复杂，但其模块化设计非常清晰。找到了 Tomcat 最核心的模块，就抓住了 Tomcat 的“七寸”。下面是 Tomcat 的总体结构图：从上图中可以看出，Tomcat 的核心是两个组件：Connector 和 Container。这两个组件将在后面详细介绍。Connector 组件是可以被替换的，这样可以提供给服务器设计者更多的选择。因为这个组件不仅与服务器本身的设计相关，还与不同的应用场景十分相关，所以一个 Container 可以选择对应多个 Connector。多个 Connector 和一个 Container 形成了一个 Service。Service 的概念大家都很熟悉了，有了 Service 就可以对外提供服务了。但是 Service 还需要一个生存环境，必须要有人能够赋予其生命、掌握其生死大权，那就非 Server 莫属了。所以整个 Tomcat 的生命周期由 Server 控制。以 Service 作为“婚姻”我们将 Tomcat 中 Connector、Container 作为一个整体比作一对情侣的话：Connector 主要负责对外交流，可以比作为 Boy；Container 主要处理 Connector 接受的请求，主要是处理内部事务，可以比作为 Girl。那么这个 Service 就是连接这对男女的结婚证了。是 Service 将它们连接在一起，共同组成一个家庭。当然要组成一个家庭还要很多其它的元素。说白了，Service 只是在 Connector 和 Container 外面多包一层，把它们组装在一起，向外面提供服务。一个 Service 可以设置多个 Connector，但是只能有一个 Container 容器。这个 Service 接口的方法列表如下：从 Service 接口中定义的方法中可以看出，它主要是为了关联 Connector 和 Container，同时会初始化它下面的其它组件。注意接口中它并没有规定一定要控制它下面的组件的生命周期。所有组件的生命周期在一个 Lifecycle 的接口中控制，这里用到了一个重要的设计模式，关于这个接口将在后面介绍。Tomcat 中 Service 接口的标准实现类是 StandardService。它不仅实现了 Service 接口，同时还实现了 Lifecycle 接口，这样它就可以控制它下面的组件的生命周期了。StandardService 类结构图如下：从上图中可以看出，除了 Service 接口方法的实现以及控制组件生命周期的 Lifecycle 接口的实现，还有几个方法是用于事件监听的方法的实现。不仅是这个 Service 组件，Tomcat 中其它组件也同样有这几个方法，这也是一个典型的设计模式，将在后面介绍。下面看一下 StandardService 中主要的几个方法实现的代码，下面是 setContainer 和 addConnector 方法的源码：清单 1. StandardService.setContainerpublic void setContainer(Container container) { Container oldContainer = this.container; if ((oldContainer != null) && (oldContainer instanceof Engine)) ((Engine) oldContainer).setService(null); this.container = container; if ((this.container != null) && (this.container instanceof Engine)) ((Engine) this.container).setService(this); if (started && (this.container != null) && (this.container instanceof Lifecycle)) { try { ((Lifecycle) this.container).start(); } catch (LifecycleException e) { ; } } synchronized (connectors) { for (int i = 0; i < connectors.length; i++) connectors[i].setContainer(this.container); } if (started && (oldContainer != null) && (oldContainer instanceof Lifecycle)) { try { ((Lifecycle) oldContainer).stop(); } catch (LifecycleException e) { ; } } support.firePropertyChange("container", oldContainer, this.container); }这段代码很简单，其实就是先判断当前的这个 Service 有没有已经关联了 Container。如果已经关联了，那么去掉这个关联关系——oldContainer.setService(null)。如果这个 oldContainer 已经被启动了，结束它的生命周期。然后再替换新的关联、再初始化并开始这个新的 Container 的生命周期。最后将这个过程通知感兴趣的事件监听程序。这里值得注意的地方就是，修改 Container 时要将新的 Container 关联到每个 Connector，还好 Container 和 Connector 没有双向关联，不然这个关联关系将会很难维护。清单 2. StandardService.addConnectorpublic void addConnector(Connector connector) { synchronized (connectors) { connector.setContainer(this.container); connector.setService(this); Connector results[] = new Connector[connectors.length + 1]; System.arraycopy(connectors, 0, results, 0, connectors.length); results[connectors.length] = connector; connectors = results; if (initialized) { try { connector.initialize(); } catch (LifecycleException e) { e.printStackTrace(System.err); } } if (started && (connector instanceof Lifecycle)) { try { ((Lifecycle) connector).start(); } catch (LifecycleException e) { ; } } support.firePropertyChange("connector", null, connector); } }上面是 addConnector 方法，这个方法也很简单，首先是设置关联关系，然后是初始化工作，开始新的生命周期。这里值得一提的是，注意 Connector 用的是数组而不是 List 集合，这个从性能角度考虑可以理解。有趣的是这里用了数组但是并没有向我们平常那样，一开始就分配一个固定大小的数组，它这里的实现机制是：重新创建一个当前大小的数组对象，然后将原来的数组对象 copy 到新的数组中，这种方式实现了类似的动态数组的功能，这种实现方式，值得我们以后拿来借鉴。最新的 Tomcat 6 中 StandardService 也基本没有变化，但是从 Tomcat 5 开始 Service、Server 和容器类都继承了 MBeanRegistration 接口，MBeans 的管理更加合理。以 Server 为“居”前面说一对情侣因为 Service 而成为一对夫妻，有了能够组成一个家庭的基本条件，但是它们还要有个实体的家，这是它们在社会上生存之本，有了家它们就可以安心的为人民服务了，一起为社会创造财富。Server 要完成的任务很简单，就是要能够提供一个接口让其它程序能够访问到这个 Service 集合、同时要维护它所包含的所有 Service 的生命周期，包括如何初始化、如何结束服务、如何找到别人要访问的 Service。还有其它的一些次要的任务，如您住在这个地方要向当地政府去登记啊、可能还有要配合当地公安机关日常的安全检查什么的。Server 的类结构图如下：它的标准实现类 StandardServer 实现了上面这些方法，同时也实现了 Lifecycle、MBeanRegistration 两个接口的所有方法。下面主要看一下 StandardServer 重要的一个方法 addService 的实现：清单 3. StandardServer.addServicepublic void addService(Service service) { service.setServer(this); synchronized (services) { Service results[] = new Service[services.length + 1]; System.arraycopy(services, 0, results, 0, services.length); results[services.length] = service; services = results; if (initialized) { try { service.initialize(); } catch (LifecycleException e) { e.printStackTrace(System.err); } } if (started && (service instanceof Lifecycle)) { try { ((Lifecycle) service).start(); } catch (LifecycleException e) { ; } } support.firePropertyChange("service", null, service); } }从上面第一句就知道了 Service 和 Server 是相互关联的，Server 也是和 Service 管理 Connector 一样管理它，也是将 Service 放在一个数组中，后面部分的代码也是管理这个新加进来的 Service 的生命周期。Tomcat 6 中也是没有什么变化的。组件的生命线"Lifecycle"前面一直在说 Service 和 Server 管理它下面组件的生命周期，那它们是如何管理的呢？Tomcat 中组件的生命周期是通过 Lifecycle 接口来控制的。组件只要继承这个接口并实现其中的方法就可以统一被拥有它的组件控制了，这样一层一层的直到一个最高级的组件就可以控制 Tomcat 中所有组件的生命周期。这个最高的组件就是 Server，而控制 Server 的是 Startup，也就是您启动和关闭 Tomcat。下面是 Lifecycle 接口的类结构图：除了控制生命周期的 Start 和 Stop 方法外还有一个监听机制，在生命周期开始和结束的时候做一些额外的操作。这个机制在其它的框架中也被使用，如在 Spring 中。关于这个设计模式会在后面介绍。Lifecycle 接口的方法的实现都在其它组件中，就像前面中说的，组件的生命周期由包含它的父组件控制，所以它的 Start 方法自然就是调用它下面的组件的 Start 方法，Stop 方法也是一样。如在 Server 中 Start 方法就会调用 Service 组件的 Start 方法，Server 的 Start 方法代码如下：清单 4. StandardServer.startpublic void start() throws LifecycleException { if (started) { log.debug(sm.getString("standardServer.start.started")); return; } lifecycle.fireLifecycleEvent(BEFORE_START_EVENT, null); lifecycle.fireLifecycleEvent(START_EVENT, null); started = true; synchronized (services) { for (int i = 0; i < services.length; i++) { if (services[i] instanceof Lifecycle) ((Lifecycle) services[i]).start(); } } lifecycle.fireLifecycleEvent(AFTER_START_EVENT, null); }监听的代码会包围 Service 组件的启动过程，就是简单的循环启动所有 Service 组件的 Start 方法，但是所有 Service 必须要实现 Lifecycle 接口，这样做会更加灵活。Server 的 Stop 方法代码如下：清单 5. StandardServer.stoppublic void stop() throws LifecycleException { if (!started) return; lifecycle.fireLifecycleEvent(BEFORE_STOP_EVENT, null); lifecycle.fireLifecycleEvent(STOP_EVENT, null); started = false; for (int i = 0; i < services.length; i++) { if (services[i] instanceof Lifecycle) ((Lifecycle) services[i]).stop(); } lifecycle.fireLifecycleEvent(AFTER_STOP_EVENT, null); }它所要做的事情也和 Start 方法差不多。Connector 组件Connector 组件是 Tomcat 中两个核心组件之一，它的主要任务是负责接收浏览器的发过来的 TCP 连接请求，创建一个 Request 和 Response 对象分别用于和请求端交换数据，然后会产生一个线程来处理这个请求并把产生的 Request 和 Response 对象传给处理这个请求的线程，处理这个请求的线程就是 Container 组件要做的事了。由于这个过程比较复杂，大体的流程可以用下面的顺序图来解释：Tomcat 5 中默认的 Connector 是 Coyote，这个 Connector 是可以选择替换的。Connector 最重要的功能就是接收连接请求然后分配线程让 Container 来处理这个请求，所以这必然是多线程的，多线程的处理是 Connector 设计的核心。Tomcat 5 将这个过程更加细化，它将 Connector 划分成 Connector、Processor、Protocol，另外 Coyote 也定义自己的 Request 和 Response 对象。下面主要看一下 Tomcat 中如何处理多线程的连接请求，先看一下 Connector 的主要类图：看一下 HttpConnector 的 Start 方法：清单 6. HttpConnector.startpublic void start() throws LifecycleException { if (started) throw new LifecycleException (sm.getString("httpConnector.alreadyStarted")); threadName = "HttpConnector[" + port + "]"; lifecycle.fireLifecycleEvent(START_EVENT, null); started = true; threadStart(); while (curProcessors < minProcessors) { if ((maxProcessors > 0) && (curProcessors >= maxProcessors)) break; HttpProcessor processor = newProcessor(); recycle(processor); } }threadStart() 执行就会进入等待请求的状态，直到一个新的请求到来才会激活它继续执行，这个激活是在 HttpProcessor 的 assign 方法中，这个方法是代码如下：清单 7. HttpProcessor.assignsynchronized void assign(Socket socket) { while (available) { try { wait(); } catch (InterruptedException e) { } } this.socket = socket; available = true; notifyAll(); if ((debug >= 1) && (socket != null)) log(" An incoming request is being assigned"); }创建 HttpProcessor 对象是会把 available 设为 false，所以当请求到来时不会进入 while 循环，将请求的 socket 赋给当前处理的 socket，并将 available 设为 true。当 available 设为 true 是 HttpProcessor 的 run 方法将被激活，接下去将会处理这次请求。Run 方法代码如下：清单 8. HttpProcessor.runpublic void run() { while (!stopped) { Socket socket = await(); if (socket == null) continue; try { process(socket); } catch (Throwable t) { log("process.invoke", t); } connector.recycle(this); } synchronized (threadSync) { threadSync.notifyAll(); } }解析 socket 的过程在 process 方法中，process 方法的代码片段如下：清单 9. HttpProcessor.processprivate void process(Socket socket) { boolean ok = true; boolean finishResponse = true; SocketInputStream input = null; OutputStream output = null; try { input = new SocketInputStream(socket.getInputStream(), connector.getBufferSize()); } catch (Exception e) { log("process.create", e); ok = false; } keepAlive = true; while (!stopped && ok && keepAlive) { finishResponse = true; try { request.setStream(input); request.setResponse(response); output = socket.getOutputStream(); response.setStream(output); response.setRequest(request); ((HttpServletResponse) response.getResponse()) .setHeader("Server", SERVER_INFO); } catch (Exception e) { log("process.create", e); ok = false; } try { if (ok) { parseConnection(socket); parseRequest(input, output); if (!request.getRequest().getProtocol().startsWith("HTTP/0")) parseHeaders(input); if (http11) { ackRequest(output); if (connector.isChunkingAllowed()) response.setAllowChunking(true); } } // ... } try { ((HttpServletResponse) response).setHeader ("Date", FastHttpDateFormat.getCurrentDate()); if (ok) { connector.getContainer().invoke(request, response); } // ... } try { shutdownInput(input); socket.close(); } catch (IOException e) { ; } catch (Throwable e) { log("process.invoke", e); } socket = null; } }当 Connector 将 socket 连接封装成 request 和 response 对象后接下来的事情就交给 Container 来处理了。Servlet 容器"Container"Container 是容器的父接口，所有子容器都必须实现这个接口。Container 容器的设计用的是典型的责任链的设计模式，它有四个子容器组件构成，分别是：Engine、Host、Context、Wrapper。这四个组件不是平行的，而是父子关系：Engine 包含 Host，Host 包含 Context，Context 包含 Wrapper。通常一个 Servlet class 对应一个 Wrapper，如果有多个 Servlet 就可以定义多个 Wrapper，如果有多个 Wrapper 就要定义一个更高的 Container 了，如 Context。Context 通常就是对应下面这个配置：清单 10. server.xml<Context path="/library" docBase="D:\projects\library\deploy\target\library.war" reloadable="true" />容器的总体设计Context 还可以定义在父容器 Host 中，Host 不是必须的，但是要运行 war 程序，就必须要 Host，因为 war 中必有 web.xml 文件，这个文件的解析就需要 Host 了。如果要有多个 Host 就要定义一个 top 容器 Engine 了。而 Engine 没有父容器了，一个 Engine 代表一个完整的 Servlet 引擎。那么这些容器是如何协同工作的呢？先看一下它们之间的关系图：当 Connector 接受到一个连接请求时，将请求交给 Container，Container 是如何处理这个请求的？这四个组件是怎么分工的，怎么把请求传给特定的子容器的呢？又是如何将最终的请求交给 Servlet 处理。下面是这个过程的时序图：这里看到了 Valve 是不是很熟悉，没错 Valve 的设计在其他框架中也有用的，同样 Pipeline 的原理也基本是相似的，它是一个管道。Engine 和 Host 都会执行这个 Pipeline，您可以在这个管道上增加任意的 Valve，Tomcat 会挨个执行这些 Valve，而且四个组件都会有自己的一套 Valve 集合。您怎么才能定义自己的 Valve 呢？在 server.xml 文件中可以添加，如给 Engine 和 Host 增加一个 Valve 如下：清单 11. server.xml<Engine defaultHost="localhost" name="Catalina"> <Valve className="org.apache.catalina.valves.RequestDumperValve"/> <Host appBase="webapps" autoDeploy="true" name="localhost" unpackWARs="true" xmlNamespaceAware="false" xmlValidation="false"> <Valve className="org.apache.catalina.valves.FastCommonAccessLogValve" directory="logs" prefix="localhost_access_log." suffix=".txt" pattern="common" resolveHosts="false"/> </Host> </Engine>StandardEngineValve 和 StandardHostValve 是 Engine 和 Host 的默认的 Valve，它们是最后一个 Valve，负责将请求传给它们的子容器，以继续往下执行。前面是 Engine 和 Host 容器的请求过程，下面看 Context 和 Wrapper 容器时如何处理请求的。下面是处理请求的时序图：从 Tomcat 5 开始，子容器的路由放在了 request 中，request 中保存了当前请求正在处理的 Host、Context 和 Wrapper。Engine 容器Engine 容器比较简单，它只定义了一些基本的关联关系，接口类图如下：它的标准实现类是 StandardEngine，这个类注意一点就是 Engine 没有父容器了，如果调用 setParent 方法时将会报错。添加子容器也只能是 Host 类型的，代码如下：清单 12. StandardEngine.addChildpublic void addChild(Container child) { if (!(child instanceof Host)) throw new IllegalArgumentException (sm.getString("standardEngine.notHost")); super.addChild(child); } public void setParent(Container container) { throw new IllegalArgumentException (sm.getString("standardEngine.notParent")); }它的初始化方法也就是初始化和它相关联的组件，以及一些事件的监听。Host 容器Host 是 Engine 的子容器，一个 Host 在 Engine 中代表一个虚拟主机，这个虚拟主机的作用就是运行多个应用，它负责安装和展开这些应用，并且标识这个应用以便能够区分它们。它的子容器通常是 Context，它除了关联子容器外，还有就是保存一个主机应该有的信息。下面是和 Host 相关的类关联图：从上图中可以看出除了所有容器都继承的 ContainerBase 外，StandardHost 还实现了 Deployer 接口，上图清楚的列出了这个接口的主要方法，这些方法都是安装、展开、启动和结束每个 web application。Deployer 接口的实现是 StandardHostDeployer，这个类实现了的主要的几个方法，Host 可以调用这些方法完成应用的部署等。Context 容器Context 代表 Servlet 的 Context，它具备了 Servlet 运行的基本环境，理论上只要有 Context 就能运行 Servlet 了。简单的 Tomcat 可以没有 Engine 和 Host。Context 最重要的功能就是管理它里面的 Servlet 实例，Servlet 实例在 Context 中是以 Wrapper 出现的。还有一点就是 Context 如何才能找到正确的 Servlet 来执行它呢？Tomcat 5 以前是通过一个 Mapper 类来管理的，Tomcat 5 以后这个功能被移到了 request 中，在前面的时序图中就可以发现获取子容器都是通过 request 来分配的。Context 准备 Servlet 的运行环境是在 Start 方法开始的，这个方法的代码片段如下：清单 13. StandardContext.startpublic synchronized void start() throws LifecycleException { // ... if (!initialized) { try { init(); } catch (Exception ex) { throw new LifecycleException("Error initializaing ", ex); } } // ... lifecycle.fireLifecycleEvent(BEFORE_START_EVENT, null); setAvailable(false); setConfigured(false); boolean ok = true; File configBase = getConfigBase(); if (configBase != null) { if (getConfigFile() == null) { File file = new File(configBase, getDefaultConfigFile()); setConfigFile(file.getPath()); try { File appBaseFile = new File(getAppBase()); if (!appBaseFile.isAbsolute()) { appBaseFile = new File(engineBase(), getAppBase()); } String appBase = appBaseFile.getCanonicalPath(); String basePath = (new File(getBasePath())).getCanonicalPath(); if (!basePath.startsWith(appBase)) { Server server = ServerFactory.getServer(); ((StandardServer) server).storeContext(this); } } catch (Exception e) { log.warn("Error storing config file", e); } } else { try { String canConfigFile = (new File(getConfigFile())).getCanonicalPath(); if (!canConfigFile.startsWith(configBase.getCanonicalPath())) { File file = new File(configBase, getDefaultConfigFile()); if (copy(new File(canConfigFile), file)) { setConfigFile(file.getPath()); } } } catch (Exception e) { log.warn("Error setting config file", e); } } } // ... Container children[] = findChildren(); for (int i = 0; i < children.length; i++) { if (children[i] instanceof Lifecycle) ((Lifecycle) children[i]).start(); } if (pipeline instanceof Lifecycle) ((Lifecycle) pipeline).start(); // ... }它主要是设置各种资源属性和管理组件，还有非常重要的就是启动子容器和 Pipeline。我们知道 Context 的配置文件中有个 reloadable 属性，如下面配置：清单 14. server.xml<Context path="/library" docBase="D:\projects\library\deploy\target\library.war" reloadable="true" />当这个 reloadable 设为 true 时，war 被修改后 Tomcat 会自动的重新加载这个应用。如何做到这点的呢？这个功能是在 StandardContext 的 backgroundProcess 方法中实现的，这个方法的代码如下：清单 15. StandardContext.backgroundProcesspublic void backgroundProcess() { if (!started) return; count = (count + 1) % managerChecksFrequency; if ((getManager() != null) && (count == 0)) { try { getManager().backgroundProcess(); } catch (Exception x) { log.warn("Unable to perform background process on manager", x); } } if (getLoader() != null) { if (reloadable && (getLoader().modified())) { try { Thread.currentThread().setContextClassLoader (StandardContext.class.getClassLoader()); reload(); } finally { if (getLoader() != null) { Thread.currentThread().setContextClassLoader (getLoader().getClassLoader()); } } } if (getLoader() instanceof WebappLoader) { ((WebappLoader) getLoader()).closeJARs(false); } } }它会调用 reload 方法，而 reload 方法会先调用 stop 方法然后再调用 Start 方法，完成 Context 的一次重新加载。可以看出执行 reload 方法的条件是 reloadable 为 true 和应用被修改，那么这个 backgroundProcess 方法是怎么被调用的呢？这个方法是在 ContainerBase 类中定义的内部类 ContainerBackgroundProcessor 被周期调用的。这个类是运行在一个后台线程中，它会周期的执行 run 方法，它的 run 方法会周期调用所有容器的 backgroundProcess 方法。因为所有容器都会继承 ContainerBase 类，所以所有容器都能够在 backgroundProcess 方法中定义周期执行的事件。Wrapper 容器Wrapper 代表一个 Servlet，它负责管理一个 Servlet，包括的 Servlet 的装载、初始化、执行以及资源回收。Wrapper 是最底层的容器，它没有子容器了，所以调用它的 addChild 将会报错。Wrapper 的实现类是 StandardWrapper，StandardWrapper 还实现了拥有一个 Servlet 初始化信息的 ServletConfig，由此看出 StandardWrapper 将直接和 Servlet 的各种信息打交道。下面看一下非常重要的一个方法 loadServlet，代码片段如下：清单 16. StandardWrapper.loadServletpublic synchronized Servlet loadServlet() throws ServletException { // ... Servlet servlet; try { // ... ClassLoader classLoader = loader.getClassLoader(); // ... Class classClass = null; // ... servlet = (Servlet) classClass.newInstance(); if ((servlet instanceof ContainerServlet) && (isContainerProvidedServlet(actualClass) || ((Context) getParent()).getPrivileged())) { ((ContainerServlet) servlet).setWrapper(this); } classLoadTime = (int) (System.currentTimeMillis() - t1); try { instanceSupport.fireInstanceEvent(InstanceEvent.BEFORE_INIT_EVENT, servlet); if (System.getSecurityManager() != null) { Class[] classType = new Class[]{ServletConfig.class}; Object[] args = new Object[]{((ServletConfig) facade)}; SecurityUtil.doAsPrivilege("init", servlet, classType, args); } else { servlet.init(facade); } if ((loadOnStartup >= 0) && (jspFile != null)) { // ... if (System.getSecurityManager() != null) { Class[] classType = new Class[]{ServletRequest.class, ServletResponse.class}; Object[] args = new Object[]{req, res}; SecurityUtil.doAsPrivilege("service", servlet, classType, args); } else { servlet.service(req, res); } } instanceSupport.fireInstanceEvent(InstanceEvent.AFTER_INIT_EVENT, servlet); // ... } return servlet; }它基本上描述了对 Servlet 的操作，当装载了 Servlet 后就会调用 Servlet 的 init 方法，同时会传一个 StandardWrapperFacade 对象给 Servlet，这个对象包装了 StandardWrapper。ServletConfig 与它们的关系图如下：Servlet 可以获得的信息都在 StandardWrapperFacade 封装，这些信息又是在 StandardWrapper 对象中拿到的。所以 Servlet 可以通过 ServletConfig 拿到有限的容器的信息。当 Servlet 被初始化完成后，就等着 StandardWrapperValve 去调用它的 service 方法了，调用 service 方法之前要调用 Servlet 所有的 filter。Tomcat 中其它组件Tomcat 还有其它重要的组件，如安全组件 security、logger 日志组件、session、mbeans、naming 等其它组件。这些组件共同为 Connector 和 Container 提供必要的服务。说明：本文基于 Tomcat 5 架构编写，兼顾 Tomcat 4/6。现代版本（Tomcat 8/9/10+）在连接器实现（如 NIO）、Servlet 规范支持（如 Servlet 3.0/4.0/5.0）以及内部模块划分等方面已有显著变化，但核心设计模式（如 Lifecycle、Container-Connector 架构）仍具参考价值。
编者注：本文为历史博文归档；涉及 JDK、框架与工具链版本请以当前官方文档为准。引用外链图片可能失效，阅读时请注意时效性。项目概述Panda 是一个基于 Netty 构建的 Java Web 服务器项目（GitHub 仓库）。该项目展示了从简单 HTTP 服务器到集成 Spring Boot 的完整演进过程，旨在探索 Netty 在 Web 容器领域的实现细节。1. 项目名称与简介项目名称：panda简介：一个基于 Netty 的 Java Web 服务器。随着版本迭代，项目逐步增加了静态文件下载、Controller 支持、Servlet 支持，并最终实现了与 Spring Boot 的集成。2. 技术栈核心框架：Netty, Spring Boot, Spring WebNetty：高性能、异步事件驱动的 NIO 框架，用于构建可维护的高性能协议服务器和客户端。Spring Boot：简化基于 Spring 的应用开发，通过自动配置和启动类帮助开发者快速搭建 Spring 应用。Spring Web：Spring 框架的 Web 模块，提供全面的 Web 开发支持。3. 功能与特性支持静态文件下载。提供 Controller 和 Servlet 支持，便于开发传统 Web 应用。集成 Spring Boot，支持替换内嵌 Tomcat，使项目更易于部署和运行。支持异步处理逻辑与 HTTP 流事件分块传输（Chunked），提升应用性能与响应能力。4. 社区与文档项目托管于 GitHub，便于开发者协作与贡献。提供 README 文件，简要介绍项目使用方法与版本历史。可通过微信公众号"Java 码界探秘"获取更多项目相关技术信息。5. 许可证项目采用 Apache-2.0 许可证。允许用户自由地使用、修改、分发和再许可软件，但需遵守相关条件与限制。6. 未来展望随着技术发展与社区贡献，Panda 项目计划继续增加新功能并优化现有架构，以满足更多开发者的需求。架构设计项目整体架构遵循分层设计，从网络层到应用层的请求处理流程如下：外部客户端 --(HTTP 请求)--> Netty 网络层 | V Spring Boot 应用层 / \ Controller/ Session 管理 Servlet (包括异步处理) \ / 业务逻辑处理 | V 版本演进项目经历了多个版本的迭代，主要里程碑如下：v0.1：实现 HTTP 协议基础。v1.0：构建一个简单的 HTTP Server。v2.0：通信模型迁移至 Netty。支持静态文件下载。v3.0：添加 Controller 支持。v4.0：添加 Servlet 支持。对接 Spring Web。v5.0：集成 Spring Boot，替换内嵌 Tomcat。技术栈整合：Netty + Spring Boot + Spring Web。v6.0.0：新建模块 spring-boot-starter-netty。v6.0.1：新增 ChannelHandler 线程池。v6.0.2：新增 Session 处理逻辑。v6.0.3：新增异步处理逻辑。v6.0.4：支持 HTTP Stream Event Chunked 传输。社区与资源欢迎关注微信公众号获取更多技术资讯：博客地址：https://blog.jsdiff.com/说明：本文涉及的技术版本（如 JDK、Spring Boot 等）可能随时间演进，具体兼容性请以官方文档及项目最新仓库为准。
编者注：本文为历史博文归档；涉及 JDK、框架与工具链版本请以当前官方文档为准。引用外链图片可能失效，阅读时请注意时效性。背景与目的为了确保服务不会被过多的 HTTP 长连接压垮，我们需要对 Tomcat 设定最大连接数。超过该连接数的请求将被拒绝，从而将负载引导至其它机器。这既能达到自我保护的目的，也能起到连接数负载均衡的作用。核心参数探索与实验初步尝试：MaxKeepAliveRequests一开始根据故障 TodoList 提供的参数 MaxKeepAliveRequests 进行验证。我们将 Tomcat 配置 server.xml 修改为：同时，启动客户端模拟 30 个长连接。预期应该只有 10 个连接能保持住，但结果与预期不符：30 个连接都连上了，而且正常。这让我们怀疑该配置参数是否真正限制了最大连接数。KeepAlive 是在 HTTP/1.1 中定义的，用来保持客户机和服务器的长连接，通过减少建立 TCP Session 的次数来提高性能。常用的配置参数有 {KeepAlive, KeepAliveTimeout, MaxKeepAliveRequests}，具体含义如下：KeepAlive：决定开启 KeepAlive 支持。KeepAliveTimeout：决定一个 KeepAlive 的连接能保持多少时间。Timeout 后就尽快 shutdown 链接，若还有数据必须再建立新的连接。MaxKeepAliveRequests：与 KeepAliveTimeout 相似，意思是服务多少个请求就 shutdown 连接。显然，这些参数与我们想要求的“最大连接数限制”不符。再次尝试：maxConnections搜索其它配置参数后，发现 maxConnections。根据字面意思觉得就应该是这个了。去验证吧：最大连接数设置为 10，我们启动 30 个长连接。预期应该是只有 10 个长连接，实际结果却是远超过 10 个。这个现象有点不应该。最终验证：maxThreads 与 acceptCount原来还有个参数可以决定连接数的大小：maxThreads：Tomcat 启动的最大线程数，即同时处理的任务个数，默认值为 200。acceptCount：当 Tomcat 启动的线程数达到最大时，接受排队的请求个数，默认值为 100。这两个值如何起作用，请看下面三种情况：情况 1：接受一个请求，此时 Tomcat 启动的线程数没有到达 maxThreads，Tomcat 会启动一个线程来处理此请求。情况 2：接受一个请求，此时 Tomcat 启动的线程数已经到达 maxThreads，Tomcat 会把此请求放入等待队列，等待空闲线程。情况 3：接受一个请求，此时 Tomcat 启动的线程数已经到达 maxThreads，等待队列中的请求个数也达到了 acceptCount，此时 Tomcat 会直接拒绝此次请求，返回 connection refused。同时加上 maxConnections 配置：原来 Tomcat 最大连接数取决于 maxConnections 这个值加上 acceptCount 这个值。在连接数达到了 maxConnections 之后，Tomcat 仍会保持住连接，但是不处理，等待其它请求处理完毕之后才会处理这个请求。源码原理分析Tomcat 的最大连接数参数是 maxConnections，这个值表示最多可以有多少个 socket 连接到 Tomcat 上。BIO 模式：默认最大连接数是它的最大线程数（缺省是 200）。NIO 模式：默认是 10000。APR 模式：则是 8192（Windows 上则是低于或等于 maxConnections 的 1024 的倍数）。不限制：如果设置为 -1 则表示不限制。在 Tomcat 里通过一个计数器来控制最大连接，比如在 Endpoint 的 Acceptor 里大致逻辑如下：while (running) { ... //if we have reached max connections, wait countUpOrAwaitConnection(); //计数 +1，达到最大值则等待 ... // Accept the next incoming connection from the server socket socket = serverSock.accept(); ... processSocket(socket); ... countDownConnection(); //计数 -1 closeSocket(socket); }计数器是通过 LimitLatch 锁来实现的，它内部主要通过一个 java.util.concurrent.locks.AbstractQueuedSynchronizer 的实现来控制。我们将最大连接数设置为 10，同时启动超过 30 个长连接，然后通过 jstack 可以看到 acceptor 线程阻塞在 countUpOrAwaitConnection 方法上："http-nio-8080-Acceptor-0" daemon prio=10 tid=0x00007f9cfc191000 nid=0x1e07 waiting on condition [0x00007f9ca9fde000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x000000076595b688> (a org.apache.tomcat.util.threads.LimitLatch$Sync) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:156) at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:811) at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedInterruptibly(AbstractQueuedSynchronizer.java:969) at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly(AbstractQueuedSynchronizer.java:1281) at org.apache.tomcat.util.threads.LimitLatch.countUpOrAwait(LimitLatch.java:115) at org.apache.tomcat.util.net.AbstractEndpoint.countUpOrAwaitConnection(AbstractEndpoint.java:755) at org.apache.tomcat.util.net.NioEndpoint$Acceptor.run(NioEndpoint.java:787) at java.lang.Thread.run(Thread.java:662)代码层面也解释了这种现象。调优策略建议Tomcat 能支持的最大连接数由 maxConnections 加上 acceptCount 来决定。同时 maxThreads 如何设定？以下部分结论引用自：http://duanfei.iteye.com/blog/1894387一般的服务器操作都包括两方面：1. 计算（主要消耗 CPU），2. 等待（IO、数据库等）。计算密集型：如果我们的操作是纯粹的计算，那么系统响应时间的主要限制就是 CPU 的运算能力。此时 maxThreads 应该尽量设的小，降低同一时间内争抢 CPU 的线程个数，可以提高计算效率，提高系统的整体处理能力。IO 密集型：如果我们的操作纯粹是 IO 或者数据库，那么响应时间的主要限制就变为等待外部资源。此时 maxThreads 应该尽量设的大，这样才能提高同时处理请求的个数，从而提高系统整体的处理能力。此情况下因为 Tomcat 同时处理的请求量会比较大，所以需要关注一下 Tomcat 的虚拟机内存设置和 Linux 的 open file 限制。我在测试时遇到一个问题，maxThreads 我设置的比较大比如 3000，当服务的线程数大到一定程度时（一般是 2000 出头），单次请求的响应时间就会急剧的增加。百思不得其解这是为什么，四处寻求答案无果，最后我总结的原因可能是 CPU 在线程切换时消耗的时间随着线程数量的增加越来越大。CPU 把大多数时间都用来在这 2000 多个线程直接切换上了，当然 CPU 就没有时间来处理我们的程序了。以前一直简单的认为多线程=高效率，其实多线程本身并不能提高 CPU 效率，线程过多反而会降低 CPU 效率。当 CPU 核心数 < 线程数时，CPU 就需要在多个线程直接来回切换，以保证每个线程都会获得 CPU 时间，即通常我们说的并发执行。所以 maxThreads 的配置绝对不是越大越好。现实应用中，我们的操作都会包含以上两种类型（计算、等待），所以 maxThreads 的配置并没有一个最优值，一定要根据具体情况来配置。最好的做法是：在不断测试的基础上，不断调整、优化，才能得到最合理的配置。acceptCount 的配置，我一般是设置的跟 maxThreads 一样大，这个值应该是主要根据应用的访问峰值与平均值来权衡配置的。如果设的较小，可以保证接受的请求较快响应，但是超出的请求可能就直接被拒绝。如果设的较大，可能就会出现大量的请求超时的情况，因为我们系统的处理能力是一定的。Tomcat 6 的 Connector 配置示例如下：<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" maxThreads="800" acceptCount="1000"/>附：Linux 系统连接数查看以下命令可用于查看系统当前的连接状态及并发数。1. 查看当前并发访问数netstat -an | grep ESTABLISHED | wc -l可对比 httpd.conf 中 MaxClients 的数字差距多少。2. 查看进程数ps aux | grep httpd | wc -l3. 统计特定进程数ps -ef | grep httpd | wc -l统计 httpd 进程数，每个请求会启动一个进程，适用于 Apache 服务器。表示 Apache 能够处理 1388 个并发请求，这个值 Apache 可根据负载情况自动调整。4. 查看特定端口连接数netstat -nat | grep -i "80" | wc -lnetstat -an 会打印系统当前网络链接状态，而 grep -i "80" 是用来提取与 80 端口有关的连接的，wc -l 进行连接数统计。最终返回的数字就是当前所有 80 端口的请求总数。5. 查看已建立连接数netstat -an | grep ESTABLISHED | wc -lnetstat -an 会打印系统当前网络链接状态，而 grep ESTABLISHED 提取出已建立连接的信息，然后 wc -l 统计。最终返回的数字就是当前所有 80 端口的已建立连接的总数。查看所有建立连接的详细记录：netstat -nat | grep ESTABLISHED | wc6. 查看 TCP 连接状态查看 Apache 的并发请求数及其 TCP 连接状态：netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'返回结果示例：LAST_ACK 5 SYN_RECV 30 ESTABLISHED 1597 FIN_WAIT1 5 FIN_WAIT2 504 TIME_WAIT 1057其中的状态含义：SYN_RECV：表示正在等待处理的请求数。ESTABLISHED：表示正常数据传输状态。TIME_WAIT：表示处理完毕，等待超时结束的请求数。说明：本文基于 Tomcat 6 及早期版本测试撰写。不同 Tomcat 版本（如 8.5/9/10）在连接器（Connector）实现及默认参数上可能存在差异，生产环境配置请以官方文档及实际压测结果为准。
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 的所有组件的生命周期管理是基于观察者模式（Observer Pattern）设计，它和 Tomcat 的管理是类似的。下面是 LifeCycle 的类关系图：LifeCycle 的类关系图每个组件都会持有一个观察者（在这里是 Listener 类，这个类通常对应到观察者模式中常用的 Observer 角色。关于观察者模式可以参考 《Tomcat 系统架构与设计模式，第 2 部分：设计模式分析》 一文中关于观察者模式的讲解）集合。当 start、fail 或 stop 等事件触发时，这些 Listener 将会被调用。这是最简单的一种设计方式，相比 Tomcat 的 LifeCycle 要简单得多。Handler 的体系结构前面所述 Jetty 主要是基于 Handler 来设计的，Handler 的体系结构影响着整个 Jetty 的方方面面。下面总结了一下 Handler 的种类及作用：图 3. Handler 的体系结构（查看大图）Jetty 主要提供了两种 Handler 类型：HandlerWrapper：它可以将一个 Handler 委托给另外一个类去执行。如我们要将一个 Handler 加到 Jetty 中，那么就必须将这个 Handler 委托给 Server 去调用。配合 ScopeHandler 类我们可以拦截 Handler 的执行，在调用 Handler 之前或之后，可以做一些另外的事情，类似于 Tomcat 中的 Valve。HandlerCollection：这个 Handler 类可以将多个 Handler 组装在一起，构成一个 Handler 链，方便我们做扩展。Jetty 的启动过程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 如何基于这两种协议工作，并且它们如何建立连接和接受请求的。基于 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 启动创建建立连接的时序图：图 5. 建立连接的时序图Jetty 创建接受连接环境需要三个步骤：创建一个队列线程池，用于处理每个建立连接产生的任务，这个线程池可以由用户来指定，这个和 Tomcat 是类似的。创建 ServerSocket，用于准备接受客户端的 socket 请求，以及客户端用来包装这个 socket 的一些辅助类。创建一个或多个监听线程，用来监听访问端口是否有连接进来。相比 Tomcat 创建建立连接的环境，Jetty 的逻辑更加简单，牵涉到的类更少，执行的代码量也更少了。当建立连接的环境已经准备好了，就可以接受 HTTP 请求了。当 Acceptor 接受到 socket 连接后将转入下图所示流程执行：图 6. 处理连接时序图Acceptor 线程将会为这个请求创建 ConnectorEndPoint。HttpConnection 用来表示这个连接是一个 HTTP 协议的连接，它会创建 HttpParse 类解析 HTTP 协议，并且会创建符合 HTTP 协议的 Request 和 Response 对象。接下去就是将这个线程交给队列线程池去执行了。基于 AJP 工作通常一个 Web 服务站点的后端服务器不是将 Java 的应用服务器直接暴露给服务访问者，而是在应用服务器（如 Jboss）的前面再加一个 Web 服务器（如 Apache 或者 Nginx）。我想这个原因大家应该很容易理解，如做日志分析、负载均衡、权限控制、防止恶意请求以及静态资源预加载等等。下图是通常的 Web 服务端的架构图：图 7. 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 创建连接环境时序图：图 8. Jetty 创建连接环境时序图 (AJP)与 HTTP 方式唯一不同的地方的就是将 SocketConnector 类替换成了 Ajp13SocketConnector。改成 Ajp13SocketConnector 的目的就是可以创建 Ajp13Connection 类，表示当前这个连接使用的是 AJP 协议，所以需要用 Ajp13Parser 类解析 AJP 协议，处理连接的逻辑都是一样的。如下时序图所示：图 9. 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 处理时序图：图 10. 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(); server.join();创建一个 ServletContextHandler 并给这个 Handler 添加一个 Servlet，这里的 ServletHolder 是 Servlet 的一个装饰类，它十分类似于 Tomcat 中的 StandardWrapper。下面是请求这个 Servlet 的时序图：图 11. Jetty 处理请求的时序图上图可以看出 Jetty 处理请求的过程就是 Handler 链上 handle 方法的执行过程。在这里需要解释的一点是 ScopeHandler 的处理规则，ServletContextHandler、SessionHandler 和 ServletHandler 都继承了 ScopeHandler，那么这三个类组成一个 Handler 链。它们的执行规则是：ServletContextHandler.handle → ServletContextHandler.doScope → SessionHandler.doScope → ServletHandler.doScope → ServletContextHandler.doHandle → SessionHandler.doHandle → ServletHandler.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 包。解压后如下图：图 12. 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 的 Valve 设计也是与 Jetty 的 Handler 类似的方式。要自己实现一个 Valve 与写一个 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 服务端有个总体的了解。相关主题查看文章 《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 编程各个方面的文章。说明本文内容基于 Jetty 6/7 及 Tomcat 6/7 时期的架构特性编写（文中提及 jetty-jboss-6.1.9 及 BIO 默认配置等细节）。现代版本（如 Jetty 9/10/11 及 Tomcat 8/9/10）在默认连接器（默认 NIO）、模块结构及 JBoss 集成方式上已有显著变化，但核心设计思想（如 Handler 链、生命周期管理）仍具有参考价值。