Tomcat

Tomcat

为什么 Java Web 开发需要 Tomcat

JavaEE、Servlet、Java Web 的概念:

  • Java EE(Java Platform Enterprise Edition,2018 年 3 月更名为Jakarta EE)是一些列(Java 企业级开发)标准集合;
  • Servlet 是 Java EE 众多标准下的一个 (link JavaEE.Servlet),主要定义了进行 Java Web 开发的规范(Request、Response 对象、servlet 生命周期)

一个典型的 Servlet 工作模式:

  • 创建并填充 Request 对象,包括:URI、参数、method、请求头信息、请求体信息等
  • 创建 Response 对象
  • 执行业务逻辑,将结果通过 Response 的输出流输出到客户端

所以 Servlet 定义了众多规范:Request & Response 对象、Servlet 生命周期(init & service & destory 方法)…
但 Servlet 没有 main 方法,需要在一个 Servlet 容器里面才能执行,Servlet 容器按照上述规范调用 Servlet 的方法,Tomcat 即是一个 Servlet 容器,在下面 Tomcat 源码分析可以看到 Tomcat 是如何使用 Servlet 规范处理一个 Http 请求的:

  • Tomcat 接到 Http 请求后,使用 Poller 线程来处理这个请求的 socket,在 Poller 线程没有太多的处理,只是确认收包完整,然后把请求丢给 executor(工作线程池);
  • 在工作线程,把 Socket 读取到的数据,按照 Http 协议进行解析,并生成 Request 对象(这里也是按照 Servlet Request 的标准),并最终调用了 Servlet.service()
  • Tomcat 的一个 Context 对应一个 ServletContext,一个 Context 下有 1 个 or 多个 Servlet 实现

回答最开是问题,为什么 Java Web 开发需要 Tomcat?
Java Web 一般要遵循 Servlet 规范,Tomcat 是一个 Servlet 容器的实现,所以~ Java Web 开发需要 Tomcat。
那么开发 Java Web 可以摆脱 Servlet 吗?也可以不用,比如 Spring 5 支持的 WebFlux framework

Is it possible to create a Java web application without servlets? - Quora

Tomcat 整体架构

../_images/JavaEE.Tomcat-2023-05-04-1.png

Tomcat 的架构如上图,主要的组件如下:

  • Server: 表示服务器,它提供了一种优雅的方式来启动和停止整个系统,不必单独启停连接器和容器;它是 Tomcat 构成的顶级构成元素,所有一切均包含在 Server 中;

  • Service: 表示服务,Server 可以运行多个服务。比如一个 Tomcat 里面可运行订单服务、支付服务、用户服务等等;Server 的实现类 StandardServer 可以包含一个到多个 Services, Service 的实现类为 StandardService 调用了容器(Container)接口,其实是调用了 Servlet Engine(引擎),而且 StandardService 类中也指明了该 Service 归属的 Server;

  • Container: 表示容器,可以看做 Servlet 容器;引擎(Engine)、主机(Host)、上下文(Context)和 Wraper 均继承自 Container 接口,所以它们都是容器。

    • Engine – 引擎
    • Host – 主机
    • Context – 上下文
    • Wrapper – 包装器
  • Connector: 表示连接器, 它将 Service 和 Container 连接起来,首先它需要注册到一个 Service,它的作用就是把来自客户端的请求转发到 Container(容器),这就是它为什么称作连接器, 它支持的协议如下:

    • 支持 AJP 协议
    • 支持 Http 协议
    • 支持 Https 协议
  • Service 内部还有各种支撑组件,下面简单罗列一下这些组件

    • Manager – 管理器,用于管理会话 Session
    • Logger – 日志器,用于管理日志
    • Loader – 加载器,和类加载有关,只会开放给 Context 所使用
    • Pipeline – 管道组件,配合 Valve 实现过滤器功能
    • Valve – 阀门组件,配合 Pipeline 实现过滤器功能
    • Realm – 认证授权组件

在下面讲解配置文件的时候可以看到,server.xml 里的元素,与上面的组件一一对应。

本节参考 @ref:: https://pdai.tech/md/framework/tomcat/tomcat-x-arch.html

配置文件 server.xml

一个配置文件实例:

<Server port="8005" shutdown="SHUTDOWN">
<Listener className="org.apache.catalina.startup.VersionLoggerListener" />
<Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
<Listener className="org.apache.catalina.core.JasperListener" />
<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
<Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
<Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />

<GlobalNamingResources>
<Resource name="UserDatabase" auth="Container"
type="org.apache.catalina.UserDatabase"
description="User database that can be updated and saved"
factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
pathname="conf/tomcat-users.xml" />
</GlobalNamingResources>

<Service name="Catalina">
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
<Engine name="Catalina" defaultHost="localhost">
<Realm className="org.apache.catalina.realm.LockOutRealm">
<Realm className="org.apache.catalina.realm.UserDatabaseRealm" resourceName="UserDatabase"/>
</Realm>

<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_access_log." suffix=".txt"
pattern="%h %l %u %t &quot;%r&quot; %s %b" />
</Host>
</Engine>
</Service>
</Server>

server.xml 配置元素分类

整体结构

server.xml 的整体结构如下:

<Server>
<Service>
<Connector />
<Connector />
<Engine>
<Host>
<Context />
<Context />
<!-- 现在常常使用自动部署,不推荐配Context -->
<!-- Context表示一个War应用 -->
</Host>
</Engine>
</Service>
</Server>

该结构中只给出了 Tomcat 的核心组件,除了核心组件外,Tomcat 还有一些其他组件,下面介绍一下组件的分类。

元素分类

server.xml 文件中的元素可以分为以下 4 类:

  • (1)顶层元素:<Server><Service>

<Server> 元素是整个配置文件的根元素,<Service> 元素则代表一个 Engine 元素以及一组与之相连的 Connector 元素。

  • (2)连接器:<Connector>

<Connector> 代表了外部客户端发送请求到特定 Service 的接口;同时也是外部客户端从特定 Service 接收响应的接口。

  • (3)容器:<Engine> <Host> <Context>

容器的功能是处理 Connector 接收进来的请求,并产生相应的响应。Engine、Host 和 Context 都是容器,都实现了 Container 接口,但它们不是平行的关系,而是父子关系:Engine 包含 Host,Host 包含 Context。

  • Engine 表示一个 Servlet 引擎,它可以包含一个或多个子容器,比如 Host 或者 Context 容器;
  • Host 表示一台虚拟的主机,它可以包含一系列 Context 容器;
  • Context 表示一个唯一的 ServletContext,一个 Context 对应一个 Web 工程,它可以包含一个或多个 Wrapper 容器;
  • Wrapper 表示一个独立的 Servlet 定义,即 Wrapper 本质就是对 Servlet 进行了一层包装。

一个 Engine 组件可以处理 Service 中的所有请求,一个 Host 组件可以处理发向一个特定虚拟主机的所有请求,一个 Context 组件可以处理一个特定 Web 应用的所有请求。

  • (4)内嵌组件:

可以内嵌到容器中的组件。实际上,Server、Service、Connector、Engine、Host 和 Context 是最重要的最核心的 Tomcat 组件,其他组件都可以归为内嵌组件。

下面将详细介绍 Tomcat 中各个核心组件的作用,以及相互之间的关系。

核心组件

本部分将分别介绍各个核心组件的作用、特点以及配置方式等。

Server

Server 元素在最顶层,代表整个 Tomcat 容器,因此它必须是 server.xml 中唯一一个最外层的元素。一个 Server 元素中可以有一个或多个 Service 元素。

在第一部分的例子中,在最外层有一个 <Server> 元素,shutdown 属性表示关闭 Server 的指令;port 属性表示 Server 接收 shutdown 指令的端口号,设为-1可以禁掉该端口。

Server 的主要任务,就是提供一个接口让客户端能够访问到这个 Service 集合,同时维护它所包含的所有的 Service 的声明周期,包括如何初始化、如何结束服务、如何找到客户端要访问的 Service。

Service

Service 的作用,是在 Connector 和 Engine 外面包了一层,把它们组装在一起,对外提供服务。一个 Service 可以包含多个 Connector,但是只能包含一个 Engine;其中 Connector 的作用是从客户端接收请求,Engine 的作用是处理接收进来的请求。

在第一部分的例子中,Server 中包含一个名称为“Catalina”的 Service。实际上,Tomcat 可以提供多个 Service,不同的 Service 监听不同的端口,后文会有介绍。

Connector

Connector 的主要功能,是接收连接请求,创建 Request 和 Response 对象用于和请求端交换数据;然后分配线程让 Engine 来处理这个请求,并把产生的 Request 和 Response 对象传给 Engine。

通过配置 Connector,可以控制请求 Service 的协议及端口号。在第一部分的例子中,Service 包含两个 Connector:

<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
  • (1)通过配置第 1 个 Connector,客户端可以通过 8080 端口号使用 http 协议访问 Tomcat。其中,protocol 属性规定了请求的协议,port 规定了请求的端口号,redirectPort 表示当强制要求 https 而请求是 http 时,重定向至端口号为 8443 的 Connector,connectionTimeout 表示连接的超时时间。
  • (2)通过配置第 2 个 Connector,客户端可以通过 8009 端口号使用 AJP 协议访问 Tomcat。AJP 协议负责和其他的 HTTP 服务器(如 Apache)建立连接;在把 Tomcat 与其他 HTTP 服务器集成时,就需要用到这个连接器。之所以使用 Tomcat 和其他服务器集成,是因为 Tomcat 可以用作 Servlet/JSP 容器,但是对静态资源的处理速度较慢,不如 Apache 和 IIS 等 HTTP 服务器;因此常常将 Tomcat 与 Apache 等集成,前者作 Servlet 容器,后者处理静态资源,而 AJP 协议便负责 Tomcat 和 Apache 的连接。Tomcat 与 Apache 等集成的原理如下图(图片来源):

tomcat_ajp_apache

Engine

Engine 组件在 Service 组件中有且只有一个;Engine 是 Service 组件中的请求处理组件。Engine 组件从一个或多个 Connector 中接收请求并处理,并将完成的响应返回给 Connector,最终传递给客户端。

在第一部分的例子中,Engine 的配置语句如下:

<Engine name="Catalina" defaultHost="localhost">

其中,name 属性用于日志和错误信息,在整个 Server 中应该唯一。defaultHost 属性指定了默认的 host 名称,当发往本机的请求指定的 host 名称不存在时,一律使用 defaultHost 指定的 host 进行处理;因此,defaultHost 的值,必须与 Engine 中的一个 Host 组件的 name 属性值匹配。

Host

Engine 与 Host

Host 是 Engine 的子容器。Engine 组件中可以内嵌 1 个或多个 Host 组件,每个 Host 组件代表 Engine 中的一个虚拟主机。Host 组件至少有一个,且其中一个的 name 必须与 Engine 组件的 defaultHost 属性相匹配。

Host 的作用

Host 虚拟主机的作用,是运行多个 Web 应用(一个 Context 代表一个 Web 应用),并负责安装、展开、启动和结束每个 Web 应用。

Host 组件代表的虚拟主机,对应了服务器中一个网络名实体(如” www.test.com ”,或 IP 地址”116.25.25.25”);为了使用户可以通过网络名连接 Tomcat 服务器,这个名字应该在 DNS 服务器上注册。

Host 的配置

在第一部分的例子中,Host 的配置如下:

<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">

下面对其中配置的属性进行说明:

name 属性指定虚拟主机的主机名,一个 Engine 中有且仅有一个 Host 组件的 name 属性与 Engine 组件的 defaultHost 属性相匹配;一般情况下,主机名需要是在 DNS 服务器中注册的网络名,但是 Engine 指定的 defaultHost 不需要,原因在前面已经说明。

unpackWARs 指定了是否将代表 Web 应用的 WAR 文件解压;如果为 true,通过解压后的文件结构运行该 Web 应用,如果为 false,直接使用 WAR 文件运行 Web 应用。

Host 的 autoDeploy 和 appBase 属性,与 Host 内 Web 应用的自动部署有关;此外,本例中没有出现的 xmlBase 和 deployOnStartup 属性,也与 Web 应用的自动部署有关;将在下一节(Context)中介绍。

Context

Context 的作用

Context 元素代表在特定虚拟主机上运行的一个 Web 应用。每个 Web 应用基于 WAR 文件,或 WAR 文件解压后对应的目录(这里称为应用目录)。

Context 是 Host 的子容器,每个 Host 中可以定义任意多的 Context 元素。

在第一部分的例子中,可以看到 server.xml 配置文件中并没有出现 Context 元素的配置。这是因为,Tomcat 开启了自动部署,Web 应用没有在 server.xml 中配置静态部署,而是由 Tomcat 通过特定的规则自动部署。下面介绍一下 Tomcat 自动部署 Web 应用的机制。

Web 应用自动部署

如何开启自动部署 War:

<Host name="localhost"  appBase="webapps"
unpackWARs="true" autoDeploy="true">

此外 Context 组件还有一个 reloadable 属性, <Context docBase="xxx" path="/xxx" reloadable="true"/>
替换 WEB-INF/lib 目录中的 jar 文件或 WEB-INF/classes 目录中的 class 文件时,reloadable=”true”会让修改生效(但代价不小),该选项适合调试。

autoDeploy 和 reloadable 的区别是, 前者是 Host 的属性后者是 Context 的属性,
前者监控的是 webapps 目录下 War 包的改动, 后者监控的是 webapps 下面文件夹内 jar 或者 class 文件的变化;

一般线上环境会关闭这两个参数, 开发阶段可以通过这两个参数无需重启 tomcat 预览改变;

自动部署的实现

Tomcat 的 Engine 会启动一个线程,该线程每 10s 会发送一个发送一个事件,监听到该事件的部署配置类, 会自动去扫描 webapp 文件夹下的 war 包,将其加载成一个 Context,即启动一个 web 服务。

Tomcat 的 StandardEngine 会在 starInternal() 启动一个线程,该线程运行的是 ContainerBackgroundProcessor.run() 方法,
这个 run 每隔10s 唤醒调用一次 processChildren(), 继续跟踪该方法,会看到调用其子容器 Engine、Host、Context、Wrapper 各容器组件及与它们相关的其它组件的 backgroundProcess 方法。
backgroundProcess() 发送一个事件 Lifecycle.PERIODIC_EVENT,
StandardHost 通 server.xml 配置了 HostConfig 监听器,对该事件的响应方法是 HostConfig.lifecycleEvent(),
lifecycleEvent() 会检查 autoDeploy="true" 的配置, 如果开启了, 则调用 deployApps() 扫描 webapp 文件夹下的 war 包,将其加载成一个 Context,即启动一个 web 服务。

核心组件的关联

整体关系

核心组件之间的整体关系,在上一部分有所介绍,这里总结一下:

Server 元素在最顶层,代表整个 Tomcat 容器;一个 Server 元素中可以有一个或多个 Service 元素。
Service 在 Connector 和 Engine 外面包了一层,把它们组装在一起,对外提供服务。一个 Service 可以包含多个 Connector,但是只能包含一个 Engine; Connector 接收请求,Engine 处理请求。
Engine、Host 和 Context 都是容器,且 Engine 包含 Host,Host 包含 Context。每个 Host 组件代表 Engine 中的一个虚拟主机;每个 Context 组件代表在特定 Host 上运行的一个 Web 应用。

如何确定请求由谁处理?

当请求被发送到 Tomcat 所在的主机时,如何确定最终哪个 Web 应用来处理该请求呢?

  • (1)根据协议和端口号选定 Service 和 Engine

Service 中的 Connector 组件可以接收特定端口的请求,当请求进来时,Tomcat 便可以根据协议和端口号选定处理请求的 Service;Service 一旦选定,Engine 也就确定。

  • (2)根据域名或 IP 地址选定 Host

Service 确定后,Tomcat 在 Service 中寻找名称与域名/IP 地址匹配的 Host 处理该请求。如果没有找到,则使用 Engine 中指定的 defaultHost 来处理该请求。

  • (3)根据 URI 选定 Context/Web 应用

这一点在 Context 一节有详细的说明:Tomcat 根据应用的 path 属性与 URI 的匹配程度来选择 Web 应用处理相应请求,这里不再赘述。

  • (4)举例

以请求 http://localhost:8080/app1/index.html为例 ,首先通过协议和端口号(http 和 8080)选定 Service;然后通过主机名(localhost)选定 Host;然后通过 uri(/app1/index.html)选定 Web 应用。

如何部署多个 war 包项目

@todo

其他组件

Listener

Listener(即监听器)定义的组件,可以在特定事件发生时执行特定的操作;被监听的事件通常是 Tomcat 的启动和停止。
监听器可以在 Server、Engine、Host 或 Context 中,本例中的监听器都是在 Server 中。实际上,本例中定义的 6 个监听器,都只能存在于 Server 组件中。监听器不允许内嵌其他组件。
监听器需要配置的最重要的属性是 className,该属性规定了监听器的具体实现类,该类必须实现了 org.apache.catalina.LifecycleListener 接口。

GlobalNamingResources 与 Realm

@todo

Valve

在第一部分的例子中,Host 元素内定义了 Valve 组件:

<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs" prefix="localhost_access_log." suffix=".txt" pattern="%h %l %u %t &quot;%r&quot; %s %b" />

单词 Valve 的意思是“阀门”,在 Tomcat 中代表了请求处理流水线上的一个组件;Valve 可以与 Tomcat 的容器(Engine、Host 或 Context)关联。
不同的 Valve 有不同的特性,下面介绍一下本例中出现的 AccessLogValve。
AccessLogValve 的作用是通过日志记录其所在的容器中处理的所有请求,在本例中,Valve 放在 Host 下,便可以记录该 Host 处理的所有请求。AccessLogValve 记录的日志就是访问日志,每天的请求会写到一个日志文件里。AccessLogValve 可以与 Engine、Host 或 Context 关联;在本例中,只有一个 Engine,Engine 下只有一个 Host,Host 下只有一个 Context,因此 AccessLogValve 放在三个容器下的作用其实是类似的。

性能优化

bin/catalina.sh

这里主要是对 JVM 参数的设置,主要修改 Xms, Xmx, PermSize 几个参数:

JAVA_OPTS="-Xms8g -Xmx8g -Xmn2g -server -DServer=mblog -XX:PermSize=128m -XX:MaxPermSize=128m -XX:MaxTenuringThreshold=4 -XX:+UseConcMarkSweepGC -XX:SurvivorRatio=8 -XX:CMSInitiatingOccupancyFraction=70 -XX:+ExplicitGCInvokesConcurrent -XX:+PrintFlagsFinal -XX:+PrintCommandLineFlags -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCApplicationStoppedTime -XX:+PrintGCApplicationConcurrentTime -Xloggc:../gclogs/gc.log.$nowday"

conf/server.xml

这里主要是对 Connector 和线程池的设置

主要优化点 @tldr:

  • maxThreads = 业务线程池大小,一般几百~几 K,如果并发到来的请求数超过 maxThreads,再新来的请求不能被及时处理;
  • maxConnections:意思是 Tomcat 维护的已建立连接数上限。当连接数达到 maxConnections,Tomcat 不会再 accept,而是直接 block 掉(这时候系统仍然能 accept,但 Tomcat 不会把新连接扔给 Poller 线程了,而是 block 掉新的连接),maxConnections 在 NIO 模式默认是 1W;
  • acceptCount:如果所有的业务线程都在忙( all possible request processing threads are in use),即并发处理中的请求数超过了 maxThreads。这时候新请会放入 accept 队列(区别 socket 的 accept),acceptCount 即是这个队列的长度,默认 100;

区分连接数 & 请求数:

  • 连接数:建立并监听读写事件的 Connection 数量,大部分 Connection 都是空闲的,少数有读写
  • 请求数:指客户端发 Http Request 过来,Tomcat 会把 Request 放入业务线程池处理

maxConnections 决定连接数上限;
acceptCount + maxThread 决定并发请求的上限;

ASCIIFlow 画图解释:


新连接到来:

[Acceptor] ────────────► [Poller] ──线程池是否满?────NO─────► [业务线程池]<maxThreads limits>
<maxConnections limits> │ ▲
Yes │
│ │
┴───►[accept queue]──┴
<acceptCount limits>




图示:
[ ] 表示对象实体
< > 表示限制

这几个参数容易混淆,如果搞清楚 Tomcat 的 NIO 线程模型,就不难理解了


Tomcat 采用 Request Per Thread 策略, 每个用户请求由一个线程处理, <Executor> 部分定义了该线程池, 该线程池被多个 <Connector> 共享, server.xml 里的优化主要在 <Connector>:

<Executor className="org.apache.catalina.core.StandardThreadExecutor" name="tomcatThreadPool" namePrefix="catalina-exec"
maxThreads="300" minSpareThreads="50"/>

<!-- 指定使用上面的线程池 -->
<Connector executor="tomcatThreadPool"
port="8080"
protocol="HTTP/1.1"
connectionTimeout="8000"
enableLookups="false"
acceptCount="300"
maxThreads="300"
acceptorThreadCount="1"
URIEncoding="utf-8"
redirectPort="443"
compression="on" compressionMinSize="1024" compressableMimeType="text/html,text/xml,text/javascript,text/css,text/plain,application/json,application/xml" />

参数说明:

  • Executor 参数

    • maxThreads: 线程池最大线程数
    • minSpareThreads: 线程池最小线程数(线程池初始化时), 默认 25
  • Connector 参数:

    • executor: 指明使用哪一个 Executor, 如果指定了,那么 Connector 中所有关于线程的设定会被忽略, 如果没有指定一个线程池, Connector 将会创建一个私有的线程池.
    • maxThreads: 用于处理客户端请求的最大线程数, 设置为多少视 CPU 处理能力而定, 一般单个应用不应该超过 300, 如果超过 300 应考虑多个 Tomcat 组成集群方式
    • enableLookups: 是否开启域名反查,一般设置为 false 来提高处理能力,它的取值还有 true,一般很少使用。若设为 true, 则支持域名解析,可把 ip 地址解析为主机名
    • connectionTimeout: 网络连接超时,单位:毫秒。设置为 0 表示永不超时
    • acceptorThreadCount: 默认为 1,表示用于 accept 新 socket 连接的线程个数。一般跟监听端口数匹配。
    • acceptCount: 当全部线程都在忙(意味着客户端并发数超过 maxThreads 个线程), 新的请求会放入 accept 队列, 该值是队列的 size, 默认 100
    • maxConnections: Tomcat 的最大连接数, 如果连接数超过 maxConnections, tomcat 将不再 accept 新的连接而是拒绝, For NIO and NIO2 the default is 10000. For APR/native, the default is 8192.
    • compressionMinSize: 大于这个数值讲开启压缩, 默认为 2K
    • compressableMimeType: 压缩哪些类型
    • protocol:协议类型,可选类型有四种,分别为 BIO(阻塞型 IO),NIO,NIO2 和 APR。

      • BIO:BIO(Blocking I/O),顾名思义,即阻塞式 I/O 操作,表示 Tomcat 使用的是传统的 Java I/O 操作(即 java.io 包及其子包)。Tomcat 在默认情况下,是以 bio 模式运行的。遗憾的是,就一般而言,bio 模式是三种运行模式中性能最低的一种。BIO 配置采用默认即可。
      • NIO:NIO(New I/O),是 Java SE 1.4及后续版本提供的一种新的 I/O 操作方式(即 java.nio 包及其子包)。Java nio 是一个基于缓冲区、并能提供非阻塞 I/O 操作的 Java API,因此 nio 也被看成是 non-blocking I/O 的缩写。它拥有比传统 I/O 操作(bio)更好的并发运行性能。要让 Tomcat 以 nio 模式来运行也比较简单,我们只需要 protocol 类型修改为:protocol="org.apache.coyote.http11.Http11NioProtocol"
        对于互联网应用,我们应该在 NIO、NIO2 之间做选择,因为它能够有效的提升性能(主要是并发能力),其中 NIO2 即为 AIO,需要 JDK 1.7+、Linux 2.6+才能支持。
        • NIO:JDK 1.6+,tomcat 6.x+
        • NIO2:JDK 1.7+,tomcat 7.x+
      • APR: 通过 Native 实现的 I/O 库, Tomcat 通过 JNI 调用;

        // Tomcat 支持的几种protocol:
        //NIO
        protocol="org.apache.coyote.http11.Http11NioProtocol"
        //NIO2
        protocol="org.apache.coyote.http11.Http11Nio2Protocol"
        //ARP
        protocol="org.apache.coyote.http11.Http11AprProtocol"

@ref:

附: Connector 和线程池参数解析

  1. <Executor> 部分, 参数说明: https://tomcat.apache.org/tomcat-8.5-doc/config/executor.html
    • namePrefix: The name prefix for each thread created by the executor
    • maxThreads: The max number of active threads in this pool, default is 200
    • minSpareThreads: The minimum number of threads (idle and active) always kept alive, default is 25
    • maxIdleTime: The number of milliseconds before an idle thread shutsdown, unless the number of active threads are less or equal to minSpareThreads. Default value is 60000(1 minute)
    • maxQueueSize: The maximum number of runnable tasks that can queue up awaiting execution before we reject them. Default value is Integer.MAX_VALUE
  2. <Connector> 部分, https://tomcat.apache.org/tomcat-8.5-doc/config/http.html
    • executor: A reference to the name in an Executor element. If this attribute is set, and the named executor exists, the connector will use the executor, and all the other thread attributes will be ignored. Note that if a shared executor is not specified for a connector then the connector will use a private, internal executor to provide the thread pool.
    • acceptCount: The maximum queue length for incoming connection requests when all possible request processing threads are in use. Any requests received when the queue is full will be refused. The default value is 100.
    • connectionTimeout: The number of milliseconds this Connector will wait, after accepting a connection, for the request URI line to be presented. Use a value of -1 to indicate no (i.e. infinite) timeout. The default value is 60000 (i.e. 60 seconds) but note that the standard server.xml that ships with Tomcat sets this to 20000 (i.e. 20 seconds). Unless disableUploadTimeout is set to false, this timeout will also be used when reading the request body (if any).
    • maxThreads: The maximum number of request processing threads to be created by this Connector, which therefore determines the maximum number of simultaneous requests that can be handled. If not specified, this attribute is set to 200. If an executor is associated with this connector, this attribute is ignored as the connector will execute tasks using the executor rather than an internal thread pool. Note that if an executor is configured any value set for this attribute will be recorded correctly but it will be reported (e.g. via JMX) as -1 to make clear that it is not used.
    • acceptorThreadCount: The number of threads to be used to accept connections. Increase this value on a multi CPU machine, although you would never really need more than 2. Also, with a lot of non keep alive connections, you might want to increase this value as well. Default value is 1.
    • maxConnections: The maximum number of connections that the server will accept and process at any given time. When this number has been reached, the server will accept, but not process, one further connection. This additional connection be blocked until the number of connections being processed falls below maxConnections at which point the server will start accepting and processing new connections again. Note that once the limit has been reached, the operating system may still accept connections based on the acceptCount setting. The default value varies by connector type. For NIO and NIO2 the default is 10000. For APR/native, the default is 8192.
      Note that for APR/native on Windows, the configured value will be reduced to the highest multiple of 1024 that is less than or equal to maxConnections. This is done for performance reasons.

Tomcat 在 NIO 模式下的线程模型

Tomcat 使用 Connector 完成整个 HTTP Request 的处理流程, 包括 accept socket → NIO Selector 处理 socket 读事件 → 把可读 Socket 分发给 Work 线程 → 从 socket 读取数据并解析为 Http 请求 → Http 请求交给 CoyoteAdaper 处理, CoyoteAdaper 通过 Mapper 找到对应的 Servlet.

Tomcat-NIO-Connector-Process

在 NIO 实现的 Connector 中,处理请求的主要实体是 NIoEndpoint 对象。NIoEndpoint 中除了包含 Acceptor 和 Worker 外,还是用了 Poller,处理流程如下图所示:

Tomcat-NioEndpoint-Poler-Worker

  • Acceptor accept 客户端请求, 这里虽然是基于 NIO 的 connector,但是在接收 socket 方面还是传统的 serverSocket.accept() 方式, Acceptor 获取到客户端请求的 socket, 封装进 tomcat 的实现类 org.apache.tomcat.util.net.NioChannel 对象中, 然后将 NioChannel 对象封装在一个 PollerEvent 对象中,并将 PollerEvent 对象压入 events queue
  • Poller 读取 events queue 取出 PollerEvent, Poller 线程中维护了一个 Selector 对象, Poller 从 Event 中取出客户端请求的 socketChannel, 把这个 channel 的 READ 事件注册到 Selector 上.
  • Poller 通过 Selector.select() 遍历可读的 socketChannel, 从 Worker 线程池中拿到可用的 Worker 线程,将 socket 传递给 Worker 处理
  • Worker 线程将 socket 封装在 SocketProcessor 对象中。然后从 Http11ConnectionHandler 中取出 Http11NioProcessor 对象,从 Http11NioProcessor 中调用 CoyoteAdapter 的逻辑

本节参考:
https://my.oschina.net/weiweiblog/blog/1830173
https://www.jianshu.com/p/f91f99610b9e

APR 支持

APR(Apache Portable Runtime)可移植运行库,它是 Apache HTTP Server 2.x 的核心。APR 有很多用途,包括访问高级 IO 功能(例如 sendfile,
epoll 和 OpenSSL),OS 级别功能(随机数生成,系统状态等等),本地进程管理(共享内存,NT 管道和 UNIX sockets)。
这些功能可以使 Tomcat 作为一个通常的前台 WEB 服务器(类似 Nginx,只转发请求,不做业务),能更好地和其它本地 web 技术集成,总体上让 Java 更有效率作为一个高性能 web 服务器平台而不是简单作为后台业务容器

可以简单地理解为: Tomcat 将以 JNI 的形式调用 APR 库中的 Native Method 处理文件读取或网络传输操作,提升 Tomcat 对静态文件等等的处理性能,APR 模式下的 Tomcat 更像是 Nginx 的角色。

安装步骤:

  • 下载 APR, 编译 make && make install
  • 安装 Tomcat Native 到 Tomcat 的安装目录下
  • 修改 conf/server.xml, 修改 <Connector> 的 protocol

日志

参考: 日志机制 - Tomcat 8 权威指南 - 极客学院Wiki

在 Apache Tomcat 上运行的 Web 应用可以使用以下日志:

  • 任何自选的日志框架,如 log4j
  • 使用 JDK 提供的日志 java.util.logging
  • Java Servlets 规范所提供的日志 API,如 javax.servlet.ServletContext.log(...)

当 tomcat 启动时会为每个 app 分配了一个 WebappClassLoader ,这样来避免多个 app 会加载相同 jar 包的问题,
不同 Web 应用程序下使用的 Servlet 日志(或者日志框架提供的日志,如 log4j 等)是相互独立的(这与 Tomcat 的 class loader 有关,参考 Class Loader HOW-TO )。
如果 Web 应用程序使用的是 java.util.logging 日志,那么它们并不相互独立,这是因为 java.util.logging 是由 JAVA 系统中的 Bootstrap ClassLoader 来加载的,因此它在各 Web 应用程序之间是共享的。

JULI vs JUL

JUL API(java.util.logging,日志实现,非日志门面)的默认实现功能有限,因此 tomcat 的默认配置中,新增了另一种日志实现 JULI API(org.apache.juli),

可以在 Tomcat 的 logging.properties 里看到定义了两种日志:
java.util.logging 的 java.util.logging.ConsoleHandler ;
JULI 的 org.apache.juli.FileHandler ;

JULI 同样支持标准 JDK java.util.logging 的配置机制(都默认使用 logging.properties 作为配置文件),不同的是 JULI 的每一个类加载属性文件都可以被设置,并可以在其中定义处理器,这样就给了开发者更大的自由度。
JULI 的日志配置分为全局配置和 WebApp 项目配置。
全局配置位于 tomcat 的配置目录 ${catalina.base}/conf/logging.properties 文件,
如果该文件未配置或不可读,那么 tomcat 将会使用 JRE 中的默认日志配置,可以在 ${java.home}/lib/logging.properties 查看配置文件的内容;
项目配置则是针对不同的项目,配置文件位于 WEB-INFO/classes/logging.properties.

JUL 和 JULI 使用相同的日志级别:SEVERE (最高级别) > WARNING > INFO > CONFIG > FINE > FINER > FINEST (所有内容,最低级别)

JULI 所使用的配置与 java.util.logging 所支持的配置基本相同,只不过使用了一些扩展,以便更灵活地配置 logger 和 handler。主要的差别在于:

  • JULI 的 handler 名称前可以加上前缀,所以同一类可以实例化出多个 handler。前缀是一个以数字开头的字符串,并以 . 结尾。比如 22foobar. 就是个有效的前缀。
  • JULI 的 handler 支持额外的属性, 比如 bufferSize

Tomcat 日志配置解析

以下是一个 $CATALINA_BASE/conf 中的默认 logging.properties 文件:

## 声明所有的handlers
handlers = 1catalina.org.apache.juli.FileHandler, 2localhost.org.apache.juli.FileHandler, 3manager.org.apache.juli.FileHandler, 4host-manager.org.apache.juli.FileHandler, java.util.logging.ConsoleHandler

## RootLogger使用的handlers
.handlers = 1catalina.org.apache.juli.FileHandler, java.util.logging.ConsoleHandler

## 定义了4个 org.apache.juli.FileHandler : 1catalina, 2localhost, 3manager, 4host-manager
1catalina.org.apache.juli.FileHandler.level = FINE
1catalina.org.apache.juli.FileHandler.directory = ${catalina.base}/logs
1catalina.org.apache.juli.FileHandler.prefix = catalina.
1catalina.org.apache.juli.AsyncFileHandler.encoding = UTF-8

2localhost.org.apache.juli.FileHandler.level = FINE
2localhost.org.apache.juli.FileHandler.directory = ${catalina.base}/logs
2localhost.org.apache.juli.FileHandler.prefix = localhost.
2localhost.org.apache.juli.AsyncFileHandler.encoding = UTF-8

3manager.org.apache.juli.FileHandler.level = FINE
3manager.org.apache.juli.FileHandler.directory = ${catalina.base}/logs
3manager.org.apache.juli.FileHandler.prefix = manager.
3manager.org.apache.juli.FileHandler.bufferSize = 16384
3manager.org.apache.juli.AsyncFileHandler.encoding = UTF-8

4host-manager.org.apache.juli.FileHandler.level = FINE
4host-manager.org.apache.juli.FileHandler.directory = ${catalina.base}/logs
4host-manager.org.apache.juli.FileHandler.prefix = host-manager.
4host-manager.org.apache.juli.AsyncFileHandler.encoding = UTF-8

## 定义 ava.util.logging.ConsoleHandler :
java.util.logging.ConsoleHandler.level = FINE
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
java.util.logging.ConsoleHandler.encoding = UTF-8


## 定义handlers, 使用上面定义的 FileHandler
org.apache.catalina.core.ContainerBase.[Catalina].[localhost].level = INFO
org.apache.catalina.core.ContainerBase.[Catalina].[localhost].handlers = 2localhost.org.apache.juli.FileHandler

org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/manager].level = INFO
org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/manager].handlers = 3manager.org.apache.juli.FileHandler

org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/host-manager].level = INFO
org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/host-manager].handlers = 4host-manager.org.apache.juli.FileHandler

# For example, set the org.apache.catalina.util.LifecycleBase logger to log
# each component that extends LifecycleBase changing state:
#org.apache.catalina.util.LifecycleBase.level = FINE

# To see debug messages in TldLocationsCache, uncomment the following line:
#org.apache.jasper.compiler.TldLocationsCache.level = FINE
  • java.util.logging.ConsoleHandler:
    When running Tomcat on unixes, the console output is usually redirected to the file named catalina.out.
    The name is configurable using an environment variable. (See the startup scripts).
    Whatever is written to System.err/out will be caught into that file. That may include:
    • Uncaught exceptions printed by java.lang.ThreadGroup.uncaughtException(..)
    • Thread dumps, if you requested them via a system signal

java.util.logging.ConsoleHandler 是 java 自带的日志处理系统(JUL)的控制台日志 Handler,
Tomcat 通过 System.err.println() / System.out.println() 打出的日志会通过 java.util.logging.ConsoleHandler 写入 Tomcat 进程的 stdout/stderr,
并最终输出到文件”catalina.out”中, 这个文件名是在 Tomcat 启动脚本里定义的:

  • org.apache.juli.FileHandler:
    org.apache.juli.FileHandler supports buffering of the logs.
    The buffering is not enabled by default. To configure it, use the bufferSize property of a handler.
    The value of 0 uses system default buffering (typically an 8K buffer will be used).
    A value of < 0 forces a writer flush upon each log write.
    A value > 0 uses a BufferedOutputStream with the defined value but note that the system default buffering will also be applied.

org.apache.支持日志缓存。日志缓存默认是没有启用的。使用 handler 的 bufferSize 属性可以配置它:
属性值为 0 时,代表使用系统默认的缓存(通常使用 8k 缓存);
属性值小于 0 时,将在每个日志写入上强制使用 writer flush(将缓存区中的数据强制写出到系统输出)功能;
属性值大于 0 时,则使用带有定义值的 BufferedOutputStream 类——但要注意的是,这也将应用于系统默认的缓存。

WebApp 的日志配置解析

下例是一个用于 servlet-examples 应用的 WEB-INF/classes 中的 logging.properties 文件

handlers = org.apache.juli.FileHandler

############################################################
# Handler specific properties.
# Describes specific configuration info for Handlers.
############################################################

org.apache.juli.FileHandler.level = FINE
org.apache.juli.FileHandler.directory = ${catalina.base}/logs
org.apache.juli.FileHandler.prefix = ${classloader.webappName}.

Tomcat 启动时报错 “SEVERE: Error listenerStart” 或者 “SEVERE: Error filterStart” 等, 但没有具体的错误日志:
这种一般是因为 Tomcat WebAppClassLoader 加载的org.springframework类的日志没有关联一个 Handler,
可以修改 webapps/xxx/WEB-INF/classes/logging.properties, Tomcat 就会在打印 org.springframework 类的详细的报错信息了.
注意, 老的应用可能还在使用 System.out 或 System.err,这种情况下还需要在 web 应用的 classes/logging.properties 里增加 java.util.logging.ConsoleHandler:

handlers = org.apache.juli.FileHandler, java.util.logging.ConsoleHandler

## JULI.FileHandler 的设置
...

## JUL.ConsoleHandler 的设置
java.util.logging.ConsoleHandler.level = FINE
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter

对于还在使用 System.out 或 System.err 的应用:
可以通过在 Context 元素上设置 swallowOutput 属性来调整。如该属性设为 true,那么在请求阶段对 System.out/err 的调用就会被拦截,它们的输出也会通过 javax.servlet.ServletContext.log(...) 调用反馈给日志系统。

日志文件

  • catalina.2017-08-29.log //Cataline 引擎的日志文件
  • localhost.2017-08-29.log //Tomcat 下内部代码抛出的日志 jsp 页面内部错误的异常
  • manager.2017-08-29.log //Tomcat 下默认 manager 应用日志
  • host-manager.2017-08-29.log //Tomcat 下默认 manager 应用日志
  • localhost_access_log.2017-08-29.txt //访问日志记录
  • catalina.out //控制台输出的日志,Linux 下默认重定向到 catalina.out

查看 catalina.sh, 最终启动 tomcat 执行的命令行是 java ${JAVA_OPTS} org.apache.catalina.startup.Bootstrap start

生产环境中的日志

可能需要注意以下方面:

  • 将 ConsoleHandler 从配置中移除。默认( .handlers 设置)日志会使用 FileHandler 和 ConsoleHandler。
    后者的输出经常会被捕获到一个文件中,比如 catalina.out。从而导致同一消息可能生成了两个副本。
  • 对于不使用的应用(比如 host-manager),可以考虑将 FileHandlers 移除。
  • handler 默认使用系统缺省编码来写入日志文件,通过 encoding 属性可以修改设置,详情查看相关的 javadoc 文档。
  • 增加 Access 访问日志。

使用 log4j

参考 使用 Log4j @ref

  • 如果只是想在自己的 Web 应用上使用 log4j 时
    • 只需将 log4j.jar 和 log4j.properties 放到 Web 应用的 WEB-INF/lib 和 WEB-INF/classes 中即可
  • 如果想配置 Tomcat 以便利用 log4j 来进行自身日志记录时,下面的步骤都是必需的:
    • 创建一个包含下列配置的 log4j.properties 文件,将其保存到 $CATALINA_BASE/lib。Important!
    • 下载 log4j
    • 下载或构建 tomcat-juli.jar 和 tomcat-juli-adapters.jar,以便作为 Tomcat 的额外组件使用。
    • 将 log4j.jar 和 tomcat-juli-adapters.jar 从 extras 中放入 $CATALINA_HOME/lib 中。
    • 用 extras 中的 tomcat-juli.jar 替换 $CATALINA_HOME/bin/tomcat-juli.jar。
    • 删除 $CATALINA_BASE/conf/logging.properties,以防止 java.util.logging 生成零长度的日志文件。

高级 IO

推送: Comet 支持

施工中

发送大型静态文件: sendfile

施工中

How to deploy war

有 3 中方式部署 war 包:

  1. 在 server.xml 的<Host>标签中声明<Context>标签
  2. 在 server.xml 的<Host>标签中开启 autoDeploy, 将 war 包放入 webapps 中会自动部署
  3. context.xml 配置方式

Using the UI manager

Tomcat 提供了一个网页版的 Manager App, 默认位置在 webapps/manager, 也是一个 web 项目, 使用方式参考: Apache Tomcat 6.0 (6.0.53) - Manager App HOW-TO @ref

Go to [<protocol>://]localhost:<port>/manager/html/ (usually localhost:8080/manager/html/),
If you get:

403 Access Denied

go to %CATALINA_HOME%\conf\tomcat-users.xml and check that you have enabled a line like this:

<user username="tomcat" password="tomcat" roles="tomcat,role1,manager-gui"/>

Using maven

待补充…

ClassLoader

为什么 Tomcat 的 Web App ClassLoader 没有遵循 「双亲委派」?[[../12.Java/Advanced-Java.04.ClassLoader#Tomcat]]


翻译自: Apache Tomcat 7 (7.0.93) - Class Loader HOW-TO

与很多服务器应用一样,Tomcat 也安装了各种类加载器。借助类加载器,容器的不同部分以及运行在容器里的 Web Apps 就可以访问不同的仓库(保存着可使用的类和资源)。
// 这里英文原文是”different repositories of available classes and resources.” 不知道该怎么翻译这里的”repositories” ?

在 Java 环境中,类加载器的布局结构是一种父子树的形式。通常,类加载器被请求加载一个特定的类或资源时,它会先把这一请求委托给它的父类加载器,只有(一个或多个)父类加载器无法找到请求的类或资源时,它才开始查看自身的仓库。

注意,Web 应用的类加载器模式跟这个稍有不同,下文将详细介绍,但基本原理是一样。

当 Tomcat 启动后,它就会创建一组类加载器,这些类加载器被布局成如下图所示这种父子关系,父类加载器在子类加载器之上:

    Bootstrap
|
System
|
Common
/ \
WebApp1 WebApp2 ..

如上图所示,Tomcat 在初始化时会创建如下这些类加载器:

  • Bootstrap 这种类加载器包含 JVM 所提供的基本的运行时类,以及来自系统扩展目录($JAVA_HOME/jre/lib/ext)里 JAR 文件中的类。
    注意:在有些 JVM 的实现中,它的作用不仅仅是类加载器,或者它可能根本不可见(作为类加载器)。

  • System 这种类加载器通常是根据 CLASSPATH 环境变量内容进行初始化的(也称为 App ClassLoader)。所有的这些类对于 Tomcat 内部类以及 Web 应用来说都是可见的。不过,标准的 Tomcat 启动脚本($CATALINA_HOME/bin/catalina.sh%CATALINA_HOME%\bin\catalina.bat)完全忽略了 CLASSPATH 环境变量自身的内容,相反从下列仓库来构建系统类加载器:

    • $CATALINA_HOME/bin/bootstrap.jar 包含用来初始化 Tomcat 服务器的 main() 方法,以及它所依赖的类加载器实现类。
    • $CATALINA_BASE/bin/tomcat-juli.jar$CATALINA_HOME/bin/tomcat-juli.jar 日志实现类。其中包括了对 java.util.logging API 的功能增强类(Tomcat JULI),以及对 Tomcat 内部使用的 Apache Commons 日志库的包重命名副本。详情参看 Tomcat 日志文档。
      如果 *$CATALINA_BASE/bin* 中存在 tomcat-juli.jar,就不会使用 $CATALINA_HOME/bin 中的那一个。它有助于日志的特定配置。
    • $CATALINA_HOME/bin/commons-daemon.jar Apache Commons Daemon 项目的类。该 JAR 文件并不存在于由 catalina.bat 或 catalina.sh 脚本所创建的 CLASSPATH 中,而是引用自 bootstrap.jar 的清单文件。
  • Common 这种类加载器包含更多的额外类,它们对于 Tomcat 内部类以及所有 Web 应用都是可见的。
    通常,应用类不会放在这里。该类加载器所搜索的位置定义在 $CATALINA_BASE/conf/catalina.properties 的 common.loader 属性中。默认的设置会搜索下列位置(按照列表中的上下顺序)。

    • $CATALINA_BASE/lib 中的解包的类和资源。
    • $CATALINA_BASE/lib 中的 JAR 文件。
    • $CATALINA_HOME/lib 中的解包类和资源。
    • $CATALINA_HOME/lib 中的 JAR 文件。

默认,它包含以下这些内容:

  • annotations-api.jar JavaEE 注释类。
  • catalina.jar Tomcat 的 Catalina servlet 容器部分的实现。
  • jsp-api.jar JSP 2.3 API
  • servlet-api.jar Servlet 3.1 API
  • tomcat-api.jar Tomcat 定义的一些接口
  • tomcat-dbcp.jar 数据库连接池实现,基于 Apache Commons Pool 的包重命名副本和 Apache Commons DBCP。
  • tomcat-jdbc.jar 一个数据库连接池替代实现,又被称作 Tomcat JDBC 池。详情参看 JDBC 连接池文档。
  • WebappX 为每个部署在单个 Tomcat 实例中的 Web 应用创建的类加载器。你的 Web 应用的 /WEB-INF/classes 目录中所有的解包类及资源,以及 /WEB-INF/lib 目录下 JAR 文件中的所有类及资源,对于该应用而言都是可见的,但对于其他应用来说则不可见。

如上所述,Web 应用类加载器背离了默认的 Java 委托模式(根据 Servlet 规范 2.4 版的 9.7.2 Web Application Classloader 一节中提供的建议)。
当某个请求想从 Web 应用的 WebappX 类加载器中加载类时,该类加载器会先查看自己的仓库,而不是预先进行委托处理。
JRE 基类的部分类不能被重写。对于一些类(比如 J2SE 1.4+ 的 XML 解析器组件),可以使用 J2SE 1.4 支持的特性。
最后,类加载器会显式地忽略所有包含 Servlet API 类的 JAR 文件,所以不要在 Web 应用包含任何这样的 JAR 文件。Tomcat 其他的类加载器则遵循常用的委托模式。

因此,从 Web 应用的角度来看,加载类或资源时,要查看的仓库及其顺序如下:

  1. JVM 的 Bootstrap 类
  2. Web 应用的 /WEB-INF/classes
  3. Web 应用的 /WEB-INF/lib/*.jar 类,其中2和3 都是 WebApp Classloader加载的
  4. System 类加载器的类(加载 CLASSPATH 下的类)
  5. Common 类加载器的类(加载 CATALINA_BASECATALINA_HOME 的lib下的类)

如果 Web 应用类加载器配置有 <Loader delegate="true"/> ,则顺序变为:

  1. JVM 的 Bootstrap 类
  2. System 类加载器的类(如上所述)
  3. Common 类加载器的类(如上所述)
  4. Web 应用的 /WEB-INF/classes 类
  5. Web 应用的 /WEB-INF/lib/*.jar 类

源码导读 (Tomcat 8.5)

编译

ant clean
ant

启动

java -cp ./output/classes -Dcatalina.home=./output/build org.apache.catalina.startup.Bootstrap

启动过程源码调用时序

Bootstrap.main // 入口方法
Bootstrap.init()
initClassLoaders() // 初始化 commonLoader, catalinaLoader, sharedLoader
commonLoader = createClassLoader("common", null);
ClassLoaderFactory.createClassLoader(repositories, parent); // 该方法通过 AccessController.doPrivileged 创建了 URLClassLoader, 并返回
catalinaLoader.loadClass("org.apache.catalina.startup.Catalina")` // 创建 Catalina 对象
Bootstrap.load(args)
Catalina.load()
Digester digester = createStartDigester() // 为 digester 添加 Rule
Digester.parse(inputSource) // 解析 server.xml !!
Digester.startElement()
Rule.begin()
ObjectCreateRule.begin() // 这里通过反射调用了 Server & Connector & Context 等类的构造方法
org.apache.catalina.core.StandardServer.StandardServer() // Server 构造
org.apache.catalina.core.StandardService.StandardService() // Service 构造
ConnectorCreateRule.begin
org.apache.catalina.connector.Connector.Connector()
// Connector 构造, 根据配置中的 "protocol" 设置创建不同的创建 ProtocolHandler:
Http11NioProtocol() // 默认的 ProtocolHandler
AbstractHttp11Protocol(new NioEndpoint())
NioEndpoint()
AbstractEndpoint() // 创建 worker 线程池
Bootstrap.start()
Catalina.start()
StandardServer.start() => LifecycleBase.start()
StandardServer.startInternal()
StandardService.start() => LifecycleBase.start()
StandardService.startInternal()
Engine.start() => LifecycleBase.start() // 启动 Engine
StandardEngine.startInternal()
ContainerBase.startInternal()
StartChild.call() // 多线程启动, 线程数=Host 数量
StandardHost.start() => LifecycleBase.start()
StandardHost.startInternal()
ContainerBase.startInternal() => LifecycleBase.setStateInternal()
ContainerBase.setState(LifecycleState.STARTING)
LifecycleBase.fireLifecycleEvent(lifecycleEvent, data)
HostConfig.start()
HostConfig.deployApps()
HostConfig.deployWARs() // 解析 web.xml !!
MapperListener.start() => LifecycleBase.start() // 启动 MapperListener
MapperListener.startInternal()
Connector.start() => LifecycleBase.start() // 启动(多个) Connector
Connector.startInternal()
Http11NioProtocol.start() => AbstractProtocol.start()
AbstractEndpoint.start() => NioEndpoint.startInternal()
// 创建三个 cache: processorCache, eventCache, nioChannels
startAcceptorThreads()
new AsyncTimeout(); // Start async timeout thread
StandardServer.await() // 创建一个在 8005 监听的 ServerSocket, 用于监听关闭
ServerSocket.accept // 阻塞在这里

源码分析

参考 Tomcat实现:源码分析Tomcat实现细节 @Archived

* Connect.start()—创建并发线程模型: Work 线程, Poller 线程, Acceptor 线程, AsyncTimeout 线程
* 请求处理: Acceptor 线程, Poller 线程, Selector

如何解析 server.xml

  • 在 Catalina.load() 创建 digester: Digester digester = createStartDigester()
  • createStartDigester方法创建了 digester 对象, 并给 digester 对象添加多种 Rule, 每种 Rule 都对应 server.xml 里的一个节点类型, 比如<Server>, <Connector>;
  • digester 对 server.xml 设置的标签动作有 5 种调用:
    • addObjectCreate:遇到起始标签的元素,初始化一个实例对象入栈
    • addSetProperties:遇到某个属性名,使用 setter 来赋值
    • addSetNext:遇到结束标签的元素,调用相应的方法
    • addRule:调用 rule 的 begin 、body、end、finish 方法来解析 xml,入栈和出栈给对象赋值
    • addRuleSet:调用 addRuleInstances 来解析 xml 标签
  • 从这些规则和 xml 中可以看到,Calatina 的 Server 对象是 StandardServer。 StandardService 包含了多个 Connector(xml 中有 2 个 connector)和一个 StandardEngine Container。 StandardEngine 包含了一个 Host Container

初始化 Connector

根据配置文件 protocol = “HTTP/1.1”,”AJP/1.3” 创建对应 protocol, 默认是 Http11NioProtocol,
再由 Http11NioProtocol 创建 NioEndpoint:

代码流程

  • 调用Connector(String protocol), 构造函数Connector中默认创建org.apache.coyote.http11.Http11NioProtocol
  • Http11NioProtocol为例, Http11NioProtocol.init()最终调用到NioEndpoint.bind() => NioEndpoint.initServerSocket() => serverSock.socket().bind(addr,getAcceptCount()) 完成了对端口的绑定
  • bind()的最后调用了NioSelectorPool.open(), 这是一个存放 Selector 的池子,

启动 Connector

Connector 主要功能实现都是在 NioEndpoint, NioEndpoint 包括 x 个 Acceptor 线程, x 个 Poller 线程;
Acceptor 线程(默认一个)用于 accept 客户端请求, 并把客户端请求 socket 封装进 event, 放入 events queue;
Poller 线程池用于消费 events queue, 每个 Poller 都有自己的 Selector 对象, 不断取出 event, 并从中解析出 sockt, 并把 socket 的 READ 事件注册到自己的 Selector.

代码调用流程:

  • Connector 的启动会调用start方法, => Connector.startInternal方法 => Http11NioProtocol.start() => AbstractProtocol.start() => NioEndpoint.start() => NioEndpoint.startInternal()
  • NioEndpoint.startInternal()中,
    • 如果 Worker 线程池是空, 则自己创建: 调用了父类AbstractEndpoint #createExecutor (), 创建 work 线程池, 名称前缀 “-exec-“;
    • 创建NioEndpoint$Poller[]数组, Poller 是 Runnable 的实现, 然后所有的 Poller 线程都 start 起来, 线程名前缀是 “-ClientPoller-“, 数组的大小也就是 Poller 的数量是Math.min(2,Runtime.getRuntime().availableProcessors()), 可见 Poller 数量是 min(2, cpu 的 process 数量)
    • 调用startAcceptorThreads(), 创建 Acceptor线程, 默认一个(线程数是 server.xml 里的acceptCount), , 线程名前缀是”-Acceptor-“

accept 请求

Acceptor 线程 accept , 并把客户端请求 socket 封装进 event, 放入 events queue, 调用流程:

  • Acceptor.run():

    while (endpoint.isRunning()) {
    socket = endpoint.serverSocketAccept();
    endpoint.setSocketOptions(socket) // 调用了 NioEndpoint.setSocketOptions()
    }
  • 再看 NioEndpoint.setSocketOptions()里做了什么: 把客户端请求的 socket 封装进 NioChannel,
    调用 Poller.register(NioChannel): 把 NioChannel 封装进 PollerEvent, 每个 Poller 都有一个 PollerEvent 队列(events queue), 把 PollerEvent 放入这个队列 // Poller 有多个, 这里会轮询的方式选择出其中一个, AtomicInteger.incrementAndGet()) % pollers.length

处理一次 Req 请求

Poller 线程用于消费 events queue, 代码调用流程:

  • Poller.run() while 循环从 event queue 取出 PollerEvent, 然后调用 PollerEvent.run()
    • PollerEvent.run() // 主要是在 Poller 自己的 Selector 上注册 READ 事件
    • Poller.processKey(SelectionKey , NioSocketWrapper) 调用-> Poller.processSocket // 处理 OPEN_READ/OPEN_WRITE 等事件
      • 创建一个 SocketProcessorBase 的实例, 把 socket 和 Event 封装进去, SocketProcessorBase 继承自 Runnable
      • executor.execute(SocketProcessorBase) // 用 Worker 线程池运行这个 SocketProcessorBase
        • SocketProcessorBase.run() -> SocketProcessor.doRun()

再看 SocketProcessor 调用流程:

  • SocketProcessor.doRun() : // 调用 SocketChannel.keyFor()
    • AbstractProtocol$ConnectionHandler.process()
      • NioEndpoint$SocketProcessor.doRun()
        • Http11Processor.service() : 处理 Socket IO 流, 解析为 Http Request
          • ApplicationFilterChain.internalDoFilter(): 调用Filter.doFilter() ,以及Servlet.service();

如何 SHUTDOWN

  • StandardServer.await() 创建一个在 8005 监听的 ServerSocket, 是用来监听关闭 Tomcat 命令的, 当执行 shutdown.sh 关闭 tomcat 时就是连接 8005 端口执行“SHUTDOWN”命令;
  • 关闭请求发给 Tomcat, 由 StandardServer.await 处理, await 方法验证关闭请求是否有效, 如果有效则退出 await 方法, 进入 Catalina.stop(), 调用
    StandardServer.stop, StandardServer.destroy, 然后关闭 Connector, Service

从日志可以看到:

  • WebappLoader.stopInternal -> WebappClassLoaderBase.stop -> WebappClassLoaderBase.clearReferences
    • WebappClassLoaderBase.clearReferencesJdbc
    • WebappClassLoaderBase.clearReferencesThreads
  • AbstractProtocol.pause Pausing ProtocolHandler [“http-nio-8080”]
  • AbstractProtocol.pause Pausing ProtocolHandler [“ajp-nio-8009”]
  • StandardService.stopInternal

Useful Java API usage

  • AsyncChannelWrapperSecure:
    • Executors.newFixedThreadPool , shutdownNow
    • AsynchronousSocketChannel
    • ByteBuffer, flip, hasRemaining,
    • AtomicBoolean
  • WsWebSocketContainer
    • AsynchronousSocketChannel

并发的处理代码

  • 用线程池启动容器内组件

      // Start our child containers, if any
    Container children[] = findChildren();
    List<Future<Void>> results = new ArrayList<>();
    for (int i = 0; i < children.length; i++) {
    results.add(startStopExecutor.submit(new StartChild(children[i])));
    }

    boolean fail = false;
    for (Future<Void> result : results) {
    try {
    result.get();
    } catch (Exception e) {
    }}
  • 通过 Callable 封装带返回值的任务

    private static class StartChild implements Callable<Void> {

    private Container child;

    public StartChild(Container child) {
    this.child = child;
    }

    public Void call() throws LifecycleException {
    child.start();
    return null;

    } }

参考