Tomcat的结构体系

本文基于Tomcat 7,参考了 汪建的《Tomcat内核设计剖析》,也许对理解Tomcat知识体系、面试有所帮助。

Servlet规范

所谓Servelt规范,是一系列的接口和约定的说明。Tomcat实现了Servlet规范,因此有必要先大致了解一下Servlet规范。

1. Servlet接口

核心接口:javax.servlet.Servlet

public interface Servlet {
    
    
    void init(ServletConfig config) throws ServletException;
    
    ServletConfig getServletConfig();
    
    void service(ServletRequest req, ServletResponse res)throws ServletException, IOException;
    
    String getServletInfo();
    
    void destroy();
}

常用Servlet:HttpServelt
生命周期:

  1. 加载实例化(由Web容器完成)
  2. 初始化(init)
  3. 处理请求(service)
  4. 销毁(destroy)

一个问题,想想看:

Servlet是单例吗?

一般来说,在Servlet容器中,每个Servlet类智能对应一个Servlet对象,所有请求都由同一个Servlet对象处理! 因此要小心并发问题!
当客户端第一次请求某个Servlet时,Servlet容器将会根据web.xml的配置实例化这个Servlet对象。当有新的客户端请求到来时,一般不会再实例化该Servlet类,也就是说会有多个线程在使用这个Servlet实例。

2. ServletRequest、ServletResponse

没啥好讲的

3. ServletContext

一个Web应用对应一个ServeltContext接口的实例

4. Filter

涉及到一个设计模式:责任链模式

5. 会话

Servlet并没有提出协议无关的会话规定,每个协议需要自己规定。
HTTP对应的会话接口为 HttpSession。实现有两种:

  1. Cookie是常用的会话跟踪机制,其中Cookie的标准名字必须为JSESSIONID
  2. URL重写,即在URL后面添加一个jsessionid参数,不建议使用

5. web应用

当一个Web应用程序部署到容器中时,在Web程序开始处理客户端请求之前,必须按照下列步骤顺序执行:

  1. 实例化web.xml中<listener>元素标识的每个事件监听器的一个实例
  2. 对于已实例化且实现了ServletContextListener接口的监听器实例,调用其contextInitialized()方法
  3. 实例化web.xml中<filter>元素标识的每个过滤器的一个实例,并调用每个过滤器的init方法来初始化
  4. 根据Servlet配置的load-on-startup属性大小,按照顺序实例化Servlet,并调用其init()方法(load-on-startup只有为正数才能在容器启动时加载,且数值越小越优先)

6. Servlet映射

没啥好说的,Map

Tomcat整体架构及组件

Tomcat内部主要组件

如下图
Tomcat主要组件

Tomcat请求流转过程

如下图
Tomcat请求流转过程

流程说明:

  1. 当Tomat启动后,Connector组件的接收器(Acceptor)将会监听是否有客户端套接字连接并接收Socket
  2. 一旦接收到客户端的连接,则将连接交给线程池Executor处理,开始执行请求响应任务
  3. Http11Processor组件负责从客户端连接中读取消息报文,然后开始解析HTTP的请求行、请求头部、请求体。将解析后的报文封装成Request对象,方便后面处理时通过Request对象获取HTTP协议的相关值
  4. Mapper组件根据HTTP协议请求行的URL属性值和请求头部的Host属性值匹配该有哪个Host、哪个Context、哪个Wrapper容器来处理请求
  5. CoyoteAdaptor组件负责将Connector组件和Engine容器连接起来,把前面处理过程中生成的请求对象Request和响应对象Response传递到Engine容器,调用它的管道。
  6. 后续Engine、Host、Context和Wrapper容器依次处理请求,并且处理时均要通过相关容器的阀门(Valve)
  7. Wrapper容器对应的Servlet对象处理完氢气后,会将结果输出到客户端中

对照上述的组件图,下面分别简单介绍一下几个重要的组件

Server

Tomcat最顶级的组件,代表Tomcat的运行实例,在一个JVM中只有包含一个Server。
一个Server可能会包含多个Service
对应配置文件为server.xml

Server组件图
Tomcat_Server组件

几个重要的组件

下面介绍几个比较重要的组件

1. JreMemoryLeakPreventionListener

该组件主要是用来解决JRE库内存泄露问题的。

1.1 问题描述
Tomcat中重加载一个Web应用是通过实例化一个新的ClassLoader来实现的。如果旧的ClassLoader无法被回收,就会造成内存泄露。
案例一:如果Web应用中一些生命周期很长的单例对象(如 JRE库中的某些类)引用了Tomcat的ClassLoader(如 WebappClassLoader),且该单例对象会在Java程序整个运行周期内存在的话,就可能导致无法卸载该ClassLoader,从而出现内存泄露的情况。
案例二:如果Tomcat的ClassLoader被某个线程持有,然后该线程启动了一个新线程,并且新线程无限循环(如 JRE库中的 Disposer),那么新线程也会继承父线程的ClassLoader,从而造成Tomcat的ClassLoader无法被回收,出现内存泄露的情况。

1.2 解决方法
JreMemoryLeakPreventionListener则是用来解决这个问题的。它的主要作用是,在加载那些特殊的对象时,使用系统自己的ClassLoader,而不是Tomcat的ClassLoader。等到这些特殊的对象加载结束后,再将ClassLoader还原。这样就可以解决这个问题了。

主要功能代码如下:

ClassLoader loader = Thread.currentThread().getContextClassLoader();
try
{
    
    
    // Use the system classloader as the victim for all this
    // ClassLoader pinning we're about to do.
    Thread.currentThread().setContextClassLoader(
            ClassLoader.getSystemClassLoader());

	// 加载一些特殊的对象
} finally {
    
    
  Thread.currentThread().setContextClassLoader(loader);
}
2. ThreadLocalLeakPreventionListener

这个listener主要是用来解决ThreadLocal内存泄露问题的

2.1 问题描述
如果Tomcat Web应用的线程池ThreadLocal对象引用了其ClassLoader(如 WebappClassLoader),由于一般线程池生命周期比较长,若此线程池没有释放,则可能会导致Web应用重加载后,旧的ClassLoader无法释放,从而造成内存泄露。

2.2. 解决方法
通过ThreadLocalLeakPreventionListener类监听Server stop事件,如果发生了Server stop事件,则将线程池内的所有线程销毁,这样就可以解决这个问题了。

主要功能代码如下:

Engine engine = (Engine) context.getParent().getParent();
Service service = engine.getService();
 Connector[] connectors = service.findConnectors();
 if (connectors != null) {
    
    
     for (Connector connector : connectors) {
    
    
         ProtocolHandler handler = connector.getProtocolHandler();
         Executor executor = null;
         if (handler != null) {
    
    
             executor = handler.getExecutor();
         }

         if (executor instanceof ThreadPoolExecutor) {
    
    
             ThreadPoolExecutor threadPoolExecutor =
                 (ThreadPoolExecutor) executor;
             threadPoolExecutor.contextStopping();
         } else if (executor instanceof StandardThreadExecutor) {
    
    
             StandardThreadExecutor stdThreadExecutor =
                 (StandardThreadExecutor) executor;
             stdThreadExecutor.contextStopping();
         }

     }
 }

shutdown命令

Tomcat启动后,主线程会进入等待shutdown命令的环节,它会不断尝试读取客户端发送过来的消息,一旦匹配上 shutdown 命令,则会跳出循环,关闭Tomcat。而daemon线程则负责接收客户端请求,处理客户端报文。

图示如下:
Tomcat监听shutdown命令

相关源码如下:
org.apache.catalina.startup.Catalina#start

if (await) {
    
    
   await();
   stop();
}

org.apache.catalina.core.StandardServer#await

// Match against our command string
boolean match = command.toString().equals(shutdown);
if (match) {
    
    
    log.info(sm.getString("standardServer.shutdownViaPort"));
    break;
} else
	 log.warn(sm.getString("standardServer.invalidShutdownCommand", command.toString()));

Service

Service是服务的抽象,它代表请求从接收到处理的所有组件的集合。
一个Service可能会包含若干个Connector、一个Engine、若干Executor线程池组件

Connector

Connect的职责:接收客户端连接,进行客户端请求处理

JIoEndpoint

JIoEndpoint为BIO模式,主要处理流程如下:
JIoEndpoint

关注几个类方法:
Connector的初始化:org.apache.catalina.connector.Connector#initInternal
Connector的启动:org.apache.catalina.connector.Connector#startInternal
Acceptor的处理:org.apache.tomcat.util.net.JIoEndpoint.Acceptor#run
SocketProcessor的处理:org.apache.tomcat.util.net.JIoEndpoint.SocketProcessor#run

NioEndpoint

NioEndpoint为非NIO模式,主要处理流程如下:
NioEndpoint

几个关键组件:
LimitLatch:负责对连接数的控制,基于AQS并发框架实现
Acceptor:负责接收套接字连接并注册到通道队列里
Poller:负责轮询检查事件列表
SocketProcessor:任务定义器
Executor:负责处理套接字的连接池

AprEndpoint

AprEndpoint为本地库IO模式(比较复杂,感觉不用特别关注),主要处理流程如下:
AprEndpoint

AprEndpoint有两种处理模式:

  1. 如果操作系统支持TCP_DEFER_ACCEPT参数,则Acceptor通过APR获取到套接字后,直接创建SocketWithOptionsProcessor对象,最后直接放到线程池中执行套接字的读写和逻辑处理,整个过程都是阻塞的。
  2. 如果操作系统不支持TCP_DEFER_ACCEPT参数,则Acceptor通过APR获取到套接字后,将套接字放到待轮询队列PollSet中,而PollSet则不断通过APR检测已准备好的套接字,接着创建SocketProcessor对象,最后放入线程池中执行,接下来的过程也是阻塞的。

TCP_DEFER_ACCEPT参数优化:
TCP_DEFER_ACCEPT参数优化

使用了TCP_DEFER_ACCEPT参数优化的区别在于,三次握手之后,最后一次的ACK后连接并不会被接收,而是当客户端数据发送到来时才会被接收。这样一来有一个好处,就是连接只要被接收就肯定有数据。

Engine

Engine代表全局Servlet引擎
一个Engine可以包含若干个Host容器

Host

Host组件代表虚拟主机
一个Host虚拟主机可以存放若干Web应用的抽象(即 Context容器)

HostConfig组件

Host容器存放的是Context级别的容器(即 Web应用)。当Tomcat启动后,需要加载Web应用,设置相关的属性和配置,这其中就要用到HostConfig

HostConfig是Host容器的生命周期监听器,它实现了org.apache.catalina.LifecycleListener 接口,在其 lifecycleEvent 方法中,会监听 Tomcat 启动、停止等操作,然后进行相应的操作。

HostConfig 的一个重要的功能就是 Context容器(即 Web应用)的部署,其支持以下类型的Web应用部署。

  • Descriptor描述符类型
  • War包类型
  • 目录类型

其中Descriptor类型的部署,支持 Web应用的重部署重加载;而 War包类型和目录类型,只支持 Web应用的重加载

Context

Context组件是Web引用的抽象。(我们开发的Web应用最终部署到Tomcat中就会对应一个Context对象
一个Context容器会有若干个Servlet(即 Wrapper容器)

比较重要的组件:

  • Loader组件:Web应用加载器,用于加载Web应用的资源,它要保证不同Web应用之间的资源隔离
  • Manager组件:会话管理器,用于管理对应Web容器的会话
  • NamingResource组件:命名资源,它负责将Tomcat配置文件的server.xml 和 Web应用的 context.xml资源和属性映射到内存中
  • Mapper组件:Servlet映射器,它属于Context内部的路由映射器,只负责该Context容器的路由导航
  • 过滤器组件:FilterDefContextFilterMapsApplicationFilterConfig TODO
  • WebappClassLoader:类加载器的隔离 TODO
  • ApplicationContext:是Servlet规范中的ServletContext接口的标准实现,表示某个Web应用的运行环境。
  • ServletContainerInitializer:Web容器启动时,让第三方组件做一些初始化工作
  • MemoryLeakTrackingListener监听器:辅助完成关于内存泄露跟踪的工作。

Wrapper

Wrapper是Tomcat中最小的容器级别。
一般来说,一个Wrapper对应一个Servlet对象(即 所有线程共用同一个Servlet对象)。SingleThreadModel是例外,可能对应一个Servlet对象池(SingleThreadModel已经是Deprecated了)。

Servlet工作机制的大致流程是:Request -> StandardEngineValve -> StandardHostValve -> StandardContextValve -> StandardWrapperValve -> 实例化并初始化Servlet对象 -> 由过滤器链执行过滤操作 -> 调用该Servlet对象的service方法 -> Response

比较重要的组件:

  • 过滤器链
  • Comet模式的支持
  • WebSocket协议的支持
  • 异步Servlet(参考Dubbo异步调用)

根据请求资源的不同,Servlet可以分为三类:

  • 普通Servlet,处理普通的业务逻辑请求
  • JspServlet,访问JSP页面
  • DefaultServlet,访问静态资源

Valve

管道模式类似于责任链模式,顾名思义,就像一条管道将多个对象连接起来,整体看起来就像若干个阀门嵌套在管道中,而处理逻辑就放在阀门上

Tomcat 4个级别的容器中,分别都有自己的基础阀门。如下:
StandardEngineValve
StandardHostValve
StandardContextValve
StandardWrapperValve

Tomcat中常用的阀门有:
AccessLogValve:记录客户端访问日志
ErrorReportValve:将错误以HTML格式输出的阀门
PersistentValve:将请求会话持久化的阀门
RemoteAddrValve:访问IP控制限制的阀门
RemoteIPValve:代理或负载均衡处理的阀门
SemaphoreValve:控制容器并发访问的饭呢

日志

Tomcat采用工厂模式(LogFactory)生成日志对象,底层使用JDK自带的日志工具(DirectJDKLog),而没有使用第三方日志工具,以减少包的引入。没有采用JDK日志工具的默认配置,而二十通过配置系统变量和重写某些类达到特定效果。

国际化

Tomcat日志的国际化,使用了JDK里面的三个类:MessageFormat、Locale、ResourceBundle,而Tomcat中引用StringManager类把这三个类封装起来,方便操作。而StringManager类采用了一个Java包对应一个StringManager对象的设计,折中的考虑使性能与资源得以同时兼顾。

Tomcat类加载

Tomcat 7中的类加载器,能够实现公共Jar的共享 和 各个Web应用之间的隔离。加载器的继承关系如下:

Tomcat 7类加载器
Tomcat中多了Common类加载器和WebApp类加载器。

  • Common类加载器加载Tomcat公用的一些类与组件
  • WebApp类加载是用于加载Web应用程序的,每一个类加载器负责加载一个Web应用
    T
    omcat启动的时候,就会将Common类加载器设置为线程上下文类加载器。

相关代码:
org.apache.catalina.startup.Bootstrap#init()

initClassLoaders();
Thread.currentThread().setContextClassLoader(catalinaLoader);

会话

Web容器的会话机制补充了HTTP协议的无状态性。Tomcat在服务端使用jsessionid 与 客户端的cookie配合,完成了其的会话机制。
其jsession传递方式一般有三种:

  • Cookie方式
  • 重写URL方式,即将jsessionid附加到url上
  • 表单隐藏方式

Tomcat中Session相关的类:

  • 标准会话对象:StandardSession
  • 增量会话对象:DeltaSession
  • 标准会话管理器:StandardManager
  • 持久化会话管理器:PersistentManager
    支持多种会话存储方式,主要针对的是单个会话的持久化,存储方式包括:
    • 文件方式存储 FileStore
    • 数据库存储 JDBCStore
  • 集群增量会话管理器:DeltaManager
    • 全节点赋值
    • 只复制会话增量
  • 集群备份会话管理器:BackupManager
    • 集群的会话节点一般为:一主一备多备份
    • 会话黏贴(Session Stick)机制:一种会话定位技术,即在Tomcat节点上生成一种包含位置信息的会话ID,它一般附带了Tomcat实例名,当客户端再次请求时,负载均衡器会解析会话ID中的位置信息并转发到相应节点上
    • 集群RPC通信:Tribes框架

猜你喜欢

转载自blog.csdn.net/somehow1002/article/details/108331728