当前位置 博文首页 > 努力充实,远方可期:【tomcat】4、源码解析
tomcat由两大部分组成:
上篇文章https://blog.csdn.net/hancoder/article/details/113065917
介绍过,tomcat里可以有多个service
每个service可以有多个连接器
注意是连接器,不是连接数,连接器获取连接后交给容器,但在这两者内有肯定逻辑
连接器用于获取连接,由tcp协议封装成http,生成request请求,然后用适配器把request请求转成servlet request请求,交给容器
catalina
就是servlet container容器coyote
就是连接器的封装Coyote 丛林狼,草原狼(犬科动物,分布于北美); 英[ka????ti]
这部分还是得结合前篇文章阅读:https://blog.csdn.net/hancoder/article/details/118466983
Coyote
是Tomcat的连接器的总称,我们要访问tomcat必须经过连接器。
每个连接器在源码里也被称为protocolHandler
(协议处理器),源码中有很多协议处理器,ProtocolHandler
只是接口,具体实现类稍后介绍
下面的话随便看看,无所谓的
Coyote 封装了底层的网络通信(Socket 请求及响应处理),为Catalina 容器提供了统一的接口,使Catalina 容器与具体的请求协议及IO操作方式完全解耦。Coyote 将Socket 输入转换封装为 Request 对象,交由Catalina 容器进行处理,处理请求完成后, Catalina 通过Coyote 提供的Response 对象将结果写入输出流 。
Coyote 作为独立的模块,只负责具体协议和IO的相关操作, 与Servlet 规范实现没有直接关系,因此即便是 Request 和 Response 对象也并未实现Servlet规范对应的接口, 而是在Catalina 中将他们进一步封装为
ServletRequest
和ServletResponse
。
在Coyote中 , Tomcat支持的多种I/O模型和应用层协议,具体包含哪些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 的影响性能。
应用层协议 | 描述 |
---|---|
HTTP/1.1 | 这是大部分Web应用采用的访问协议。 |
AJP | 用于和Web服务器集成(如Apache),以实现对静态资源的优化以及集群部署,当前支持AJP/1.3。 |
HTTP/2 | HTTP 2.0大幅度的提升了Web性能。下一代HTTP协议 , 自8.5以及9.0版本之后支持。 |
通过协议和通信方式的组合,我们可以又把连接器分得更细了,也就是Tomcat中有6个实现类
Http11Protocol
:11代表是http1.1Http11NioProtocol
tomcat在默认启动时,也是开启了2个连接器,每个连接器可以设置处理不同的协议。如图
Tomcat为了实现支持多种I/O模型和应用层协议,一个容器可能对接多个连接器,就好比一个房间有多个门。但是单独的连接器或者容器都不能对外提供服务,需要把它们组装起来才能工作,组装后这个整体叫作Service
组件。这里请你注意,Service本身没有做什么重要的事情,只是在连接器和容器外面多包了一层,把它们组装在一起。Tomcat内可能有多个Service,这样的设计也是出于灵活性的考虑。通过在Tomcat中配置多个Service,可以实现通过不同的端口号来访问同一台机器上部署的不同应用。
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里面定义了Acceptor
和AsyncTimeout
两个内部类和一个Handler
接口。
Container是如何进行处理的以及处理完之后是如何将处理完的结果返回给Connector的?
连接器中的各个组件的作用如下:
Acceptor
和SocketProcessor
。
Executor
),后面会详细介绍EndPoint的实现类是
Tomcat如何扩展原生的Java线程池。
EndPoint接收了socket请求会把请求发送给processor。Processor处理HTTP或AJP协议
Processor : Coyote 协议处理接口 ,如果说EndPoint是用来实现TCP/IP
协议的,那么Processor用来实现HTTP
协议,Processor接收来自EndPoint的Socket,读取字节流解析成Tomcat Request
和Response
对象,并通过Adapter
将其提交到容器处理,
Processor是对应用层协议的抽象。
由于协议不同,客户端发过来的请求信息也不尽相同,Tomcat定义了自己的ServletRequest
类来“存放”这些请求信息。ProtocolHandler
接口负责解析请求并生成Request
类。但是这个Request对象不是标准的ServletRequest
,也就意味着,不能用Request作为参数来调用容器。Tomcat设计者的解决方案是引入CoyoteAdapter
,这是适配器模式的经典运用,连接器调用CoyoteAdapter的Sevice方法,传入的是Request对象,CoyoteAdapter负责将Request转成ServletRequest
,再调用容器的Service方法。
其中ProtocolHandler(连接器)又含了三个部件:Endpoint、Processor、Adapter。
Endpoint
:用来处理底层Socket的网络连接。
Acceptor
和AsyncTimeout
两个内部类和一个Handler
接口。Processor
:用于将Endpoint接收到的Socket封装成Request。他是一个线程池,我们每个【发生事件的连接】去线程池里去执行Adapter
:线程池里的连接为了去找container,他拿的是request
,但是容器接收的是http request
,那适配器就转一下
在BIO实现的Connector中,处理请求的主要实体是JIoEndpoint
对象。JIoEndpoint维护了Acceptor和Worker:
<Executor>
配置了其他线程池,原理与Worker类似。在NIO实现的Connector中,处理请求的主要实体是NIoEndpoint
对象。NIoEndpoint中除了包含Acceptor和Worker外,还使用了Poller
,处理流程如下图所示
Acceptor
接收socket后,不是直接使用Worker中的线程处理请求,而是先将请求发送给了Poller
,而Poller是实现NIO的关键。Acceptor向Poller发送请求通过队列实现,使用了典型的生产者-消费者模式。在Poller中,维护了一个Selector
对象;当Poller从队列中取出socket后,注册到该Selector中;然后通过遍历Selector,找出其中可读的socket,并使用Worker中的线程处理相应请求。与BIO类似,Worker也可以被自定义的线程池代替。
通过上述过程可以看出,在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定义在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对应在NIO中的selector,因为在Poller的构造器中有一句this.selector = Selector.open()
另外poller还是一个线程,实现了Runnable接口,也就是说让每个线程去执行select()
操作,负责处理读写事件
从acceptor获取到的socket都注册到selector上,就是注册到了poller上,poller进行select()
得到发生事件的key
另外,源码中poller也是有数组的,源码中默认是new Poller[2]
,对应到前面连接器的不同协议,如HTTP
和AJP
processor就是当select()
得到发生事件的key后,比如有多个socket有read事件,我们不能让他们在一个线程里一次执行吧?我们把每个socket.read()
放到线程池里执行。
这时候需要包装socket.read()这个操作,把每个任务包装为了processor线程。
processor线程包装的时候,是可以从缓存中拿别人用过的processor对象的。
让processor去线程池里执行processor.doRun()
,里面会执行processor.process()
在service标签中,配置了一个线程池参数,他是多个Poller共享的,也就是多个连接器共享的,然后我们可以自己写executor标签后,让connector标签引用它,这样每个连接器就有了自己的线程池。
Tomcat是一个由一系列可配置的组件构成的Web容器,而Catalina是Tomcat的servlet容器。
Catalina 是Servlet 容器实现,包含了之前讲到的所有的容器组件,以及后续章节涉及到的安全、会话、集群、管理等Servlet 容器架构的各个方面。它通过松耦合的方式集成Coyote,以完成按照请求协议进行数据读写。同时,它还包括我们的启动入口、Shell程序等。
Tomcat 的模块分层结构图, 如下:他跟源码中的包一一对应
Tomcat 本质上就是一款 Servlet 容器, 因此Catalina 才是 Tomcat 的核心 , 其他模块都是为Catalina 提供支撑的。 比如 : 通过Coyote 模块提供链接通信,Jasper 模块提供JSP引擎,Naming 提供JNDI 服务,Juli 提供日志服务。
Catalina 的主要组件结构如下:
Catalina 各个组件的职责:
组件 | 职责 |
---|---|
Catalina | 负责解析Tomcat的配置文件 , 以此来创建服务器Server组件,并根据命令来对其进行管理 |
Server | 服务器表示整个Catalina Servlet容器以及其它组件,负责组装并启动 Servlet引擎,Tomcat连接器。Server通过实现Lifecycle接口,提供了一种优雅的启动和关闭整个系统的方式 |
Service | 服务是Server内部的组件,一个Server包含多个Service。它将若干个 Connector组件绑定到一个Container(Engine)上 |
Connector | 连接器,处理与客户端的通信,它负责接收客户请求,然后转给相关的容器处理,最后向客户返回响应结果 |
Container | 容器,负责处理用户的servlet请求,并返回对象给web用户的模块 |
如下Service的源码,可以获取其中的容器
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元素代表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的主要属性包括:
上面介绍了Tomcat连接数、线程数的概念以及如何设置,下面说明如何查看服务器中的连接数和线程数。
查看服务器的状态,大致分为两种方案:(1)使用现成的工具,(2)直接使用Linux的命令查看。
现成的工具,如JDK自带的jconsole
工具可以方便的查看线程信息(此外还可以查看CPU、内存、类、JVM基本信息等),Tomcat自带的manager
,收费工具New Relic
等。下图是jconsole
查看线程信息的界面:
下面说一下如何通过Linux命令行,查看服务器中的连接数和线程数。
步骤 :
启动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的配置文件,初始化容器组件 ,监听对应的端口号, 准备接受客户端请求。
由于所有的组件均存在初始化、启动、停止等生命周期方法,拥有生命周期管理的特性, 所以Tomcat在设计的时候, 基于生命周期管理抽象成了一个接口 Lifecycle ,而组件 Server、Service、Container、Executor、Connector 组件 , 都实现了一个生命周期的接口,从而具有了以下生命周期中的核心方法:
各组件的默认实现
上面我们提到的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协议:
HTTP协议:
在init的过程中,init是LigecycleBase实现的,而initInternal是具体子类实现的。模板方法设计模式
从哪看起:org.apache.catalina.startup.BootStrap.java
‐‐‐‐>main()
从启动流程图中以及源码中,我们可以看出Tomcat的启动过程非常标准化, 统一按照生命周期管理接口Lifecycle的定义进行启动。首先调用init() 方法进行组件的逐级初始化操作,然后再调用start()方法进行启动。
每一级的组件除了完成自身的处理外,还要负责调用子组件响应的生命周期管理方法,组件与组件之间是松耦合的,因为我们可以很容易的通过配置文件进行修改和替换。
serverSocket.socket().bind()
逻辑// 调用start方法,一次次调用catalina.start、server、service、engine、host、context... 又去调用executor,connector、protocolHandler
在endpoint最后调用startAcceptorThread()开启接口线程;
// AbstractEndpoint.java
protected final void startAcceptorThreads() {
int