tomcat和Jetty概述

马上年底了,渗透任务渐少,最近需要写内存马工具了,读了一下《深入剖析Tomcat》,理解的还比较浅,做个记录。第一次发这篇文章是在2021年一月,当时读完深入剖析Tomcat做了个笔记。现在的文章是2022年五月进行了重新的修改,在写完内存马工具之后,对于Tomcat又有了新的理解。另外,文章加入了读李号双深入拆解Tomcat的总结。

1. Servlet

一般我们写个普通的Java程序Hello World,用到的是JavaSE(基于JDK的开发),而要开发Java Web程序用到的是JavaEE(基于JavaSE,基于服务器的组件、API标准)。Servlet是JavaEE规范中的一个,所有支持Servlet API的Web服务器也称为Servlet容器。Servlet容器有Tomcat、Weblogic、Jboss、Jetty、Resin、TongWeb等。这篇文章写的是Tomcat。同样Tomcat也具备了HTTP请求的功能,也就是它可以处理Socket连接(TCP/IP层)。

Servlet需要用到javax.servlet和javax.servlet.http。

javax.servlet.servlet接口中声明了五个方法,包括init\service\destory\getServletConfig\getServletInfo。前三个属于生命周期,一旦实例化servlet类,就会调用唯一一次init()方法。当servlet的客户端请求(request)到达后,servlet容器调用service方法将javax.servlet.servletRequest对象和javax.servlet.servletResponse对象作为参数传入。service方法在servlet生命周期内会被多次调用。载入servlet类可以使用java.net.URLClassLoader类的loadClass方法。

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

javax.servlet.http中有HttpServlet类,变量中包含了HTTP各种method,如DELETE、HEAD、GET、OPTIONS、POST、PUT、TRACE等。对于每种method都有doXXX的方法,以get为例,doGet方法如下:

   protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String msg = lStrings.getString("http.method_get_not_supported");
        this.sendMethodNotAllowed(req, resp, msg);
    }

这些doXXX的方法都被集成在该类的service方法中被调用。

我们想要写一个servlet,就需要继承HttpServlet,然后重写doGet或doPost方法。但是有些地址访问的时候会出现重定向,那么要在doGet中加一句resp.sendRedirect(redirectToUrl);。重定向有两种:一种是302响应,称为临时重定向,一种是301响应,称为永久重定向。两者的区别是,如果服务器发送301永久重定向响应,浏览器会缓存/hi到/hello这个重定向的关联,下次请求/hi的时候,浏览器就直接发送/hello请求了。还有一种内部转发,就是此servlet会交给另一个servlet来处理这个请求。

另外,需要提到的是支持servlet API的服务器除了Tomcat,Jetty、GlassFish、WebLogic、WebSphere这些服务器也支持。所以在内存马应用的时候一般分为Tomcat内存马、Weblogic内存马。

2. 架构

先放上一张网上的Tomcat总体架构图。可以看到主要分为两部分:连接器connector和容器container。连接器解决的是HTTP请求和解析的问题,将网络字节流转化成Request和Response对象。容器解决的是加载、管理Servlet并具体Request请求。连接器负责对外交流,容器则是内部管理。

Tomcat架构图

架构图转换成具体的流程架构图如下


流程架构图

连接器Connector

连接器解决了网络请求和解析的问题,无论协议是什么,最终在容器中获取到的都是ServletRequest对象。连接器具备的功能包括:
(1)网络通信的功能:监听网络端口、接收网络请求、读取网络请求字节流——EndPoint
(2)应用层协议解析:根据协议(HTTP/AJP)解析字节流生成Tomcat Request对象、将Tomcat Response转换成网络字节流——Processor
(3)Tomcat和Servlet标准对象之间转化:将Tomcat Request对象转换成符合Servlet标准的ServletRequest对象,调用Servlet容器得到ServletResponse转换成Tomcat Response对象——Adapter
从网络处理的逻辑来看,EndPoint 提供字节流给ProcessorProcessor提供Tomcat Request对象给 AdapterAdapter提供ServletRequest对象给容器Container。这里要提一句,为什么要对Request进行转换。因为解析HTTP生成的是Tomcat自定义的Request,但它不符合Servlet规范,所以无法调用Servlet容器中的内容。Tomcat引入的CoyoteAdapter适配器就是为了解决这个问题。

网络通信的I/O模型可能是非阻塞 I/O、异步 I/O 或者 APR。应用层协议则可能是HTTP、HTTPS或AJP的,二者之间有多种组合,所以Tomcat设计了ProtocolHandler接口来封装这二者。换言之ProtocolHandler组件包含了EndPointProcessor。根据不同的I/O模型和应用协议对接口进行不同的实现,如Http11NioProtocol、AjpNioProtocol等。但是每种协议也有自己的抽象基类:AbstractHttp11Protocol、AbstractAjpProtocol。

EndPoint:是通信端点,即通信监听的接口,接收和发送具体的Socket,实现了TCP/IP 协议。其抽象实现类是 AbstractEndpoint,具体实现类根据IO模型的不同包括:NioEndpoint 和 Nio2Endpoint 。监听接口的功能由其子类Acceptor来实现,处理Socket请求则是SocketProcessor实现,它会将请求交给线程池(也叫执行器,Executor)。

Processor:接收来自EndPoint的Socket,读取字节流解析成Tomcat Request和Response。实现的是HTTP协议,根据协议的不同具体的实现类包括 AJPProcessor、 HTTP11Processor 等,实现了协议的解析方法和请求处理方法。

容器Container

根据流程架构图可以看出,容器采用的不是平行的模块设计,而是父子关系Engine->Host->Context->Wrapper,即引擎->虚拟主机->WEB应用程序->Servlet。父到子是一对多的关系,例如一个虚拟主机。这种层次关系也在Tomcat的server.xml 配置文件有所体现。所有这些容器组件都实现了Container接口,采用组合模式进行管理。Container实现了Lifecycle接口,统一管理各组件的生命周期。

public interface Container extends Lifecycle {
    public void setName(String name);
    public Container getParent();
    public void setParent(Container container);
    public void addChild(Container child);
    public void removeChild(Container child);
    public Container findChild(String name);
}

对于多层次的设计结构,有个问题是ServletRequest最终对应的是那个Wrapper?Tomcat设计了Mapper结构,该结构保存了Web应用的配置信息:组件与访问路径的映射关系。将用户请求的URL定位到一个Servlet。


映射关系

从Engine到Wrapper逐层传递请求的过程,是通过Pipeline-Valve来实现的。它的设计思想是责任链模式。也就是请求处理过程中有多个Valve阀对请求进行处理,每个Valve处理完自己的职责后传递给下一个Valve。

public interface Valve {
    public Valve getNext();  //获取下一个节点
    public void setNext(Valve valve);
    public void invoke(Request request, Response response) //处理请求
}

public interface Pipeline extends Contained { //Pipeline维护valve链表
    public void addValve(Valve valve);
    public Valve getBasic();
    public void setBasic(Valve valve);
    public Valve getFirst();
}

可以看到Pipeline中并不存在invoke这种调用方法,实际的方法调用都是由Valve来执行。Pipeline中有getBasic和setBasic。这里的一个知识点:Basic Valve是Valve链的末端节点,负责调用下层容器组件Pipeline的第一个Valve。


Pipeline与Valve

通过上图可以看到Basic调用的是下层容器的第一个Valve。而Engine作为顶层容器,它的Valve是由Adapter调用的

connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);

Wrapper容器的最后一个 Valve会创建一个FilterChain,并调用 doFilter() 方法,最终调用Servlet的service方法。

3.生命周期管理

Container中提到,它实现了Lifecycle接口,统一管理组件的生命周期。Tomcat有很多组件,在服务启动时需要创建这些组件;服务停止时需要销毁组件。也就是这些组件的生命周期需要被动态的管理。李号双在讨论Tomcat时提到,程序设计需要找到系统的变化点和不变点。不变点可以被抽象成一个接口。无论是哪个组件都要经历初始化、启动、停止、销毁的过程,只是具体的实现方式不同。所以可以认为初始化等过程是一个不变点,可以被抽象为LifeCycle(生命周期),包含init()、start()、stop()、destory()。上面的容器章节中还提到,Container的管理是采用的组合模式,在生命周期中可以理解为,当父组件调用init()时会调用其子组件的init()

组件的init()和start()都是由父组件状态变化触发的,状态的变化可以看作是一个事件。如果想要修改init()或start()的逻辑,可以通过事件监听器(观察者模式)来实现,而无需更改组件本身的代码。也就是在LifeCycle中加入两个方法:添加和删除监听器(addLifecycleListener、removeLifecycleListener)。另外需要定义一下组件生命周期有哪些状态,如NEW、INITIALIZING、INITIALIZED、 STARTING_PREP、STARTING、STARTED等。

Lifecycle接口的实现类有很多,因为每个组件都有自己的生命周期。但是往往这些实现中又存在相同的逻辑,例如状态的改变、事件的触发等。这些公用逻辑就可以被放入到基类中。然后让子类继承这个基类,就实现了代码的重用。基类中会存在一些抽象方法,也就是基类中不作具体实现的方法,由子类来实现。它是逻辑的骨架。

// LifeCycleBase.init
public final synchronized void init() throws LifecycleException {
    if (!state.equals(LifecycleState.NEW)) {//状态合法性检查,例如当前状态必须是new才能进行实例化
        invalidTransition(Lifecycle.BEFORE_INIT_EVENT);
    {
    try{
        setStateInternal(LifecycleState.INITIALIZING, null, false); //触发INITIALIZING事件监听器 
        initInternal();  //调用具体子类的初始化方法,进而调用子组件的init方法
        setStateInternal(LifecycleState.INITIALIZED, null, false); //触发INITIALIZED事件监听器 
    }catch(Throwable t){...}
}

LifeCycleBase调用监听器的方法,那么这里还涉及到一个问题,监听器在什么时候如何被加入到系统中?主要分为两种情况:(1)Tomcat自定义监听器,在父组件创建子组件的过程中注册到子组件(2)server.xml中用户自定义的监听器,Tomcat启动时自动解析server.xml。

4. Server和Service

Tomcat的启动是通过/bin目录下的startup.sh来启动JVM,运行启动类Bootstrap。Bootstrap初始化类加载器,并实例化Catalina。Catalina解析server.xml并创建Server组件。Server启动Service组件。Service启动容器和连接器。这些启动相关的类或组件并不处理具体请求,主要负责管理。

startup.sh -> Bootstrap -> Catalina -> Server -> Service

Catalina

public void start() {
    if (getServer() == null) { // 如果Server 实例为空,就解析 server.xml 创建Server
        load();
    }
    
    if (getServer() == null) {
        log.fatal(sm.getString("catalina.noServer")); // 如果创建失败,报错退出
        return;
    }
 
    try {
        getServer().start();   // 启动 Server
    } catch (LifecycleException e) {
        return;
    }
 
    if (useShutdownHook) { // 创建并注册关闭钩子
        if (shutdownHook == null) {
            shutdownHook = new CatalinaShutdownHook();
        }
        Runtime.getRuntime().addShutdownHook(shutdownHook);
    }
 
    if (await) { // 用 await 方法监听停止请求
        await();
        stop();
    }
}

5.I/O模型

UNIX 系统下的 I/O 模型有 5 种:同步阻塞 I/O、同步非阻塞 I/O、I/O 多路复用、信号驱动 I/O 和异步 I/O。所谓的I/O就是就是计算机内存与外部设备之间拷贝数据的过程。网络I/O通信的过程中会涉及到两个对象:调用I/O操作的用户线程、操作系统内核。用户线程是不能直接访问内核空间的。用户线程发起I/O操作后,网络数据读取主要有两步:
(1)用户线程等待内核将数据从网卡拷贝到内核空间
(2)内核将数据从内核空间拷贝到用户空间
而各类I/O模型的区别就是它们实现这两个步骤的方式不同。I/O模型图示如下:

I/O模型

同步阻塞是用户线程发起read后就阻塞了,让出CPU。直到数据从内核空间拷贝到用户空间,再把用户线程唤醒。同步非阻塞就是用户线程不断发起read,数据如果没到内核空间就会返回失败。直到数据到了内核空间,并向用户空间拷贝的过程中阻塞。多路复用是线程先发起select,询问内核空间的数据是否准备完成。一旦内核数据准备好了用户线程发起read。称为多路复用的原因是,一次select掉用可以向内核查询多个数据通道Channel的状态。异步是用户线程发起read的同时注册一个回调函数,然后返回read。内核数据准备完成后再调用指定回调函数完成处理,这个过程中用户线程一直没有阻塞。

NioEndPoint

Tomcat的NioEndPoint组件实现的就是I/O多路复用模型。创建Selector后注册相应事件,然后调用select方法,等待事件发生。一旦发生就创建一个新的线程从Channel中读数据。

NioEndPoint工作流程

NioEndPoint共有五个组件:LimitLatch、Acceptor、Poller、SocketProcessorExecutor

LimitLatch:控制最大连接数。NIO 模式下默认是 10000,达到这个阈值后,连接请求被拒绝。
Acceptor:监听连接请求,在死循环中调用accept方法接收新的连接。一旦有新的连接就返回一个Channel对象并交给Poller处理。(位于单独线程)
Poller:检测Channel的I/O事件,一旦有Channel可读就创建SocketProcessor任务类给线程池Executor处理。(位于单独线程,本质是一个Selector)
Executor:线程池,负责运行SocketProcessor任务类,SocketProcessor的run方法会调用Http11Processor(应用层协议的封装)来读取和解析请求数据。

Nio2EndPoint

Nio是同步非阻塞,NIO.2 则是异步。Java NIO.2实现服务器Demo如下

// 异步服务器
public class Nio2Server {
 
   void listen(){
      //1. 创建一个线程池,用来执行来自内核的回调请求
      ExecutorService es = Executors.newCachedThreadPool();
 
      //2. 创建异步通道群组,并绑定一个线程池
      AsynchronousChannelGroup tg = AsynchronousChannelGroup.withCachedThreadPool(es, 1);
      
      //3. 创建服务端异步通道,绑定到异步通道群组
      AsynchronousServerSocketChannel assc = AsynchronousServerSocketChannel.open(tg);
 
      //4. 绑定监听端口
      assc.bind(new InetSocketAddress(8080));
 
      //5. 监听连接,传入回调类处理连接请求,另外传入的this是Nio2Server对象本身
      assc.accept(this, new AcceptHandler()); 
   }
}

// 回调类
public class AcceptHandler implements CompletionHandler<AsynchronousSocketChannel, Nio2Server> {
   @Override
   public void completed(AsynchronousSocketChannel asc, Nio2Server attachment) {      
      // 调用 accept 方法继续接收其他客户端的请求
      attachment.assc.accept(attachment, this);
      
      //1. 先分配好 Buffer,告诉内核,数据拷贝到哪里去
      ByteBuffer buf = ByteBuffer.allocate(1024);
      
      //2. 调用 read 函数读取数据,除了把 buf 作为参数传入,还传入读回调类
      channel.read(buf, buf, new ReadHandler(asc)); 
}

public interface CompletionHandler<V,A> { //模板参数 V 和 A,分别表示 I/O 调用的返回值和附件类
    void completed(V result, A attachment); // I/O 操作成功时调用
    void failed(Throwable exc, A attachment);  I/O 操作失败时调用
}

回调类AcceptHandler类实现了CompletionHandler接口的completed方法。它还有两个模板参数,第一个是异步通道,第二个就是 Nio2Server 本身

Nio2EndPoint运行流程

比较Nio2Endpoint跟NioEndpoint运行流程图会发现,Nio2Endpoint中没有 Poller 组件,也就是没有 Selector。因为在异步 I/O 模式下,Selector 的工作交给内核来做了。

AprEndpoint

AprEndpoint也实现了非阻塞 I/O,但NioEndpoint 通过调用Java的NIO API来实现非阻塞I/O,而AprEndpoint是通过JNI调用APR本地库而实现非阻塞I/O的。对于Tomcat来说,APR本地库的性能是优于Java的NIO API的,尤其是需要与操作系统进行频繁交互的场景,例如Socket、TLS加密传输。在这些场景下Java和C语言的性能相比有差距。APR就是C语言编写的。Java写的Tomcat想要调用APR需要通过JNI(Java Native Interface)方式来调用,JNI可以使Java调用其他语言编写的程序。

AprEndpoint

6. Java线程池

程序运行时需要通过使用系统资源(CPU、内存、网络、磁盘等)来完成信息的处理。如果程序需要频繁创建、销毁对象,那么就会产生性能问题。“池”就是用来解决这个问题。

常见的“池”包括:数据库连接池、内存池、线程池、常量池。池将用过的对象保存起来,等下一次需要用这个对象的时候,直接从对象池中取出,避免频繁创建和销毁。

Java线程是对操作系统线程的封装,也是一个对象。创建Java线程也消耗系统资源,因此有了线程池。Web容器一般会把处理请求的工作放到线程池中执行,Tomcat就扩展了原生的Java线程池来满足高并发的需求。

Java原生线程池内部维护了一个线程数组和一个任务队列。当任务处理不过来时就把任务放到队列里慢慢处理。Java线程池的一些核心类包括:ThreadPoolExecutor、FixedThreadPool/CachedThreadPool

ThreadPoolExecutor

public ThreadPoolExecutor(int corePoolSize,  //核心线程数
                          int maximumPoolSize, //最大线程数
                          long keepAliveTime, //超时时间
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,  //工作队列
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) // 线程拒绝策略

通过ThreadPoolExecutor的构造方法可以看出,如果线程数还没达到corePoolSize,当任务来临时就创建新的线程来执行。达到corePoolSize之后,新增的任务就不创建线程而是放入工作队列workQueue中。线程池从workQueue中获取任务。当workQueue队列也满了,线程池就创建临时线程来处理,但如果总的线程数大于maximumPoolSize就不能再创建新的线程,执行拒绝策略handler。如果工作队列中也没有线程需要处理的任务了,该线程可能会被销毁。

FixedThreadPool/CachedThreadPool

这两个类是Java中默认的线程池实现,本质是给ThreadPoolExecutor设置了不同的参数。

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                 new LinkedBlockingQueue<Runnable>()); //无界队列
}
 
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE, //对线程个书不做限制
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>()); //队列长度为0
}

FixedThreadPool是固定长度(nThreads)的线程数组,多余的任务放到无限长的队列中处理。CachedThreadPool不对线程个数进行限制,多余的任务会无限创建临时线程进行处理。这两个线程池的实现针对于参数是否限制线程个数或队列长度

Tomcat线程池

taskqueue = new TaskQueue(maxQueueSize);  //任务队列,限制最大长度
TaskThreadFactory tf = new TaskThreadFactory(namePrefix,daemon,getThreadPriority()); //线程工厂
executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), maxIdleTime, TimeUnit.MILLISECONDS,taskqueue, tf); //线程池,限制核心线程数minSpareThreads和最大线程池数量maxThreads

Tomcat 线程池扩展了原生的 ThreadPoolExecutor,通过重写 execute 方法实现了自己的任务处理逻辑。区别在于当队列满了开始创建临时线程总线程数达到maximumPoolSize后,原生的线程池是执行拒绝策略,而Tomcat线程池则是继续尝试将任务添加到队列中。如果缓冲队列也满了,插入失败再执行拒绝策略。代码如下:

public class ThreadPoolExecutor extends java.util.concurrent.ThreadPoolExecutor {
  ...
  public void execute(Runnable command, long timeout, TimeUnit unit) {
      submittedCount.incrementAndGet();
      try {
          // 调用 Java 原生线程池的 execute 去执行任务
          super.execute(command);
      } catch (RejectedExecutionException rx) { //总线程数达到maximumPoolSize,原生线程池抛出异常
         // 如果总线程数达到 maximumPoolSize,Java 原生线程池执行拒绝策略
          if (super.getQueue() instanceof TaskQueue) {
              final TaskQueue queue = (TaskQueue)super.getQueue();
              try {
                  // 继续尝试把任务放到任务队列中去
                  if (!queue.force(command, timeout, unit)) {
                      submittedCount.decrementAndGet();
                      // 如果缓冲队列也满了,插入失败,执行拒绝策略。
                      throw new RejectedExecutionException("...");
}

7. WebSocket

网络上双向链路通信的两个端点称为Socket。每个Socket对应一个IP地址和端口号,是对TCP/IP协议抽象出来的API,Socket本身不是协议。但WebSocket是一个应用层协议,类似HTTP协议。它通过HTTP协议进行一次握手,握手之后数据直接从TCP层的Socket传输,与HTTP协议不再相关。


WebSocket请求和响应头部

WebSocket的数据传输会以frame形式传输,将一条消息分为几个frame,按照先后顺序传输出去。这样大数据可以分片传输,不用考虑数据大小。并且和HTTP 的 chunk 一样,可以边生成数据边传输,提高传输效率。

Tomcat WebSocket流程

8. 热部署和热加载

热加载的实现方式是 Web 容器启动一个后台线程,定期检测类文件的变化,如果有变化,就重新加载类,在这个过程中不会清空 Session ,一般用在开发环境。

热部署原理类似,也是由后台线程定时检测 Web 应用的变化,但它会重新加载整个 Web 应用。这种方式会清空 Session,比热加载更加干净、彻底,一般用在生产环境。

Jetty

Tomcat处理HTTP和Servlet的组件分别是Connector(多个)和Container(一个)。在Jetty中则是Connector(多个)和Handler(多个),这两个组件所需的资源都是从全局线程池ThreadPool中获取。Handler根据不同的场景选取对应的Handler,如ServletHandler、SessionHandler。这两个组件的调用都是通过Server类来实现——创建并初始化Connector、Handler、ThreadPool组件,然后调用 start方法启动它们。

Jetty架构

如果对Jetty和Tomcat的架构进行对比,(1)Jetty中没有Service的概念,Connector是被所有Handler共享的。(2)在 Tomcat中每个连接器都有自己的线程池,而Jetty中共享一个全局的线程池。

Connector

上文提到Tomcat的Connector的网络通信功能支持多种IO模型,而Jetty主要支持NIO。在介绍Connector工作流程之前,先总结一下NIO。

NIO

Java NIO的核心组件有三个:Channels、Buffers、Selectors。Channel通道表示一个连接,可以理解为一个Socket。通过Channel可以读写数据,但是这个读写过程并不直接操作,需要通过Buffer来中转。也就是将数据从Channel读到Buffer,也可以从Buffer写到Channel。通道包含很多种如网络IOSockerChannel、文件IOFileChannel等。Buffer根据传递数据的基本类型不同也有ByteBuffer、CharBuffer、IntBuffer等。Selector则是位于Thread和Channel中间,允许单线程处理多个 Channel,想要使用Selector,需要向其中注册Channel,调用select方法来调用Channel。NIO模式Demo如下:

ServerSocketChannel server = ServerSocketChannel.open(); //创建服务端 Channel
server.socket().bind(new InetSocketAddress(port)); //绑定监听端口
server.configureBlocking(false);  //设置为非阻塞

Selector selector = Selector.open(); //创建Selector
server.register(selector, SelectionKey.OP_ACCEPT); //注册事件OP_ACCEPT,如果有新的连接请求就通知

 while (true) {
        selector.select();// 查询 I/O 事件
        for (Iterator<SelectionKey> i = selector.selectedKeys().iterator(); i.hasNext();) { 
            SelectionKey key = i.next();  // 遍历SelectionKey列表
            i.remove(); 
 
            if (key.isAcceptable()) {  // 如果有注册的事件
                SocketChannel client = server.accept();  // 建立一个新连接 
                client.configureBlocking(false); 
                
                client.register(selector, SelectionKey.OP_READ); // 反馈给Selector,可接收IO读事件
            } 
        }
    } 

总的来说上述代码完成了三件事:监听连接、IO事件查询、数据读写。这三个分别对应于Jetty的Acceptor、SelectorManager、Connection

Jetty Connector 工作流程

一些框架

服务接入层:反向代理 Nginx;API 网关 Node.js。
业务逻辑层:Web 容器 Tomcat、Jetty;应用层框架 Spring、Spring MVC 和 Spring Boot;ORM 框架 MyBatis;
数据缓存层:内存数据库 Redis;消息中间件 Kafka。
数据存储层:关系型数据库 MySQL;非关系型数据库 MongoDB;文件存储 HDFS;搜索分析引擎 Elasticsearch
RPC框架:Spring Cloud、Dubbo
网络通信:Netty
分布式协调:Zookeeper

Bootstrap
在org.apache.catalina.startup包下包含catalina类和Bootstrap类(引导类加载器)。前者用于启动或关闭Server对象,并解析Tomcat配置文件。后者则是一个入口点,负责创建Catalina实例,并调用其process方法。Catalina类封装了一个server对象,该对象还有一个service对象。而service对象本身包含一个servlet容器和多个连接器。

Server
org.apache.catalina.core.StandardServer类是Server接口的实现,提供了addService、removeService、findServices等方法,其生命周期包括:initialize、start、stop、await

Service接口的实现类StandardService的initialize方法用于初始化添加到其中的所有连接器,start方法可以启动连接器和所有servlet容器

HTTP请求
HTTP请求在客户端和服务端之间交互,而套接字就是两头的端点,使应用程序可以连接、发送或接收字节流。客户端由Socket类实现,其getOutputStream方法可以获取一个OutputStream对象。要发送文本给服务器的话还需要创建一个java.io.PrintWriter对象。如果客户端想接收服务器端发送的字节流则需要调用getInputStream方法。相应的,服务器端由ServerSocket类实现套接字。它与客户端不同的是,它需要等待来自客户端的连接请求。而应用程序交互过程中主要用三个类:HttpServer、Request、Response。

四种容器
servlet容器是用来处理请求servlet资源,Tomcat中共有四种类型的容器:Engine、Host、Context、Wrapper。一个容器可以又0个或多个低层级的子容器。

Engine:表示整个Catalina servlet引擎
Host:表示包含一个或多个Context容器的虚拟主机
Context:表示一个Web应用程序。一个Context可以有多个Wrapper
Wrapper:表示一个独立的servlet

上述四个接口都继承自Container接口,其标准实现分别为StandardEngine、StandardHost、StandardContext、StandardWrapper。再放上一张网图:

以Engine接口为例:

public interface Engine extends Container {
    String getDefaultHost();
    void setDefaultHost(String var1);
    String getJvmRoute();
    void setJvmRoute(String var1);
    Service getService();
    void setService(Service var1);
}

Wrapper
Wrapper本身具有“包装”的含义。org.apache.catalina.Wrapper接口的实现类主要负责管理其基础servlet类的servlet生命周期,其中有两个重要的方法:load和allocate。前者载入并初始化servlet类,后者分配一个已经初始化的servlet实例。StandardWrapper对象的主要任务是载入它所代表的servlet类,并进行实例化,但是StandardWrapper并不调用servlet的service方法,而是由StandardWrapperValve对象完成,该对象通过调用allocate方法从StandardWrapper实例中获取servlet实例,然后再调用servlet实例的service方法。StandardWrapperValve是StandardWrapper实例中的基础阀,要完成两个操作:
1.执行与该servlet实例关联的全部过滤器
2.调用servlet实例的service方法

具体执行过程大致如下:
1.调用StandardWrapper实例的allocate方法获取该StandardWrapper实例所代表的servlet实例
2.调用私有方法createFilterChain,创建过滤器链
3.调用过滤器链的doFilter方法,其中包括调用servlet实例的service方法
4.释放过滤器链
5.调用Wrapper实例的deallocate方法
6.若该servlet类再也不会被使用到,就调用Wrapper实例的upload方法。

Filter
插入一小段Filter的介绍,Filter即过滤器可以把一些公用逻辑从Servlet中抽离出来,在HTTP请求到达Servlet之前,先被filter预处理。Filter类,也在javax.servlet中,该接口有三个方法:init/doFilter/destory

public interface Filter {
    void init(FilterConfig var1) throws ServletException;
    void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException;
    void destroy();
}

如果我们自己实现一个过滤器,就要重写doFilter方法,并调用chain.doFilter

回到上面StandardWrapperValve的执行过程,createFilterChain会创建一个ApplicationFilterChain实例,并将所有需要应用到该Wrapper实例所代表的servlet实例的过滤器都添加到其中。了解ApplicationFilterChain类就要先了解FilterDef类和ApplicationFilterConfig类。

org.apache.catalina.deploy.FilterDef类表示一个过滤器的定义。该类中的每个属性表示在定义filter元素时声明的子元素。Map类型的变量parameters存储了初始化过滤器时所需要的所有参数。

org.apache.catalina.core.ApplicationFilterConfig类实现了javax.servlet.FilterConfig接口,用于管理web应用程序第一次启动时创建的所有的过滤器实例。

一个Context对象(Web应用程序)+FilterDef对象(过滤器)=构建一个ApplicationFilterConfig(web应用程序的过滤器实例)

org.apache.catalina.core.ApplicationFilterChain类实现了javax.servlet.FilterChain接口,StandardWrapperValue类则可以创建一个ApplicationFilterChain类的实例,调用其doFilter方法。

servlet请求过程:
请求servlet类->StandardWrapper.setServletClass获取servlet类完全限定名/StandardWrapper.setName为servlet类指定一个名字->StandardWrapper载入servlet类->该servlet是否实现了SingleThreadModel接口,实现了就只能载入一次。

HTTP连接过程:

connector->
StandardContext->
StandardContextPipeline->
StandardContextValue->
StandardWrapper->
StandardWrapperValue->
Servlet

Context
Context接口的实现表示一个Web应用程序,其中较为重要的方法是addWrapper和createWrapper。StandardContext对象可以读取并解析默认的web.xml文件,该文件位于%CATALINA_HOME%/conf目录下,该文件的内容会应用到所有部署到tomcat中的应用程序中。StandardContext构造函数中会为其管道对象设置基础阀,类型为org.apache.catalina.core.StandardContextValue,该基础阀会处理从连接器中接收到的每个HTTP请求。它处理请求Wrapper实例的过程是调用Context容器的map方法,并传入org.apache.catalina.Request对象,针对某个特定的协议调用findMapper方法返回一个映射器对象,来获取wrapper实例。

Host
Host容器是org.apache.catalina.Host接口的实例,其map方法可以处理引入的HTTP请求的context容器的实例。Host接口的实现是StandardHost类,它的构造函数会将一个基础阀的实例添加到管道对象中,基础阀是org.apache.catalina.core.StandardHostValue。

管道
管道包含某个servlet容器将要调用的任务,一个阀表示一个具体的执行任务。在servlet容器的管道中,有一个基础阀,还可以通过server.xml额外添加任意数量的阀。管道就像过滤链条,而阀就是其中一个一个的过滤器,基础阀是最后执行的,负责处理request和response对象。

| 阀1  | 阀2 | 阀3 |......|阀n |---->管道

管道有几个重要的接口,如Pipeline、Value、ValueContext等。Pipeline接口中的addValue、removeValue分别代表向管道中添加和删除阀。setBasic方法则将基础阀设置到管道中。Value接口中有个invoke方法,连接器调用容器的invoke方法后,容器会调用管道的invoke方法。ValueContext接口可以访问管道的所有成员,具有invokeNext方法,调用后续的阀。

servlet容器中有一个名为验证器的阀来支持安全限制。当servlet容器启动时,验证器阀会被添加到Context容器的管道中。在调用Wrapper阀之前会先调用验证器阀,对当前用户身份进行验证,如果验证成功就继续调用后续的阀,显示请求的servlet。

用户身份验证依靠“领域”,它会对用户输入的用户名密码进行判断。领域对象通过调用Context容器的setRealm方法与一个Context容器相关联。领域对象是org.apache.catalina.Realm接口的实例。该接口的基本实现类是org.apache.catalina.realm.RealmBase类,该类为抽象类。RealmBase还有一些继承类:JDBCRealm、JNDIRealm、MemoryRealm、UserDatabaseRealm等。默认情况下会使用MemoryRealm类的实例作为验证用的领域对象。登陆配置是通过org.apache.catalina.deploy.LoginConfig类的实例,其getRealmName方法获取领域对象的名字,getAuthName方法获取所使用验证方法的名字(如BASIC\DIGEST\FORM\CLIENT-CERT,分别对应验证器BasicAutherticator\DigestAuthenticator\FormAuthticator\SSLAuthenticator)

而验证器则是org.apache.catalina.Authenticator接口的实例。其实现类为AuthenticatorBase类,该类也是一个阀。

载入器
在catalina中,载入器就是org.apache.catalina.Loader接口的实例。如果要实现的是重载,那么实现的则是org.apache.cataline.loader.Reloader接口。这里有两个术语:repository仓库、resource资源。仓库表示载入器会在哪里搜索要载入的类,资源指的是一个类载入器中的DirContext对象,它的文件根路径指的是上下文的文件根路径。

类载入器
每次创建Java类实例时都需要用载入器将类载入到内存中,在这个过程中,类载入器会在一些核心Java类库及环境变量CLASSPATH中指明的目录中搜索相关类,如果找不到就会抛出java.lang.ClassNotFoundException异常。类载入器由上至下有三种:引导类载入器(bootstrap class loader)、扩展类载入器(extension class loader)、系统类载入器(system class loader)。

引导类加载器用于引导启动Java虚拟机,载入运行JVM所需的类及Java核心类,如java.lang包、java.io包等。扩展类加载器载入标准扩展目录中的类。系统类加载器是默认的类加载器,搜索在环境变量CLASSPATH中指明的路径和JVR文件。

每当需要载入一个类时,先调用系统类载入器,它会把任务再交给其父类加载器,最终交到引导类加载器。如果引导类加载器找不到相关类,就再还给子加载器去寻找。

但是类加载也有限制,servlet只能加载WEB-INF/classes目录及其子目录下的类,不能访问其他路径中的类,即使这些类包含在运行当前Tomcat的JVM的CLASSPATH环境变量中。

Tomcat中的载入器一般指的是WEB应用程序载入器而不仅仅是指类载入器。载入器必须实现org.apache.catalina.Loader接口,重载则是org.apache.catalina.loader.Reloader。Reloader接口中最重要的方法是modified方法,如果web中某个servlet或相关类被修改了,modified方法会返回true。addRepository方法可以用来添加仓库。web应用程序的载入器由Loader接口实现类org.apache.catalina.loader.WebappLoader。当调用WebappLoader类的start方法时,会进行如下工作:创建一个类载入器、设置仓库、设置类路径、设置访问权限、启动一个新线程来支持自动重载。

载入类时,webappClassLoader类要遵循如下规则:
1.所有已经载入的类都会缓存起来,所以载入类时要先检查本地缓存
2.若本地缓存中没有就检查上一层,调用java.lang.ClassLoader类的findLoadedClass方法
3.若启用了SecurityManager,则检查是否允许类载入器进行加载,防止Web应用程序中的类覆盖J2EE的类。
4.若打开标志位delegate或者待载入的类是术语包触发器中的包名,则调用父类载入器来载入相关类,如果父类载入器为null,则使用系统的类载入器。
5.从当前仓库中载入相关类
6.若当前仓库中没有需要的类,且标志位delegate关闭,则使用父类载入器,若父类载入器为null,则使用系统的类载入器进行加载
7.若仍未找到需要的类,则抛出ClassNotFoundException异常。

日志记录器
所有的日志记录器必须实现org.apache.catalina.logger接口

Session
由org.apache.catalina.Manager接口表示Session管理器来管理建立的Session对象。它必须与一个Context容器相关联,servlet实例可以通过调用javax.servlet.http.HttpServlet.Request接口的getSession方法来获取一个Session对象

异常处理
tomcat处理错误消息的方法是将其存储在一个properties文件中,便于读取和编辑。每个properties文件都是用org.apache.catalina.util.StringManager类的一个实例来处理。每个实例会读取某个包下的指定properties文件。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容