码农翻身

Tomcat架构的秘密

- by MRyan, 2023-01-02



本系列针对于 Tomcat 版本为 8.5X

文章已收录至精进Tomcat系列 系列其它文章 https://www.wormholestack.com/tag/Tomcat/

源码阅读环境:https://gitee.com/M-Analysis/source_tomcat8 已填充关键注释


1.Servlet

所谓 Servlet,其实就是 Sun 公司(被 Oracle 收购)继设计了 Applet 对 Web 应用的支持后为了让Java能实现动态可交互的网页,从而进入 Web 编程领域而制定的一套标准。

这套标准是这么说的:

你想用 Java 开发动态网页, 可以定义一个自己的 Servlet, 但一定要实现标准的 HTTPServlet 接口, 然后重载 doGet(), doPost()方法。

用户从浏览器 GET 的时候, 调用 doGet() 方法。

从浏览器向服务器发送表单数据的时候, 调用 doPost() 方法。

如果你想访问用户从浏览器传递过来的参数, 用 HttpServletRequest 对象就可以了, 里面有 getParameter(), getQueryString() 方法, 如果你处理完了, 想向浏览器返回数据, 用 HttpServletResponse 对象调用 getPrintWriter() 方法就可以输出数据了。

1.1 一个简单 Servlet 的例子

我希望在浏览器输入 http://localhost:8080/display/get 会得到一个渲染过后的动态页面,页面上展示 2 行内容,一行是当前访问的 path,一行是 url 上拼接的参数name的值。

实现如下

1. 编写代码

编写DisplayServlet.java

package com.wormholestack.tomcat.test;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * @description: DisplayServlet
 * @Author MRyan
 * @Version 1.0
 */
public class DisplayServlet extends HttpServlet {

    /**
     * 处理 GET 方法请求的方法
     *
     * @param request  an {@link HttpServletRequest} object that
     *                 contains the request the client has made
     *                 of the servlet
     * @param response an {@link HttpServletResponse} object that
     *                 contains the response the servlet sends
     *                 to the client
     * @throws ServletException
     * @throws IOException
     */
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String contextPath = request.getContextPath() + request.getServletPath();
        String name = request.getParameter("name");
        // 设置响应内容
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        String contextPathStr = String.format("Path :%s", contextPath);
        String nameStr = String.format("Name: %s", name);
        String docType =
            "<!DOCTYPE html> \n";
        out.println(docType +
            "<html>\n" +
            "<head><meta charset=\"utf-8\"><title>" + "Display" + "</title></head>\n" +
            "<body bgcolor=\"#2ed573\">\n" +
            "<h1 style=\"color: white;\">" + contextPathStr + "</h1>\n" +
            "<h1 style=\"color: white;\">" + nameStr + "</h1>\n"
        );
    }

    /**
     * 处理 POST 方法请求的方法
     *
     * @param request  an {@link HttpServletRequest} object that
     *                 contains the request the client has made
     *                 of the servlet
     * @param response an {@link HttpServletResponse} object that
     *                 contains the response the servlet sends
     *                 to the client
     * @throws ServletException
     * @throws IOException
     */
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);
    }
}

开发 Java Web 应用时,不同类型的文件有严格的存放规则,否则不仅可能会使 Web 应用无法访问,还会导致 Web 服务器启动报错

image-20230101222316952

WebRoot: Web 应用所在目录,一般情况下虚拟目录要配置到此文件夹当中。

WEB-INF:此文件夹必须位于 WebRoot 文件夹里面,而且必须以这样的形式去命名,字母都要大写。

web.xml:配置文件,有格式要求,此文件必须以这样的形式去命名,并且必须放置到 WEB-INF 文件夹中。

本例中如下图所示

image-20221227223307529

2. 配置web.xml

由于客户端是通过 URL 地址访问 web 服务器中的资源,所以 Servlet 程序若想被外界访问,必须把 servlet 程序映射到一个 URL 地址上。

这个工作在 web.xml 文件中使用 <servlet> 元素和 <servlet-mapping> 元素完成。

<servlet> 元素用于注册 Servlet,它包含有两个主要的子元素:<servlet-name><servlet-class>,分别用于设置Servlet的注册名称和Servlet的完整类名。

一个 <servlet-mapping> 元素用于映射一个已注册的Servlet的一个对外访问路径,它包含有两个子元素:<servlet-name><url-pattern>,分别用于指定Servlet的注册名称和Servlet的对外访问路径。

  • 同一个 Servlet 可以被映射到多个 URL 上,即多个 <servlet-mapping> 元素的 <servlet-name> 子元素的设置值可以是同一个 Servlet 的注册名。
  • 同一个 Url 不能对应多个 Servlet。否则会报错 Caused by: java.lang.IllegalArgumentException: The servlets named [xxx] and [xxx] are both mapped to the url-pattern xxx
<!DOCTYPE web-app PUBLIC
        "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
        "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
    <display-name>Archetype Created Web Application</display-name>
    <servlet>
        <!-- servlet名,一般写成类名,并不一定严格是类名 -->
        <servlet-name>displayServlet</servlet-name>
        <!-- 所在的包 -->
        <servlet-class>com.wormholestack.tomcat.test.DisplayServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <!-- 与上面的servlet-name相对应-->
        <servlet-name>displayServlet</servlet-name>
        <!-- 访问的网址 -->
        <url-pattern>/get</url-pattern>
    </servlet-mapping>
</web-app>

访问 http://localhost:8080/display/get?name=MRyan 看看效果吧

image-20221227223510212

实际上一个对每个请求, Servlet 容器会为其完成以下 3 个操作:

  • 读取客户端(浏览器)发送的数据。创建并填充 Request 对象,包括:URI、参数、method、请求头信息、请求体信息等,Request 对象是 javax.servlet.ServletRequest 接口或 javax.servlet.ServletRequest 接口的一个实例。
  • 创建一个调用 Response 对象, 用来向 Web 客户端发送响应。response 对象是 javax.servlet.http.ServletResponse 接口或 javax.servlet.ServletResponse 接口的一个实例。
  • 调用 Servlet 的 service 方法, 将 request 对象和 response 对象作为参数传入, Servlet 从 request 对象中读取信息, 处理数据并生成结果。这个过程可能需要访问数据库,调用 Web 服务,或者直接计算得出对应的响应。平时的业务逻辑就是在这个部分实现,并通过 response 对象发送处理过后的响应信息,通过 Response 的输出流输出到客户端。


Servlet 不能独立运行,它是一个供 Servlet 引擎吗,Web容器(例如 Tomcat 调用的 Java 类),它的运行完全由 Servlet 引擎来控制,这也是我们所编写的 Web 项目中不用写 main 方法的原因。

上面举的例子,我是将 servlet 运行在了 Tomcat 上,关于 Tomcat 我稍后介绍。


针对客户端的多次 Servlet 请求,通常情况下,服务器只会创建一个 Servlet 实例对象,也就是说Servlet 实例对象一旦创建,它就会驻留在内存中,为后续的其它请求服务,直至 web 容器退出,servlet 实例对象才会销毁。

在 Servlet 的整个生命周期内,Servlet 的 init 方法只被调用一次。而对一个 Servlet 的每次访问请求都导致 Servlet 引擎调用一次 servlet 的 service 方法。

对于每次访问请求,Servlet 引擎都会创建一个新的 HttpServletRequest 请求对象和一个新的HttpServletResponse 响应对象,然后将这两个对象作为参数传递给它调用的 Servlet 的 service()方法,service 方法再根据请求方式分别调用 doXXX方法。

如果在 <servlet> 元素中配置了一个 <load-on-startup> 元素,那么 WEB 应用程序在启动时,就会装载并创建 Servlet 的实例对象、以及调用 Servlet 实例对象的 init()方法。

    <servlet>
        <servlet-name>invoker</servlet-name>
        <servlet-class>
            org.apache.catalina.servlets.InvokerServlet
        </servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

很多第三方框架也需要在应用一加载就实例化 Serlvet。

例如 SpringMVC 中的 org.springframework.web.servlet.DispatcherServlet 下面的servlet配置为启动时装载

<!-- springMVC的核心控制器 -->
<servlet>
    <servlet-name>springMVC</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath*:springMVC-servlet.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
    <async-supported>true</async-supported>
</servlet>
<servlet-mapping>
    <servlet-name>springMVC</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

1.2 缺省Servlet

如果某个 Servlet 的映射路径仅仅为一个正斜杠(/),那么这个 Servlet 就成为当前 Web 应用程序的缺省 Servlet。

凡是在 web.xml 文件中找不到匹配的 <servlet-mapping> 元素的URL,它们的访问请求都将交给缺省 Servlet 处理,也就是说,缺省 Servlet 用于处理所有其他 Servlet 都不处理的访问请求。

比如在 Tomcat 中 confweb.xml 文件中,注册了一个名称为 org.apache.catalina.servlets.DefaultServlet的Servlet,并将这个 Servlet 设置为了缺省 Servlet。

    <servlet>
        <servlet-name>default</servlet-name>
        <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
        <init-param>
            <param-name>debug</param-name>
            <param-value>0</param-value>
        </init-param>
        <init-param>
            <param-name>listings</param-name>
            <param-value>false</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <!-- The mapping for the default servlet -->
    <servlet-mapping>
        <servlet-name>default</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

当访问 Tomcat 服务器中的某个静态 HTML 文件和图片时,实际上是在访问这个缺省 Servlet。

1.3 Servlet线程安全问题

当多个客户端并发访问同一个 Servlet 时,Web 服务器会为每一个客户端的访问请求创建一个线程,并在这个线程上调用 Servlet 的 service 方法,因此 service 方法内如果访问了同一个资源的话,就有可能引发线程安全问题。

所有在 Servlet 中尽量避免使用实例变量,最好使用局部变量。


Servlet 容器默认是采用单实例多线程的方式处理多个请求的

  • 当 Web 服务器启动的时候(或客户端发送请求到服务器时),Servlet 就被加载并实例化(只存在一个Servlet 实例)
  • 容器初始化化 Servlet 主要就是读取配置文件(例如 Tomcat,可以通过 servlet.xml 的<Connector> 设置线程池中线程数目,初始化线程池通过 web.xml ,初始化每个参数值等等。
  • 当请求到达时,Servlet 容器通过调度线程(Dispatchaer Thread) 调度它管理下线程池中等待执行的线程(Worker Thread)给请求者。
  • 线程执行 Servlet 的 service 方法。
  • 请求结束,放回线程池,等待被调用;


从上面可以看出(好处):

  1. Servlet 单实例,减少了产生 Servlet 的开销;
  2. 通过线程池来响应多个请求,提高了请求的响应时间;
  3. Servlet 容器并不关心到达的 Servlet 请求访问的是否是同一个 Servlet 还是另一个 Servlet,直接分配给它一个新的线程。
  4. 如果是同一个 Servlet 的多个请求,那么 Servlet 的 service 方法将在多线程中并发的执行;
  5. 每一个请求由 ServletRequest 对象来接受请求,由 ServletResponse 对象来响应该请求;

1.4 Servlet主要对象

1.4.1 ServletConfig

如何需要配置 Serlvet 初始化参数怎么办,这就需要用到 ServletConfig。在 Servlet 的配置文件web.xml 中,可以使用一个或多个 <init-param> 标签为 servlet 配置一些初始化参数。

<servlet>
    <servlet-name>ServletConfigDemo</servlet-name>
    <servlet-class>com.wormholestack.com.test.ServletConfigDemo</servlet-class>
    <!--配置ServletConfig的初始化参数 -->
    <init-param>
        <param-name>name</param-name>
        <param-value>MRyan</param-value>
    </init-param>
     <init-param>
        <param-name>password</param-name>
        <param-value>XXXX</param-value>
    </init-param>
    <init-param>
        <param-name>charset</param-name>
        <param-value>UTF-8</param-value>
    </init-param>
</servlet>

当 servlet 配置了初始化参数后,Web 容器在创建 servlet 实例对象时,会自动将这些初始化参数封装到 ServletConfig 对象中,并在调用 servlet 的 init 方法时,将 ServletConfig 对象传递给servlet。进而,我们通过 ServletConfig 对象就可以得到当前 servlet 的初始化参数信息。

获取方式如下:

    /**
     * 定义ServletConfig对象来接收配置的初始化参数
     */
    private ServletConfig config;
    
    /**
     * 初始化参数信息
     */
    @Override
    public void init(ServletConfig config) throws ServletException {
        this.config = config;
    }

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        //获取在web.xml中配置的初始化参数
        String nameStr = this.config.getInitParameter("name");
        String passwordStr = this.config.getInitParameter("password");
        response.getWriter().print(nameStr);
        response.getWriter().print(passwordStr);
      
    }

比如在使用 SpringMVC 的时候,在 web.xml 就使用到了 ServletConfig

    <servlet>
        <servlet-name>SpringMVC</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!--配置ServletConfig的初始化参数 -->
        <init-param>
            <description>SpringMVC</description>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring/spring-mvc.xml</param-value>
        </init-param>
        <!--web容器一旦加载就会创建这个servlet,并且会调用init方法-->
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>SpringMVC</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

1.4.2 ServletContext

  • WEB 容器在启动时,它会为每个 WEB 应用程序都创建一个对应的 ServletContext 对象,它代表当前 Web 应用。
  • ServletConfig 对象中维护了 ServletContext 对象的引用,开发人员在编写 servlet 时,可以通过 ServletConfig.getServletContext 方法获得 ServletContext 对象。
  • 由于一个 WEB 应用中的所有 Servlet 共享同一个 ServletContext 对象,因此 Servlet 对象之间可以通过 ServletContext 对象来实现通讯。ServletContext 对象通常也被称之为 context 域对象,也就是应用上下文。通常用途在读取对应的配置文件(xxx.properties)

image-20221227233457251

2. Tomcat

2.1 Tomcat是什么

Tomcat 的前身为 Catalina,而 Catalina 又是一个轻量级的 Servlet 容器,Tomcat 从 4.x版本开始除了作为支持 Servlet 的容器外,额外加入了很多的功能,比如:jsp、el、naming 等等,所以说Tomcat 不仅仅是 Catalina。

上文我们提到 servlet 运行在了 Tomcat 上,也就是说 Tomcat 是一个 Web 容器,Web 容器也是一种服务程序,遵守 J2EE 规范,当有客户端请求时,自动调用对应的 Servlet,请求规范为 ServletRequest、响应规范为 ServletResponse。。

项目 git 链接:https://github.com/apache/tomcat

小伙伴们可以 clone 到本地,编译源码,后续文章会对源码进行分析。


Tomcat 的核心就是接受 http 请求解析 http 请求并反馈客户端。

我们使用浏览器向某一个网站发起请求,发出的是 Http 请求,那么 HTTP 服务器接收到这个请求之后,会调用具体的程序对应的 Java 类进行处理,往往不同的请求由不同的 Java 类执行,如果由 HTTP 服务器直接调用业务代码类完成业务处理,那么整体项目会非常的耦合,不利于后续发展,而且 Tomcat 的定位是 Web 容器一定具有通用性。

所以 Tomcat 是这么处理的:

img

服务器接收到请求之后把请求交给 Servlet 容器来处理,Servlet 容器通过 Servlet 接口调用业务类。Servlet 接口和 Servlet 容器这一整套内容也就是文章开篇提到过的叫作 Servlet 规范。

这也是一种IOC的思想,控制反转,解耦的作用。


当然一个 Web 容器的工作远不如此,servlet 没有 main方法,也就是说 servlet的管理(启动,销毁等)都托管给了 Web 容器,Tomcat亦是如此。

2.2 Tomcat简易架构推演

Tomcat 是一个 Servlet 容器,承载着 n 个 servlet 实现,这也是最重要的。

那么Tomcat有可能是长这个样子的:

image-20221229232307723

谁来管理 Servlet 呢,谁来负责 Servlet 的装载,初始化,执行,资源回收呢?

另外假设在执行 Servlet 之前业务流想实现前置处理功能,类似 Filter 功能,该怎么办,于是我们考虑到为 Servlet 包装一层命名为 Wrapper 包装器,来完成扩展功能,一个 Wrapper 负责管理一个 Servlet。

现在 Tomcat 可能长成了这个样子:

image-20221229232316206

上文我们提到一个 Web 服务器可以承载 n 个 Servlet,用户指定 url,请求打到 Web 服务器上,由 Web 服务器接受请求,并找到请求对应的 Servlet,将请求转交给业务逻辑。

那谁来为 Request 匹配对应的 Servlet呢,匹配规则是根据URL和Servlet的映射关系。

我们引出一个 Context 上下文机制,它负责管理它里面的 Servlet 实例,并为 Request 匹配正确的Servlet,一个上下文代表一个应用程序(也就是日常开发中的一个Web应用)

现在 Tomcat 可能长这个样子:

image-20221230164715545

在我们现实生活中,一个应用都是部署在一个主机上的,所以一个主机可以包含多个应用,一个应用包含多个Servlet。

在 Tomcat 中,Host 表示虚拟主机,每个应用都可以对应一个 Host,Tomcat 在处理请求时,可以根据请求的域名进入到相应的 Host 中进行处理。

现在 Tomcat 可能长这个样子:

image-20221230191838720

现在我们提出一个引擎的概念,用于管理多个站点,一个 Service 最多只能有一个 Engine。

现在 Tomcat 可能长这个样子:

image-20230101192118102

好像还缺点东西,请求从客户端到达服务端的过程放在Service里没有达到职责单一的效果,现在引入一个 Connector 连接器的概念,专门负责将来自客户端的请求转发到引擎中,将 Service 和引擎连接起来。

现在 Tomcat 可能长这个样子:

image-20230101203730099

现在看起来我们推演的简易 Tomcat 核心流程基本可用了。

它的结构是这样的

image-20230101203958708

2.3 走进真正的Tomcat

2.3.1 Tomcat核心组件

上文我们进行了简易 Tomcat 的推演,现在让我们看看真正的 Tomcat 是如何设计的。

Tomcat 服务器的启动是基于一个 server.xml 文件的。

来到 Tomcat 的 conf 目录下的 server.xml 配置文件中。

<?xml version="1.0" encoding="UTF-8"?>
<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.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">
       <Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
            maxThreads="150" minSpareThreads="4"/>
      
        <Connector port="8080" protocol="HTTP/1.1"
                   connectionTimeout="20000"
                   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>

由 xml 配置可以看出

Server 包含了 Service,Service 包含了 Connector 和 Engine,Engine 包含了 Host,这么一看和我们简易 Tomcat 架构出奇的一致。

实际上 Tomcat 就是这样的设计。

下面我们通过 server.xml 配置来了解下 Tomcat 中的核心组件。

Server

Server 表示服务器,代表一个 Tomcat 实例,它提供了一种优雅的方式来启动和停止整个Tomcat服务器,不必单独启停连接器和容器,它是 Tomca 构成的顶层元素,所有的一切都在 Server 中。

Server 可以包含一个或多个 Service,其中每个 Service 都有自己的Engine 和 Connectors port="8005" 指定一个端口,这个端口负责监听关闭tomcat的请求。

Tomcat 启动时首先会启动一个 Server,Server 会启动 Service。

Service

Service 表示服务,Service可以运行多个服务,例如一个 Tomcat 中的交易服务,会员服务,退订服务等

包含一个Engine元素,以及一个或多个Connector,这些Connector元素共享一个Engine元素

Connector

Connector 表示连接器, 与客户程序实际交互的组件,它将 Service 和 Container(容器)连接起来,首先它需要注册到一个Service,它的作用就是接收客户端请求,把来自客户端的请求转发到 Container(容器),将结果响应给客户端,这就是它为什么称作连接器, 它支持的协议如下:

  • 支持AJP协议
  • 支持Http协议
  • 支持Https协议

属性说明:

port: 服务器连接器的端口号,该连接器将在指定端口侦听来自客户端的请求。

enableLookups: 如果为true,则可以通过调用request.getRemoteHost()进行DNS查询来得到远程客户端的实际主机名;若为false则不进行DNS查询,而是返回其ip地址。

redirectPort: 服务器正在处理http请求时收到了一个SSL传输请求后重定向的端口号。acceptCount:当所有可以使用的处理请求的线程都被用光时,可以放到处理队列中的请求数,超过这个数的请求将不予处理,而返回Connection refused错误。

connectionTimeout: 等待超时的时间数(以毫秒为单位)。

maxThreads: 设定在监听端口的线程的最大数目,这个值也决定了服务器可以同时响应客户请求的最大数目.默认值为200。

protocol: 必须设定为AJP/1.3协议。

address: 如果服务器有两个以上IP地址,该属性可以设定端口监听的IP地址,默认情况下,端口会监听服务器上所有IP地址。

minProcessors: 服务器启动时创建的处理请求的线程数,每个请求由一个线程负责。

maxProcessors: 最多可以创建的处理请求的线程数。

minSpareThreads: 最小备用线程。

maxSpareThreads: 最大备用线程。

debug: 日志等级。

disableUploadTimeout: 禁用上传超时,主要用于大数据上传时。


每个 Service 可以有一个或多个连接器 Connector 元素,xml 中 Connector元素定义了一个Http Connector,它通过 8080 端口接收 Http 请求。

容器概念

Container 表示容器,可以看作是 Servlet 容器。

Container 是容器的父接口,所有子容器都必须实现这个接口 Tomcat 中有四个子容器组件:Engine,Host,Context,Wrapper,四个组件是父子关系 Engine 包含Host,Host 包含 Context,Context 包含了 Wrapper

image-20230101220641077

Engine

Engine 表示引擎,用于管理多个站点,一个Service最多只能有一个Engine

Host

Host 表示虚拟主机,代表一个站点通过 server.xml 配置文件就可以添加 Host,一个 Host 可以运行多个 Context,但是在实践中,但 JVM 的处理能力有限,一般一个 Tomcat 实例只会配置一个 Host,也只会配置一个 Context。

Context

Context 表示上下文,代表一个应用程序,对应日常开发的一个 Web 应用,Context 最重要的功能就是管理它里面的 Servlet 实例,并为 Request 匹配正确的Servlet。

Servlet 实例在 Context 中是以 Wrapper 出现的。

Wrapper

Wrapper 表示包装器 一个 Wrapper 负责管理一个 Servlet,包括 Servlet 的装载,初始化,执行,及资源回收,Wrapper 是最底层的容器,没有子容器。

Service支撑组件

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

Listener

表示监听器

默认配置说明如下:


  <!--以日志形式输出服务器,操作系统,JVM的版本信息-->
    <Listener className="org.apache.catalina.startup.VersionLoggerListener"/>
 
    <!--加载(服务器启动)和销毁(服务器停止)APR,如果找不到APR库,则会输出日志,并不影响Tomcat启动-->
    <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on"/>
   
    <!--避免JRE内存泄漏问题-->
    <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener"/>

    <!--加载(服务器启动)和销毁(服务器停止)全局命令服务-->
    <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener"/>

    <!--在Context停止时重建Executor池中的线程,以避免ThreadLocal相关的内存泄漏-->
    <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener"/>

GlobalNamingResources

定义了服务器的全局JNDI资源

由此 Tomcat 的架构图如下所示:

image-20230101220931105

核心组件构成简化图如下:

image-20230102120616192

简单总结下:

一个 Tomcat 中只有一个 Server,一个 Server 可以包含多个 Service,一个 Service 只有一个Container,但是可以有多个 Connectors,这是因为一个服务可以有多个连接,如同时提供 Http 和Https 连接,也可以提供向相同协议不同端口的连接。

多个 Connector 和一个 Container 就形成了一个 Service,有了 Service 就可以对外提供服务了,Service 的生命周期托管给了 Server 掌管。

所以整个 Tomcat 的生命周期由 Server 控制。

2.3.2 Connector架构

从上文内容我们大致可以知道一个请求发送到 Tomcat 之后,首先经过 Service 然后会交给我们的Connector,Connector 用于接收请求并将接收的请求封装为 Request 和 Response 来具体处理,封装完之后再交由 Container 进行处理,Container 处理完请求之后再返回给 Connector,最后在由Connector 通过 Socket 将处理的结果返回给客户端,整个请求的就处理完毕了。

Connector 组件参数设置在 server.xml 中配置

 <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />

port 表示端口号,暴露一个socket端口来accept客户端的链接。

protocol 表示协议,Connector 默认支持两种协议:HTTP/1.1AJP/1.3。其中HTTP/1.1用于支持http1.1协议,而AJP/1.3用于支持对apache服务器的通信。

image-20230102120512110

Tomcat 中连接器的组件命名为 Coyote,客户端通过 Coyote 与服务器建立连接、发送请求并接受响应,Coyote 负责的是具体协议(应用层)和 IO 相关内容,它封装了底层的网络通信,使容器组件与具体的请求协议及IO操作方式完全解耦。

由此我们可以得出连接器的作用

  • 监听网络端口。
  • 接受网络连接请求。
  • 根据具体应用层协议(HTTP /AJP)解析字节流,生成统一的 Tomcat Request 对象,将 Tomcat Request 对象转成标准的 ServletRequest。
  • 调用 Servlet 容器,得到 ServletResponse,将 ServletResponse 转成 Tomcat Response 对象,将 Tomcat Response 转成网络字节流。
  • 将响应字节流写回给浏览器。


那你也许会好奇,Connector 如何接受请求并封装请求的?

首先看下 Connector 的结构图

image-20230102120549966

Connector 使用 ProtocolHandler 来处理请求的,ProtocolHandler 是 Coyote 协议接口,通过 Endpoint 和 Processor 实现针对具体协议的处理能力。

不同的 ProtocolHandler 代表不同的连接类型,例如如:Http11Protocol 使用的是普通 Socket 来连接的,Http11NioProtocol 使用的是 NioSocket 来连接的。


由包含了三个部件:Endpoint、Processor、Adapter

  • Endpoint 是 Coyote 通信端点,是对传输层的抽象,用来监听通信端口处理底层 Socket 的网络连接,具体 Socket 的接收与法,Endpoint 是用来实现 TCP/IP 协议的。
  • Processor 是 Coyote 协议处理接口,是对应用层协议的抽象,接受来自 EndPoint 的 Socket,读取字节流解析成 Tomcat Request和Response,并通过Adapter将其提交到容器处理,Processor 用来实现 HTTP 协议的。
  • Adapter 用于将 Request 适配到 Servlet 容器进行具体的处理。

Endpoint 的抽象实现 AbstractEndpoint 里面实现了对 Acceptor 和 AsyncTimeout 两个核心线程的创建。

  • Acceptor 用于监听请求。
  • AsyncTimeout 用于检查异步 Request 的超时。

2.3.3 管道与阀门

在一个比较复杂的大型系统中,如果一个对象或数据流需要进行繁杂的逻辑处理,我们可以选择在一个大的组件中直接处理这些繁杂的业务逻辑, 这个方式虽然达到目的,但扩展性和可重用性较差, 因为可能牵一发而动全身,那怎么设计比较合理呢。

上文中我们了解到了 Container 的结构,现在我们想一下,Tomcat 是如何从 Engine 一层一层执行直到 Wrapper。

Tomcat 引入了Pipeline-Valve管道-阀门机制,可以理解成是责任链模式。

用一条管道把多个对象(阀门部件)连接起来,整体看起来就像若干个阀门嵌套在管道中一样,而处理逻辑放在阀门上。

image-20230102130743547

每一个组件都会有一个 Pipeline 结构,里面包含了 n 个Valve,每个 Pipeline 都有特定的 Valve,而且是在管道的最后一个执行,这个 Valve 叫做 BaseValve(不可删除的)。若配置了一个 Valve1,则加在最前面,若没有配置,则走默认的Pipeline流程(责任链模式)

在上层容器的管道的BaseValve中会调用下层容器的管道。


Container 包含四个子容器,而这四个子容器对应的 BaseValve 分别在:StandardEngineValve、StandardHostValve、StandardContextValve、StandardWrapperValve。

image-20230102130554163

  1. Connector 在接收到请求后会首先调用最顶层容器的 Pipeline 来处理,这里的最顶层容器的 Pipeline 就是 EnginePipeline(Engine的管道)。
  2. 在 Engine 的管道中如果配置了其它 Value 则会依次执行 EngineValve1、EngineValve2 等等,最后会执行 StandardEngineValve,在 StandardEngineValve 中会调用 Host 管道,
  3. 在 Host 的管道中如果配置了其它 Value,则会依次执行 HostValve1、HostValve2 等,最后在执行 StandardHostValve,然后再依次调用 Context 的管道和 Wrapper 的管道,最后执行到StandardWrapperValve。
  4. 当执行到 StandardWrapperValve 的时候,会在 StandardWrapperValve 中创建FilterChain,并调用其 doFilter 方法来处理请求,这个 FilterChain 包含着我们配置的与请求相匹配的 Filter 和 Servlet,其 doFilter 方法会依次调用所有的 Filter 的 doFilter 方法和 Servlet 的 service 方法处理请求。
  5. 当所有的 Pipeline-Valve 都执行完之后,并且处理完了具体的请求,这个时候就可以将返回的结果交给 Connector 了,Connector 在通过 Socket 的方式将结果返回给客户端。

2.3.4 生命周期机制LifeCycle

Tomcat 组件生命周期机制非常规范,统一接口管理,它就是 Lifecycle。

实际上它就是一个状态机,实现对组件的由初始化到销毁状态的管理。

public interface Lifecycle {
    // 添加监听器
    public void addLifecycleListener(LifecycleListener listener);
    // 获取所以监听器
    public LifecycleListener[] findLifecycleListeners();
    // 移除某个监听器
    public void removeLifecycleListener(LifecycleListener listener);
    // 初始化方法
    public void init() throws LifecycleException;
    // 启动方法
    public void start() throws LifecycleException;
    // 停止方法
    public void stop() throws LifecycleException;
    // 销毁方法
    public void destroy() throws LifecycleException;
    // 获取生命周期状态
    public LifecycleState getState();
    // 获取字符串类型的生命周期状态
    public String getStateName();
}

LifecycleBase 是 Lifecycle 的基本实现。

容器组件 StandardEngine,StandardHost,StandardContext,StandardWrapper 等都继承与它。

简单代码展示,此处后续文章源码分析中会详细说明

image-20230102133922591

image-20230102133935654

统一生命周期管理机制的好处就是,Engine 中注册了几个 Host的 Listener,Host 中注册了几个Context 的 Listener,于是当 Engine 调用 fireLifecycleEvent 时,遍历了所有的 Host,并通知 Host,Host接到通知,由于上级容器 start 了 Host 也进行 start,同时继续自身的 fireLifecycleEvent,这样不断反复,就能够将所有的子组件全部启动,这种方式,比显式的写一堆组件的start要来得更加简洁清晰。

实际上 Lifecycle 是个典型的观察者设计模式。

3.小节

本文首先回顾了 Servlet,然后对我们想象中一个 Web 容器的架构进行了推演,引出真正的 Tomcat 的架构设计,并介绍了 Tomcat 中的核心组件和特殊机制。

下面从一个完整请求的角度来看 Tomcat 处理过程来将上文贯穿起来。

image-20230102120405134

image-20230102120420619

假设来自客户的请求为:http://localhost:8080/demo2/info 请求被发送到本机端口 8080,被在那里侦听的 Coyote HTTP/1.1 Connector,然后

  • Connector 把该请求交给它所在的 Service 的 Engine 来处理,并等待 Engine 的回应
  • Engine 获得请求localhost:8080/demo2/info,匹配它所有虚拟主机 Host
  • Engine 匹配到名为 localhost 的 Host(即使匹配不到也把请求交给该 Host 处理,因为该 Host 被定义为该 Engine 的默认主机)。
  • localhost Host 获得请求 /demo2/info,匹配它所拥有的所有 Context。
  • Host 匹配到路径为 /demo2 的 Context (如果匹配不到就把该请求交给路径名为""的 Context 去处理)。
  • path = "/demo2" 的 Context 获得请求 /info,在它的 mapping table 中寻找对应的 servlet
  • Context 匹配到 URL PATTERN 为 /info 的servlet,对应于 HttpServlet 类,注意在 Tomcat 中 Servlet 是以 Wrapper 包装类的形式出现。构造HttpServletRequest 对象和 HttpServletResponse 对象,作为参数调用 HttpServlet 的doGet 或 doPost 方法。
  • Context 把执行完了之后的 HttpServletResponse 对象返回给 Host。
  • Host 把 HttpServletResponse 对象返回给 Engine。
  • Engine 把 HttpServletResponse 对象返回给 Connector。
  • Connector 把HttpServletResponse 对象返回给客户 browser。

image.png

作者:MRyan


本文采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。
转载时请注明本文出处及文章链接。本文链接:https://www.wormholestack.com/archives/641/
2024 © MRyan 112 ms