tomcat源码学习


tomcat源码学习

看到一篇文章深度剖析tomcat源码,以后可能要做相关的安全研究,就跟源码学习一下。链接放在文章最后了。当然,我看源码的角度是从安全研究出发,可能和Java开发大佬的思路不太相同qwq。

tomcat架构设计

tomcat功能分析

我们平常管tomcat叫中间件,主要目的是实现web请求的处理,实现客户端和服务端的通信。

HTTP服务器

一般来说,最广泛的应用通信就是实现HTTP协议,实现web服务。我们希望tomcat能够接受并且处理http请求,作为http服务器,进行数据传送通信。

我们来回忆一下计算机学院必教的《计算机通信原理》,看看一个完整的http通信过程是什么样的:

img

http协议是应用层协议,基于TCP。图中步骤总共有十一步:

  1. 用户通过浏览器发起请求
  2. 浏览器向服务器发起TCP连接请求
  3. 经典TCP三次握手建立连接
  4. 浏览器将请求的数据打包成一个HTTP协议格式的数据包
  5. 数据包传输到服务端
  6. 服务端解包分析
  7. 服务端解析完,处理请求
  8. 服务端将处理完的结果使用HTTP协议打包
  9. 服务端发送包含处理结果的HTTP数据包
  10. 浏览器拿到HTTP数据包,解包
  11. 浏览器将解析后的数据呈现给用户

所以为了实现以上十一条,tomcat作为一个HTTP服务器,只要功能就是就是

  • 建立连接
  • 解析请求数据
  • 处理请求
  • 发送响应数据

关于处理请求的部分:

当我们使用浏览器向网站发起HTTP请求,服务端收到这个请求后,会调用具体的程序(Java类)进行处理。不同的请求需要由不同的Java类进行处理,我们需要让HTTP服务器知道针对不同的请求使用哪个Java类中的哪个方法。

img

servlet容器

面向接口编程:我们可以定义一个servlet接口,所有的业务类都实现这个接口。我们还需要解决servlet定位问题,当HTTP请求到来时,tomcat如何知道该由哪个servlet进行处理。于是出现了servlet容器。Servlet 容器作为 HTTP 服务器和具体业务类进行交互的桥梁,HTTP 服务器将请求交由 Servlet 容器去处理,而 Servlet 容器则负责将请求转发到具体的 Servlet,并且调用 Servlet 的方法进行业务处理,它们之间的调用通过 Servlet 接口进行解耦。

servlet接口和容器是由JavaEE进行定义,tomcat只是对其进行了实现。

img

Tomcat Servlet 容器工作流程

  1. HTTP服务器把请求信息使用ServletRequest对象封装起来
  2. 进一步调用Servlet 容器中具体的Servlet
  3. 根据URL和Servlet的映射关系,找到相应的Servlet
  4. 如果Servlet还没有被加载,就使用反射机制创建此Servlet并调用其init方法初始化
  5. 接着调用具体Servletservice方法来处理请求,请求结果使用ServletResponse 对象封装
  6. ServletResponse 对象返回给HTTP服务器,HTTP服务器把相应发送回客户端

img

web服务器

根据上述分析,我们知道了 Tomcat 要实现成 “HTTP 服务器 + Servlet 容器”,也就是所谓的 Web 服务器。
而作为一个 Web 服务器,Tomcat 要实现两个非常核心的功能:

  • Http 服务器功能:进行 Socket 通信(基于 TCP/IP),解析 HTTP 报文
  • Servlet 容器功能:加载和管理 Servlet,由 Servlet 具体负责处理 Request 请求

img

tomcat组件设计

连接器和容器

  • 连接器(Connector)负责对外交流,完成HTTP服务器的功能
  • 容器(Container)负责内部处理,完成servlet功能

因为连接器主要是对外交流,我们研究的是tomcat本身处理请求的过程,所以只对容器进行研究说明:

容器的设计

容器部分设计了4种容器,分别是Engine、Host、Context、Wrapper。这四种容器是父子关系,整体形成一个分层结构,如下图所示。

img

  • Engine 表示整个Catalina的Servlet引擎,用来管理多个虚拟站点。一个Service最多只能有一个Engine,但一个Engine可以包含多个Host
  • Host 代表一个虚拟主机或者一个站点,可以给Tomcat配置多个虚拟主机地址,一个虚拟主机下可以包含多个Context
  • Context 表示一个web应用程序,可包含多个Wrapper
  • Wrapper 表示一个Servlet,负责整个Servlet的生命周期。

Catalina 也是 Tomcat 中的一个组件,它负责的是解析 Tomcat 的配置文件(server.xml),以此来创建服务器 Server 组件并进行管理。

因此也可以认为整个 Tomcat 就是一个 Catalina 实例,Tomcat 启动的时候会初始化这个实例,Catalina 实例通过加载server.xml 完成其他实例的创建,创建并管理一个 Server,Server 创建并管理多个服务, 每个服务又可以有多个Connector 和一个 Container。

tomcat架构汇总

配置文件

其实这些组件的设计更多是为了使用者能够灵活的进行 web 项目部署配置,因此我们将其抽取成一个配置文件,名为 server.xml,如下图所示,在配置文件中你也能很清晰的对应上这些层级关系。

tomcat模块分层结构

Tomcat 是一个由一系列可配置的组件构成的 Web 容器,在实现时根据不同的功能 Tomcat 内部进行了模块分层,其中 Catalina 模块作为 Tomcat 的 servlet 容器实现,它是 Tomcat 的核心模块。因为从另一个角度来说,Tomcat 本质上就是一款 Servlet 容器。而其他模块的设计都是为 Catalina 提供支撑的。
相关模块的功能说明如下:

img

整体模块分层结构图如下:

img

总体架构

img

  • Listener 可以在Tomcat生命周期中完成某些容器相关的监听器(研究内存马时会再次提到)
  • JNDI JNDI是 Java 命名与目录接口,是属于 J2EE 规范的,Tomcat 对其进行了实现。JNDI 在 J2EE 中的角色就是“交换机”,即 J2EE 组件在运行时间接地查找其他组件、资源或服务的通用机制(你可以简单理解为给资源取个名字,再根据名字来找资源)
  • Cluster组件 提供了集群功能,可以将对应容器需要共享的数据同步到集群中的其他Tomcat实例中
  • Realm组件 提供了容器级别的用户-密码-权限数据对象
  • Loader组件 web应用加载器,用于加载Web应用的资源,保证不同Web应用之间的资源隔离
  • Manager租价 Servlet映射器,属于Context内部的路由映射器,只负责当前Context容器的路由导航

手撸tomcat源码

tomcat源码构建

  • 解压源码包,进入 apache-tomcat-8.5.50-src 目录
  • 在当前目录中创建 source 文件夹,然后将 conf、webapps 目录移动到 source 文件夹中
  • 在当前目录下创建 pom.xml,文件内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>apache-tomcat-8.5.50-src</artifactId>
    <name>Tomcat8.5</name>
    <version>8.5</version>
    <build>
        <!--指定源目录-->
        <finalName>Tomcat8.5</finalName>
        <sourceDirectory>java</sourceDirectory>
        <resources>
            <resource>
                <directory>java</directory>
            </resource>
        </resources>
        <plugins>
            <!--引入编译插件-->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version>
                <configuration>
                    <encoding>UTF-8</encoding>
                    <source>11</source>
                    <target>11</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
    <!--tomcat依赖的基础包-->
    <dependencies>
        <dependency>
            <groupId>org.easymock</groupId>
            <artifactId>easymock</artifactId>
            <version>3.4</version>
        </dependency>
        <dependency>
            <groupId>ant</groupId>
            <artifactId>ant</artifactId>
            <version>1.7.0</version>
        </dependency>
        <dependency>
            <groupId>wsdl4j</groupId>
            <artifactId>wsdl4j</artifactId>
            <version>1.6.2</version>
        </dependency>
        <dependency>
            <groupId>javax.xml</groupId>
            <artifactId>jaxrpc</artifactId>
            <version>1.1</version>
        </dependency>
        <dependency>
            <groupId>org.eclipse.jdt.core.compiler</groupId>
            <artifactId>ecj</artifactId>
            <version>4.5.1</version>
        </dependency>
        <dependency>
            <groupId>javax.xml.soap</groupId>
            <artifactId>javax.xml.soap-api</artifactId>
            <version>1.4.0</version>
        </dependency>
    </dependencies>
</project>

搜索Bootstrap类的main方法,运行,根据报错使用idea自动修补报错,然后给 tomcat 的源码程序启动类 Bootstrap 配置VM参数(注意路径需要修改为自己的项目位置),因为 tomcat 源码运行也需要加载配置文件等。

-Dcatalina.home=C:/Users/86178/Desktop/apache-tomcat-8.5.50-src/source
-Dcatalina.base=C:/Users/86178/Desktop/apache-tomcat-8.5.50-src/source
-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
-Djava.util.logging.config.file=C:/Users/86178/Desktop/apache-tomcat-8.5.50-src/source/conf/logging.properties

最后还需要在 ContextConfig 类中的 configureStart 方法中增加一行代码将 Jsp 引擎初始化,如下

image-20220503205233975

重新跑一遍,启动正常。访问127.0.0.1:8080成功。

tomcat启动流程分析

tomcat启动入口

在tomcat源码下有个bin文件夹,其中包含了适用于各平台的启动脚本

image-20220503205506459

我们看看Linux下的stratup.sh:

os400=false
case "`uname`" in
OS400*) os400=true;;
esac

# resolve links - $0 may be a softlink
PRG="$0"

while [ -h "$PRG" ] ; do
  ls=`ls -ld "$PRG"`
  link=`expr "$ls" : '.*-> \(.*\)$'`
  if expr "$link" : '/.*' > /dev/null; then
    PRG="$link"
  else
    PRG=`dirname "$PRG"`/"$link"
  fi
done

PRGDIR=`dirname "$PRG"`
EXECUTABLE=catalina.sh

# Check that target executable exists
if $os400; then
  # -x will Only work on the os400 if the files are:
  # 1. owned by the user
  # 2. owned by the PRIMARY group of the user
  # this will not work if the user belongs in secondary groups
  eval
else
  if [ ! -x "$PRGDIR"/"$EXECUTABLE" ]; then
    echo "Cannot find $PRGDIR/$EXECUTABLE"
    echo "The file is absent or does not have execute permission"
    echo "This file is needed to run this program"
    exit 1
  fi
fi

exec "$PRGDIR"/"$EXECUTABLE" start "$@"

最后就是执行了catalina.sh start。我们再去看看catalina.sh的内容:

核心内容如下图所示:

image-20220505162855936

附加了很多JVM参数之后执行了Bootstrap类,就来到了Bootstrap类的main方法,也即是tomcat的启动入口。

Bootstrap逐级初始化

我们来看看Bootstrap类的main方法:

image-20220509101434791

先实例化一个Bootstrap类,然后执行了bootstrap.init方法进行初始化。我们跟入看看init方法的具体代码及其实现:

public void init() throws Exception {

        initClassLoaders();

        Thread.currentThread().setContextClassLoader(catalinaLoader);

        SecurityClassLoad.securityClassLoad(catalinaLoader);

        // Load our startup class and call its process() method
        if (log.isDebugEnabled())
            log.debug("Loading startup class");
        Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
        Object startupInstance = startupClass.getConstructor().newInstance();

        // Set the shared extensions class loader
        if (log.isDebugEnabled())
            log.debug("Setting startup class properties");
        String methodName = "setParentClassLoader";
        Class<?> paramTypes[] = new Class[1];
        paramTypes[0] = Class.forName("java.lang.ClassLoader");
        Object paramValues[] = new Object[1];
        paramValues[0] = sharedLoader;
        Method method =
            startupInstance.getClass().getMethod(methodName, paramTypes);
        method.invoke(startupInstance, paramValues);

        catalinaDaemon = startupInstance;
    }

先执行了initClassLoaders();方法,进行类加载器的初始化。在此方法中创建了commonLoader,catalinaLoader,sharedLoader。创建类加载器的方法名叫createClassLoader,其中commonLoader创建失败时会使用反射重新创建。

if (commonLoader == null) {
    commonLoader = this.getClass().getClassLoader();
}

这里有tomcat类加载机制的相关知识需要学习。挂一些链接有空总结吧

接着看init方法:我们看到了熟悉的反射操作,反射获取Catalina类并且进行实例化

image-20220509110043884

接着反射调用Catalina类中的setParentClassLoader类,最后将实例化后的对象startupInstance赋值给catalinaDaemon,init方法结束。

回到main函数中,将初始化完成的Bootstrap实例化对象bootstrap赋值给daemon。接着对daemon进行操作(daemon是Bootstrap类的实例)。看到下面的try里一堆if判断匹配参数,我们启动tomcat给的参数肯定是start,于是跳到start:

image-20220509113013163

主要执行了load和start方法。在 load 方法中实际是使用反射调用了 Catalina.load() 方法。这个load方法实际上就是tomcat逐级初始化的主要方法,也是我们重点关注的方法。

我们定位到Catalina#load方法,第一个try-catch是对配置文件进行一些操作,直到其结束后进行了getServer方法中的操作:

getServer().setCatalina(this);//getServer方法
getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());
getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());

getServer() 返回的是一个 StandardServer 对象,分别对该对象设置了Catalina 对象、catalinaHomecatalinaBase 属性,catalinahome 是咱们 tomcat 的安装目录,catalinabase 是工作目录,其实这两个都是我们在构建源码时配置的 tomcat 源码下的 source 目录,最后关键的一步是调用了 serverinit 方法,即 server 组件的初始化。我们进入init方法,可以看到此init方法实际上是LifecycleBase类的init方法。

进入到LifecycleBase#init方法中,跟进initInternal();

image-20220511210448746

这个方法实际上是被定义于StandardServer,而StandardServer也是LifecycleBase的子类。直接跟到此方法最后,遍历所有service,并且进行初始化:

image-20220511210911641

我们跟进此init方法,发现回到了LifecycleBase中的init方法。再次步入到其initInternal方法中,回到了StandardServer类下的该方法。可以看到下面有各种组件的初始化加载行为。

image-20220512204730730

这些组件都有一个共性,就是都继承了 LifecycleBase 抽象类并实现了其中的抽象方法。

逐级启动

看完了Bootstrapload方法,我们来看看紧接着的start方法。回到Bootstrap类中,跟进daemon.start();

public void start() throws Exception {
    if (catalinaDaemon == null) {
        init();
    }

    Method method = catalinaDaemon.getClass().getMethod("start", (Class [])null);
    method.invoke(catalinaDaemon, (Object [])null);
}

先是判断Catalina类的实例catalinaDaemon是否存在,然后使用反射调用其start方法。接着跟进,来到Catalina#start中,运行到如图位置:

image-20220512205939182

这里调用到了getServer().start();,进行初始启动。我们跟入,与load类似,state值为INITIALIZED时,会执行此类中的startInternal();方法。进入后遍历所有service,并且start,逐级启动。

image-20220512210704802

img

Lifecycle 接口

生命周期机制

在调试过程中,我们可以发现,tomcat很规范地为几乎每个组件都实现了以下方法:

  • init
  • start
  • stop
  • destory

这些方法都定义在Lifecycle接口中,也就是存在于所谓的生命周期中。对于这些组件来说不变点就是每个组件的生命周期是一致的,即它们都要经历创建初始化启动停止销毁这几个过程,在这个过程中组件的状态和状态之间的转化也是不变的。而其中的变化点则是某个具体的组件在执行某个过程时是有所差异的。

reference:


文章作者: m1yuu
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 m1yuu !
  目录