当前位置 博文首页 > 努力充实,远方可期:【tomcat】4、源码解析

    努力充实,远方可期:【tomcat】4、源码解析

    作者:[db:作者] 时间:2021-08-22 18:07

    • Tomcat系列文章专栏:https://blog.csdn.net/hancoder/category_11180472.html
    • 0 sevlet的知识,从上面目录中找
    • 1 tomcat的安装与目录结构:https://blog.csdn.net/hancoder/article/details/106765035
    • 2 tomcat源码环境搭建: https://blog.csdn.net/hancoder/article/details/113064325
    • tomcat架构:https://blog.csdn.net/hancoder/article/details/118466983
    • 3 tomcat架构与参数:https://blog.csdn.net/hancoder/article/details/113065917
    • 4 tomcat源码分析:https://blog.csdn.net/hancoder/article/details/113062146
    • 5 tomcat调优:https://blog.csdn.net/hancoder/article/details/113065948

    一、tomcat结构

    tomcat由两大部分组成:

    • 连接器:处理Socket连接,接收请求,负责网络字节流与Request和Response对象的转化。
    • servlet容器:加载和管理Servlet,以及具体处理Request请求。

    在这里插入图片描述

    上篇文章https://blog.csdn.net/hancoder/article/details/113065917

    介绍过,tomcat里可以有多个service

    每个service可以有多个连接器

    注意是连接器,不是连接数,连接器获取连接后交给容器,但在这两者内有肯定逻辑

    连接器与容器的关系

    连接器用于获取连接,由tcp协议封装成http,生成request请求,然后用适配器把request请求转成servlet request请求,交给容器

    • catalina就是servlet container容器
    • coyote就是连接器的封装

    在这里插入图片描述

    二、连接器:Coyote

    Coyote 丛林狼,草原狼(犬科动物,分布于北美); 英[ka????ti]

    这部分还是得结合前篇文章阅读:https://blog.csdn.net/hancoder/article/details/118466983

    1、概述

    Coyote 是Tomcat的连接器的总称,我们要访问tomcat必须经过连接器。

    每个连接器在源码里也被称为protocolHandler(协议处理器),源码中有很多协议处理器,ProtocolHandler只是接口,具体实现类稍后介绍

    下面的话随便看看,无所谓的

    Coyote 封装了底层的网络通信(Socket 请求及响应处理),为Catalina 容器提供了统一的接口,使Catalina 容器与具体的请求协议及IO操作方式完全解耦。Coyote 将Socket 输入转换封装为 Request 对象,交由Catalina 容器进行处理,处理请求完成后, Catalina 通过Coyote 提供的Response 对象将结果写入输出流 。

    Coyote 作为独立的模块,只负责具体协议和IO的相关操作, 与Servlet 规范实现没有直接关系,因此即便是 Request 和 Response 对象也并未实现Servlet规范对应的接口, 而是在Catalina 中将他们进一步封装为ServletRequestServletResponse

    2、IO模型与协议

    在Coyote中 , Tomcat支持的多种I/O模型和应用层协议,具体包含哪些IO模型和应用层协议,请看下表:

    Tomcat 支持的IO模型
    IO模型描述
    BIO自8.5/9.0 版本起,已被移除
    NIO非阻塞I/O,采用Java NIO类库实现。
    NIO2异步I/O,采用JDK 7最新的NIO2类库实现。
    APR采用Apache可移植运行库实现,是C/C++编写的本地库。如果选择该方案,需要单独安装APR库。

    在 8.0 之前 , Tomcat 默认采用的I/O方式为 BIO , 之后改为 NIO。 无论 NIO、NIO2还是 APR, 在性能方面均优于以往的BIO。 如果采用APR, 甚至可以达到 Apache HTTP Server 的影响性能。

    Tomcat 支持的应用层协议 :
    应用层协议描述
    HTTP/1.1这是大部分Web应用采用的访问协议。
    AJP用于和Web服务器集成(如Apache),以实现对静态资源的优化以及集群部署,当前支持AJP/1.3。
    HTTP/2HTTP 2.0大幅度的提升了Web性能。下一代HTTP协议 , 自8.5以及9.0版本之后支持。
    实现类

    通过协议和通信方式的组合,我们可以又把连接器分得更细了,也就是Tomcat中有6个实现类

    • Http11Protocol:11代表是http1.1
    • Http11NioProtocol
    • Http11Nio2Protocol
    • AjpNioProtocol
    • AjpAprProtocol
    • AjpNio2Protocol

    tomcat在默认启动时,也是开启了2个连接器,每个连接器可以设置处理不同的协议。如图

    这里写图片描述

    Tomcat为了实现支持多种I/O模型和应用层协议,一个容器可能对接多个连接器,就好比一个房间有多个门。但是单独的连接器或者容器都不能对外提供服务,需要把它们组装起来才能工作,组装后这个整体叫作Service组件。这里请你注意,Service本身没有做什么重要的事情,只是在连接器和容器外面多包了一层,把它们组装在一起。Tomcat内可能有多个Service,这样的设计也是出于灵活性的考虑。通过在Tomcat中配置多个Service,可以实现通过不同的端口号来访问同一台机器上部署的不同应用。

    3、连接器组件

    0) ProtocolHandler

    Connector就是使用ProtocolHandler来处理请求的,不同的ProtocolHandler代表不同的连接类型,比如:Http11Protocol使用的是普通BIO Socket来连接的,Http11NioProtocol使用的是NioSocket来连接的。

    其中ProtocolHandler由包含了三个部件:Endpoint、Processor、Adapter。

    (1)Endpoint用来处理底层Socket的网络连接,Processor用于将Endpoint接收到的Socket封装成Request,Adapter用于将Request交给Container进行具体的处理。

    (2)Endpoint由于是处理底层的Socket网络连接,因此Endpoint是用来实现TCP/IP协议的,而Processor用来实现HTTP协议的,Adapter将请求适配到Servlet容器进行具体的处理。

    (3)Endpoint的抽象实现AbstractEndpoint里面定义了AcceptorAsyncTimeout两个内部类和一个Handler接口。

    • Acceptor用于监听请求,
    • AsyncTimeout用于检查异步Request的超时,
    • Handler用于处理接收到的Socket,在内部调用Processor进行处理。

    Container是如何进行处理的以及处理完之后是如何将处理完的结果返回给Connector的?

    在这里插入图片描述

    这里写图片描述

    在这里插入图片描述

    连接器中的各个组件的作用如下:

    1) EndPoint与TCP

    • EndPoint : Coyote 通信端点,即通信监听的接口,是具体Socket接收和发送处理器,是对传输层的抽象,因此EndPoint用来实现TCP/IP协议的。
    • Tomcat 并没有EndPoint 接口,而是提供了一个抽象类AbstractEndpoint , 里面定义了两个内部类:AcceptorSocketProcessor
      • Acceptor用于监听Socket连接请求。
      • SocketProcessor用于处理接收到的Socket请求,它实现Runnable接口,在Run方法里调用协议处理组件Processor进行处理。为了提高处理能力,SocketProcessor被提交到线程池来执行。而这个线程池叫作执行器(Executor),后面会详细介绍

    EndPoint的实现类是

    • NioEndpoint
    • Nio2Endpoint
    • APrEndpoint

    Tomcat如何扩展原生的Java线程池。

    2) Processor与HTTP

    EndPoint接收了socket请求会把请求发送给processor。Processor处理HTTP或AJP协议

    Processor : Coyote 协议处理接口 ,如果说EndPoint是用来实现TCP/IP协议的,那么Processor用来实现HTTP协议,Processor接收来自EndPoint的Socket,读取字节流解析成Tomcat RequestResponse对象,并通过Adapter将其提交到容器处理,

    Processor是对应用层协议的抽象。

    3) Adapter

    由于协议不同,客户端发过来的请求信息也不尽相同,Tomcat定义了自己的ServletRequest类来“存放”这些请求信息。ProtocolHandler接口负责解析请求并生成Request类。但是这个Request对象不是标准的ServletRequest,也就意味着,不能用Request作为参数来调用容器。Tomcat设计者的解决方案是引入CoyoteAdapter,这是适配器模式的经典运用,连接器调用CoyoteAdapter的Sevice方法,传入的是Request对象,CoyoteAdapter负责将Request转成ServletRequest,再调用容器的Service方法。

    这里写图片描述

    4、EndPoint详解

    其中ProtocolHandler(连接器)又含了三个部件:Endpoint、Processor、Adapter。

    • Endpoint:用来处理底层Socket的网络连接。
      • Endpoint的抽象类AbstractEndpoint里面定义的AcceptorAsyncTimeout两个内部类和一个Handler接口。
      • Acceptor用于监听请求,
      • AsyncTimeout用于检查异步Request的超时,
      • Handler(某种程度上是poller)用于处理接收到的Socket,在内部调用Processor进行处理。
    • Processor:用于将Endpoint接收到的Socket封装成Request。他是一个线程池,我们每个【发生事件的连接】去线程池里去执行
    • Adapter:线程池里的连接为了去找container,他拿的是request,但是容器接收的是http request,那适配器就转一下
      • 将Request交给Container进行具体的处理。

    5 NIO套娃到连接器

    BIO的连接器

    在BIO实现的Connector中,处理请求的主要实体是JIoEndpoint对象。JIoEndpoint维护了Acceptor和Worker:

    • Acceptor接收socket,然后从Worker线程池中找出空闲的线程处理socket,如果worker线程池没有空闲线程,则Acceptor将阻塞。
    • Worker是Tomcat自带的线程池,如果通过<Executor>配置了其他线程池,原理与Worker类似。
    NIO的连接器

    在NIO实现的Connector中,处理请求的主要实体是NIoEndpoint对象。NIoEndpoint中除了包含Acceptor和Worker外,还使用了Poller,处理流程如下图所示

    img

    Acceptor接收socket后,不是直接使用Worker中的线程处理请求,而是先将请求发送给了Poller,而Poller是实现NIO的关键。Acceptor向Poller发送请求通过队列实现,使用了典型的生产者-消费者模式。在Poller中,维护了一个Selector对象;当Poller从队列中取出socket后,注册到该Selector中;然后通过遍历Selector,找出其中可读的socket,并使用Worker中的线程处理相应请求。与BIO类似,Worker也可以被自定义的线程池代替

    BIO与NIO对比

    通过上述过程可以看出,在NIoEndpoint处理请求的过程中,无论是Acceptor接收socket,还是线程处理请求,使用的仍然是阻塞方式;但在“读取socket并交给Worker中的线程”的这个过程中,使用非阻塞的NIO实现,这是NIO模式与BIO模式的最主要区别(其他区别对性能影响较小,暂时略去不提)。而这个区别,在并发量较大的情形下可以带来Tomcat效率的显著提升:

    但是BIO线程池的思想是主线程获取到一个连接后,交给线程池去处理,也就是主线程不断accept()阻塞,而每个线程对应一个连接,accept()获取到一个连接后,去线程池里去操作read操作,会阻塞,处理完read等操作后释放连接,这个线程给别的连接用。如果线程池满了,那么将由排队、拒绝策略等操作,那么连接数还是等于线程数

    而对于NIO,accept()虽然是阻塞的,但是接收到连接后注册到了selector,selector采用事件机制,连接可以都注册到上面,有了事件才去线程池里处理。因为可以注册很多事件,所以连接数可以大于线程数

    目前大多数HTTP请求使用的是长连接(HTTP/1.1默认keep-alive为true),而长连接意味着,一个TCP的socket在当前请求结束后,如果没有新的请求到来,socket不会立马释放,而是等timeout后再释放。如果使用BIO,“读取socket并交给Worker中的线程”这个过程是阻塞的,也就意味着在socket等待下一个请求或等待释放的过程中,处理这个socket的工作线程会一直被占用,无法释放;因此Tomcat可以同时处理的socket数目不能超过最大线程数,性能受到了极大限制。而使用NIO,“读取socket并交给Worker中的线程”这个过程是非阻塞的,当socket在等待下一个请求或等待释放时,并不会占用工作线程,因此Tomcat可以同时处理的socket数目远大于最大线程数,并发性能大大提高。

    在这里我要用NIO的知识对应到连接器源码中

    我在上篇文章说过bind(addr,backlog)的问题,backlog是阻塞的个数,在源码中叫Accept[backlog]

    Acceptor接收socket

    Acceptor定义在AbstractEndpoint类中,他实现了Runnable,是个线程

    源码中Accept[backlog]什么意思:就是说创建几个线程执行accept()serverSock.accept();这个逻辑可以在NioEndpoint的子类Acceptor.run()中看到【他继承了AbstractEndpoint中的Acceptor,实现了run()】

    学过NIO的都知道他的意思吧,他是接收客户端的socket,然后去读数据。

    与之前NIO有点不同的是,我们之前把serversocket注册到了selector,accept()方法也是selector通过事件知道,但tomcat中我看单独线程accept(),接收到后才注册到selector,selector只发送读写事件

    最大队列问题之前解释过了,达到最大连接数后,进去等待队列(这个等待队列有点线程池等待队列的意思,但因为有selector的关系,又感觉不全是)。等待队列满了之后再accept()到连接后,还没注册到selector上了,就阻塞了,不让注册了,直到等待队列空出来。所以说tomcat的最大连接数是maxConnections+maxAcceptors

    Poller管理socket

    poller对应在NIO中的selector,因为在Poller的构造器中有一句this.selector = Selector.open()

    另外poller还是一个线程,实现了Runnable接口,也就是说让每个线程去执行select()操作,负责处理读写事件

    从acceptor获取到的socket都注册到selector上,就是注册到了poller上,poller进行select()得到发生事件的key

    另外,源码中poller也是有数组的,源码中默认是new Poller[2],对应到前面连接器的不同协议,如HTTPAJP

    processor处理事件

    processor就是当select()得到发生事件的key后,比如有多个socket有read事件,我们不能让他们在一个线程里一次执行吧?我们把每个socket.read()放到线程池里执行。

    这时候需要包装socket.read()这个操作,把每个任务包装为了processor线程。

    processor线程包装的时候,是可以从缓存中拿别人用过的processor对象的。

    让processor去线程池里执行processor.doRun(),里面会执行processor.process()

    线程池Executor

    在service标签中,配置了一个线程池参数,他是多个Poller共享的,也就是多个连接器共享的,然后我们可以自己写executor标签后,让connector标签引用它,这样每个连接器就有了自己的线程池。

    三、容器Catalina

    Tomcat是一个由一系列可配置的组件构成的Web容器,而Catalina是Tomcat的servlet容器。

    Catalina 是Servlet 容器实现,包含了之前讲到的所有的容器组件,以及后续章节涉及到的安全、会话、集群、管理等Servlet 容器架构的各个方面。它通过松耦合的方式集成Coyote,以完成按照请求协议进行数据读写。同时,它还包括我们的启动入口、Shell程序等。

    Tomcat 的模块分层结构图, 如下:他跟源码中的包一一对应

    在这里插入图片描述

    在这里插入图片描述

    Tomcat 本质上就是一款 Servlet 容器, 因此Catalina 才是 Tomcat 的核心 , 其他模块都是为Catalina 提供支撑的。 比如 : 通过Coyote 模块提供链接通信,Jasper 模块提供JSP引擎,Naming 提供JNDI 服务,Juli 提供日志服务。

    Catalina 结构

    Catalina 的主要组件结构如下:

    • Server:Catalina负责管理Server,而Server表示着整个服务器。
    • Service:Server下面有多个服务Service,每个服务都包含着多个连接器组件Connector(Coyote 实现)和一个容器组件Container。在Tomcat 启动的时候, 会初始化一个Catalina的实例。

    在这里插入图片描述

    Catalina 各个组件的职责:

    组件职责
    Catalina负责解析Tomcat的配置文件 , 以此来创建服务器Server组件,并根据命令来对其进行管理
    Server服务器表示整个Catalina Servlet容器以及其它组件,负责组装并启动 Servlet引擎,Tomcat连接器。Server通过实现Lifecycle接口,提供了一种优雅的启动和关闭整个系统的方式
    Service服务是Server内部的组件,一个Server包含多个Service。它将若干个 Connector组件绑定到一个Container(Engine)上
    Connector连接器,处理与客户端的通信,它负责接收客户请求,然后转给相关的容器处理,最后向客户返回响应结果
    Container容器,负责处理用户的servlet请求,并返回对象给web用户的模块

    如下Service的源码,可以获取其中的容器

    在这里插入图片描述

    Container 结构

    Tomcat设计了4种容器,分别是Engine、Host、Context和Wrapper。这4种容器不是平行关系,而是父子关系。, Tomcat通过一种分层的架构,使得Servlet容器具有很好的灵活性。

    在这里插入图片描述

    容器描述
    Engine表示整个Catalina的Servlet引擎,用来管理多个虚拟站点,一个Service最多只能有一个Engine,但是一个引擎可包含多个Host
    Host代表一个虚拟主机,或者说一个站点,可以给Tomcat配置多个虚拟主机地址,而一个虚拟主机下可包含多个Context
    Context表示一个Web应用程序, 一个Web应用可包含多个Wrapper
    Wrapper表示一个Servlet,Wrapper 作为容器中的最底层,不能包含子容器

    可以看看Tomcat的server.xml。Tomcat采用了组件化的设计,它的构成组件都是可配置的,其中最外层的是Server,其他组件按照一定的格式要求配置在这个顶层容器中。

    <Server>
        <Service>
            <Connector/>
            <Connector/>
    
            <Engine>
                <Host>
                    <Context></Context>
                </Host>
            </Engine>
        </Service>
    </Server>
    

    这些容器具有父子关系,形成一个树形结构(设计模式中的组合模式)。没错,Tomcat就是用组合模式来管理这些容器的。具体实现方法是,所有容器组件都实现了Container接口,因此组合模式可以使得用户对单容器对象组合容器对象的使用具有一致性。这里单容器对象指的是最底层的Wrapper,组合容器对象指的是上面的Context、Host或者Engine。(而调用时使用的是责任链模式)

    在这里插入图片描述

    Container 接口中提供了以下方法(截图中知识一部分方法) :

    在这里插入图片描述

    在上面的接口看到了getParent、SetParent、addChild和removeChild等方法。

    Container接口扩展了LifeCycle接口,LifeCycle接口用来统一管理各组件的生命周期

    三、线程池Executor

    Executor元素代表Tomcat中的线程池,可以由其他组件共享使用;要使用该线程池,组件需要通过executor属性指定该线程池。

    Executor是Service元素的内嵌元素。一般来说,使用线程池的是Connector组件;为了使Connector能使用线程池,Executor元素应该放在Connector前面。Executor与Connector的配置举例如下:

    <Executor name="tomcatThreadPool" namePrefix ="catalina-exec-" maxThreads="150" minSpareThreads="4" />
    <Connector executor="tomcatThreadPool" port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" acceptCount="1000" />
    

    Executor的主要属性包括:

    • name:该线程池的标记
    • maxThreads:线程池中最大活跃线程数,默认值200(Tomcat7和8都是)
    • minSpareThreads:线程池中保持的最小线程数,最小值是25
    • maxIdleTime:线程空闲的最大时间,当空闲超过该值时关闭线程(除非线程数小于minSpareThreads),单位是ms,默认值60000(1分钟)
    • daemon:是否后台线程,默认值true
    • threadPriority:线程优先级,默认值5
    • namePrefix:线程名字的前缀,线程池中线程名字为:namePrefix+线程编号

    四、查看当前状态

    上面介绍了Tomcat连接数、线程数的概念以及如何设置,下面说明如何查看服务器中的连接数和线程数。

    查看服务器的状态,大致分为两种方案:(1)使用现成的工具,(2)直接使用Linux的命令查看。

    现成的工具,如JDK自带的jconsole工具可以方便的查看线程信息(此外还可以查看CPU、内存、类、JVM基本信息等),Tomcat自带的manager,收费工具New Relic等。下图是jconsole查看线程信息的界面:

    img

    下面说一下如何通过Linux命令行,查看服务器中的连接数和线程数。

    四、Tomcat源码

    在这里插入图片描述

    步骤 :

    启动tomcat , 需要调用 bin/startup.bat (在linux 目录下 , 需要调用 bin/startup.sh), 在startup.bat 脚本中, 调用了catalina.bat

    在catalina.bat 脚本文件中,调用了BootStrap中的main()方法。

    3)在BootStrap 的main 方法中调用了 init方法 , 来创建Catalina 及 初始化类加载器。

    4)在BootStrap 的main 方法中调用了 load方法 , 在其中又调用了Catalina的load方法。

    5)在Catalina 的load 方法中 , 需要进行一些初始化的工作, 并需要构造Digester 对象, 用于解析 XML。

    6) 然后在调用后续组件的初始化操作 。。。

    加载Tomcat的配置文件,初始化容器组件 ,监听对应的端口号, 准备接受客户端请求。

    Lifecycle接口

    由于所有的组件均存在初始化、启动、停止等生命周期方法,拥有生命周期管理的特性, 所以Tomcat在设计的时候, 基于生命周期管理抽象成了一个接口 Lifecycle ,而组件 Server、Service、Container、Executor、Connector 组件 , 都实现了一个生命周期的接口,从而具有了以下生命周期中的核心方法:

      1. init():初始化组件
    • 2) start():启动组件
    • 3) stop():停止组件
    • 4) destroy():销毁组件

    各组件的默认实现

    上面我们提到的Server、Service、Engine、Host、Context都是接口, 下图中罗列了这些接口的默认实现类。

    在这里插入图片描述

    在这里插入图片描述

    当前对于 Endpoint组件来说,在Tomcat中没有对应的Endpoint接口, 但是有一个抽象类 AbstractEndpoint,其下有三个实现类:

    • NioEndpoint:连接器的NIO模型(Tomcat8.5默认)
    • Nio2Endpoint:连接器的NIO2模型
    • AprEndpoint:连接器的APR模型

    ProtocolHandler : Coyote协议接口,通过封装Endpoint和Processor ,实现针对具体协议的处理功能。

    Tomcat按照协议和IO提供了6个实现类。

    AJP协议:

    • AjpNioProtocol:采用NIO的IO模型。
    • AjpNio2Protocol:采用NIO2的IO模型。
    • AjpAprProtocol:采用APR的IO模型,需要依赖于APR库。

    HTTP协议:

    • Http11NioProtocol:采用NIO的IO模型,(默认)(如果服务器没有安装APR)。
    • Http11Nio2Protocol:采用NIO2的IO模型。
    • Http11AprProtocol :采用APR的IO模型,需要依赖于APR库。

    在init的过程中,init是LigecycleBase实现的,而initInternal是具体子类实现的。模板方法设计模式

    源码流程

    从哪看起:org.apache.catalina.startup.BootStrap.java ‐‐‐‐>main()

    从启动流程图中以及源码中,我们可以看出Tomcat的启动过程非常标准化, 统一按照生命周期管理接口Lifecycle的定义进行启动。首先调用init() 方法进行组件的逐级初始化操作,然后再调用start()方法进行启动。

    每一级的组件除了完成自身的处理外,还要负责调用子组件响应的生命周期管理方法,组件与组件之间是松耦合的,因为我们可以很容易的通过配置文件进行修改和替换。

    在这里插入图片描述 在这里插入图片描述

    1 Bootstrap主类的流程

    • 创建catalina对象
    • 解析xml
    • 初始化server
      • 在初始化server里面初始化多个service
      • 依次类推。。。
      • 在service里初始化engine、host、context、
      • 又去初始化executor
      • 又去初始化connector、protocolhandler、endpoint
      • endpoint里有serverSocket.socket().bind()逻辑
    • 启动start

    2 init

    connector.init
    • new CoyotaAdapter适配器,被设置到protocolhandler
    • protocolhandler.init
      • endpoint.init
        • endpoint.bind在里面能看到NIO的逻辑

    3 start

    // 调用start方法,一次次调用catalina.start、server、service、engine、host、context... 又去调用executor,connector、protocolHandler
    在endpoint最后调用startAcceptorThread()开启接口线程;
    // AbstractEndpoint.java
    protected final void startAcceptorThreads() {
        int