Tomcat 实现的 2 个核心功能:
-
处理
Socket
连接,负责网络字节流与Request
和Response
对象的转化。 -
加载并管理
Servlet
,以及处理具体的Request
请求。
Tomcat架构
-
Server 对应的就是一个 Tomcat 实例。
-
Service 默认只有一个,也就是一个 Tomcat 实例默认一个 Service。
-
Connector:一个 Service 可能多个 连接器,接受不同连接协议。
-
Container: 多个连接器对应一个容器,顶层容器其实就是 Engine。
Tomcat
支持的 I/O
模型有:
-
NIO
:非阻塞I/O
,采用Java NIO
类库实现。 -
NIO2
:异步I/O
,采用JDK 7
最新的NIO2
类库实现。 -
APR
:采用Apache
可移植运行库实现,是C/C++
编写的本地库。
Tomcat 支持的应用层协议有:
-
HTTP/1.1
:这是大部分 Web 应用采用的访问协议。 -
AJP
:用于和 Web 服务器集成(如 Apache)。 -
HTTP/2
:HTTP 2.0 大幅度的提升了 Web 性能。
连接器的功能需求就是:
-
监听网络端口。
-
接受网络连接请求。
-
读取请求网络字节流。
-
根据具体应用层协议(
HTTP/AJP
)解析字节流,生成统一的Tomcat Request
对象。 -
将
Tomcat Request
对象转成标准的ServletRequest
。 -
调用
Servlet
容器,得到ServletResponse
。 -
将
ServletResponse
转成Tomcat Response
对象。 -
将
Tomcat Response
转成网络字节流。 -
将响应字节流写回给浏览器。
连接器需要完成 3 个高内聚的功能:
-
网络通信。
-
应用层协议解析。
-
Tomcat Request/Response
与ServletRequest/ServletResponse
的转化。
即EndPoint、Processor 和 Adapter
。
EndPoint
负责提供字节流给 Processor
,Processor
负责提供 Tomcat Request
对象给 Adapter
,Adapter
负责提供 ServletRequest
对象给容器。
连接器的三个核心组件 Endpoint
、Processor
和 Adapter
来分别做三件事情,其中 Endpoint
和 Processor
放在一起抽象成了 ProtocolHandler
组件,
EndPoint
EndPoint
是通信监听的接口,是具体的 Socket 接收和发送处理器,是对传输层的抽象,因此 EndPoint
是用来实现 TCP/IP
协议数据读写的,本质调用操作系统的 socket 接口。
EndPoint
是一个接口,对应的抽象实现类是 AbstractEndpoint。
而 AbstractEndpoint
的具体子类,比如在 NioEndpoint
和 Nio2Endpoint
中,有两个重要的子组件:Acceptor
和 SocketProcessor
。
其中 Acceptor 用于监听 Socket 连接请求。SocketProcessor
用于处理 Acceptor
接收到的 Socket
请求,它实现 Runnable
接口,在 Run
方法里调用应用层协议处理组件 Processor
进行处理。为了提高处理能力,SocketProcessor
被提交到线程池来执行。
NioEndpoint:LimitLatch
、Acceptor
、Poller
、SocketProcessor
和 Executor
-
LimitLatch 是连接控制器,它负责控制最大连接数,NIO 模式下默认是 10000,达到这个阈值后,连接请求被拒绝。
-
Acceptor
跑在一个单独的线程里,它在一个死循环里调用accept
方法来接收新连接,一旦有新的连接请求到来,accept
方法返回一个Channel
对象,接着把Channel
对象交给 Poller 去处理。 -
Poller
的本质是一个Selector
,也跑在单独线程里。Poller
在内部维护一个Channel
数组,它在一个死循环里不断检测Channel
的数据就绪状态,一旦有Channel
可读,就生成一个SocketProcessor
任务对象扔给Executor
去处理。 -
SocketProcessor 实现了 Runnable 接口,其中 run 方法中的
getHandler().process(socketWrapper, SocketEvent.CONNECT_FAIL);
代码则是获取 handler 并执行处理 socketWrapper,最后通过 socket 获取合适应用层协议处理器,也就是调用 Http11Processor 组件来处理请求。Http11Processor 读取 Channel 的数据来生成 ServletRequest 对象,Http11Processor 并不是直接读取 Channel 的。这是因为 Tomcat 支持同步非阻塞 I/O 模型和异步 I/O 模型,在 Java API 中,相应的 Channel 类也是不一样的,比如有 AsynchronousSocketChannel 和 SocketChannel,为了对 Http11Processor 屏蔽这些差异,Tomcat 设计了一个包装类叫作 SocketWrapper,Http11Processor 只调用 SocketWrapper 的方法去读写数据。 -
Executor
就是线程池,负责运行SocketProcessor
任务类,SocketProcessor
的run
方法会调用Http11Processor
来读取和解析请求数据。我们知道,Http11Processor
是应用层协议的封装,它会调用容器获得响应,再把响应通过Channel
写出。
Processor:
Processor 用来实现 HTTP 协议,Processor 接收来自 EndPoint 的 Socket,读取字节流解析成 Tomcat Request 和 Response 对象,并通过 Adapter 将其提交到容器处理,Processor 是对应用层协议的抽象。
从图中我们看到,EndPoint 接收到 Socket 连接后,生成一个 SocketProcessor 任务提交到线程池去处理,SocketProcessor 的 Run 方法会调用 HttpProcessor 组件去解析应用层协议,Processor 通过解析生成 Request 对象后,会调用 Adapter 的 Service 方法,方法内部通过 以下代码将请求传递到容器中。
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
Adapter
CoyoteAdapter
,这是适配器模式的经典运用,连接器调用 CoyoteAdapter
的 Sevice
方法,传入的是 Tomcat Request
对象,CoyoteAdapter
负责将 Tomcat Request
转成 ServletRequest
,再调用容器的 Service
方法。
Server:
Service:
Connector:连接器,由Coyote实现。
Container:容器。
容器
连接器负责外部交流,容器负责内部处理。具体来说就是,连接器处理 Socket 通信和应用层协议的解析,得到 Servlet
请求;而容器则负责处理 Servlet
请求。
Wrapper
表示一个 Servlet
,Context
表示一个 Web 应用程序,而一个 Web 程序可能有多个 Servlet
;Host
表示一个虚拟主机,或者说一个站点,一个 Tomcat 可以配置多个站点(Host);一个站点( Host) 可以部署多个 Web 应用;Engine
代表 引擎,用于管理多个站点(Host),一个 Service 只能有 一个 Engine
。
请求定位 Servlet 的过程
Mapper
组件里保存了 Web 应用的配置信息,其实就是容器组件与访问路径的映射关系,比如 Host
容器里配置的域名、Context
容器里的 Web
应用路径,以及 Wrapper
容器里 Servlet
映射的路径,你可以想象这些配置信息就是一个多层次的 Map
。
整个过程分是通过连接器中的 CoyoteAdapter
触发,它会调用 Engine 的第一个 Valve:
@Override
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) {
// 省略其他代码
// Calling the container
connector.getService().getContainer().getPipeline().getFirst().invoke(
request, response);
...
}
组合模式、责任链模式、观察者模式、模板模式
热加载
Tomcat 本质是通过一个后台线程做周期性的任务,定期检测类文件的变化,如果有变化就重新加载类。我们来看 ContainerBackgroundProcessor
具体是如何实现的。
类加载
-
先在本地 Cache 查找该类是否已经加载过,也就是说 Tomcat 的类加载器是否已经加载过这个类。
-
如果 Tomcat 类加载器没有加载过这个类,再看看系统类加载器是否加载过。
-
如果都没有,就让ExtClassLoader去加载,这一步比较关键,目的 防止 Web 应用自己的类覆盖 JRE 的核心类。因为 Tomcat 需要打破双亲委托机制,假如 Web 应用里自定义了一个叫 Object 的类,如果先加载这个 Object 类,就会覆盖 JRE 里面的那个 Object 类,这就是为什么 Tomcat 的类加载器会优先尝试用
ExtClassLoader
去加载,因为ExtClassLoader
会委托给BootstrapClassLoader
去加载,BootstrapClassLoader
发现自己已经加载了 Object 类,直接返回给 Tomcat 的类加载器,这样 Tomcat 的类加载器就不会去加载 Web 应用下的 Object 类了,也就避免了覆盖 JRE 核心类的问题。 -
如果
ExtClassLoader
加载器加载失败,也就是说JRE
核心类中没有这类,那么就在本地 Web 应用目录下查找并加载。 -
如果本地目录下没有这个类,说明不是 Web 应用自己定义的类,那么由系统类加载器去加载。这里请你注意,Web 应用是通过
Class.forName
调用交给系统类加载器的,因为Class.forName
的默认加载器就是系统类加载器。 -
如果上述加载过程全部失败,抛出
ClassNotFound
异常。
Tomcat 作为 Servlet
容器,它负责加载我们的 Servlet
类,此外它还负责加载 Servlet
所依赖的 JAR 包。并且 Tomcat
本身也是也是一个 Java 程序,因此它需要加载自己的类和依赖的 JAR 包。首先让我们思考这一下这几个问题:
-
假如我们在 Tomcat 中运行了两个 Web 应用程序,两个 Web 应用中有同名的
Servlet
,但是功能不同,Tomcat 需要同时加载和管理这两个同名的Servlet
类,保证它们不会冲突,因此 Web 应用之间的类需要隔离。 -
假如两个 Web 应用都依赖同一个第三方的 JAR 包,比如
Spring
,那Spring
的 JAR 包被加载到内存后,Tomcat
要保证这两个 Web 应用能够共享,也就是说Spring
的 JAR 包只被加载一次,否则随着依赖的第三方 JAR 包增多,JVM
的内存会膨胀。 -
跟 JVM 一样,我们需要隔离 Tomcat 本身的类和 Web 应用的类。
1. WebAppClassLoader
Tomcat 的解决方案是自定义一个类加载器 WebAppClassLoader
, 并且给每个 Web 应用创建一个类加载器实例。我们知道,Context 容器组件对应一个 Web 应用,因此,每个 Context
容器负责创建和维护一个 WebAppClassLoader
加载器实例。这背后的原理是,不同的加载器实例加载的类被认为是不同的类,即使它们的类名相同。这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间,每一个 Web 应用都有自己的类空间,Web 应用之间通过各自的类加载器互相隔离。
2.SharedClassLoader
本质需求是两个 Web 应用之间怎么共享库类,并且不能重复加载相同的类。在双亲委托机制里,各个子加载器都可以通过父加载器去加载类,那么把需要共享的类放到父加载器的加载路径下不就行了吗。
因此 Tomcat 的设计者又加了一个类加载器 SharedClassLoader
,作为 WebAppClassLoader
的父加载器,专门来加载 Web 应用之间共享的类。如果 WebAppClassLoader
自己没有加载到某个类,就会委托父加载器 SharedClassLoader
去加载这个类,SharedClassLoader
会在指定目录下加载共享类,之后返回给 WebAppClassLoader
,这样共享的问题就解决了。
3. CatalinaClassloader
如何隔离 Tomcat 本身的类和 Web 应用的类?
要共享可以通过父子关系,要隔离那就需要兄弟关系了。兄弟关系就是指两个类加载器是平行的,它们可能拥有同一个父加载器,基于此 Tomcat 又设计一个类加载器 CatalinaClassloader
,专门来加载 Tomcat 自身的类。
这样设计有个问题,那 Tomcat 和各 Web 应用之间需要共享一些类时该怎么办呢?
老办法,还是再增加一个 CommonClassLoader
,作为 CatalinaClassloader
和 SharedClassLoader
的父加载器。CommonClassLoader
能加载的类都可以被 CatalinaClassLoader
和 SharedClassLoader
使用
Tomcat启动流程
Tomcat请求处理流程
Tomcat服务配置
参考文献