在具体介绍其整体构架之前,我们先来回忆一下Http协议的大致工作原理;我们应该都知道 HTTP协议是浏览器与服务器之间的数据传送协议。作为应用层协议,HTTP是基于TCP/IP 协议来传递数据的(HTML文件、图片、查询结果等),HTTP协议不涉及数据包 (Packet)传输,其主要规定了客户端和服务器之间的通信格式。
下面我们来看看一个http协议的大概工作过程:
1) 用户通过浏览器进行了某种操作:比如输入网址并回车,或者是点击某条链接。
2) 浏览器获取了用户的这个事件(浏览某个网站或点击某条链接), 并向服务端发出TCP连接请求。
3) 服务程序接受浏览器的连接请求,并经过TCP三次握手成功与浏览器建立连接。
4) 浏览器将用户的请求数据打包成一个HTTP协议格式的数据包。
5) 浏览器将该数据包推入网络,数据包经过网络传输,最终达到端服务程序。
6) 服务端程序拿到这个数据包后,同样以HTTP协议格式解包,获取到客户端的意图。
7) 得知客户端意图后进行处理,比如提供静态文件或者调用服务端程序获得动态结果。
8) 服务器将响应结果或请求结果(可能是HTML页面或者图片等)按照HTTP协议格式打包。
9) 服务器将响应数据包推入网络,数据包经过网络传输最终达回到浏览器。
10) 浏览器拿到数据包后,以HTTP协议的格式解包,然后解析数据,假设这里的数据是 HTML页面文件。
11) 浏览器将HTML文件展示在页面上。
注意:浏览器只能解析静态资源,比如html、css、js等,而不能直接解析像servlet、jsp这种动态资源,故服务器响应回给浏览器的资源都是静态的资源。
我们都知道Tomcat是一个Web服务器,那其作为一个服务器,在这个过程中都做了些什么事情呢?
答案是 接受请求连接、解析请求数据、处理请求和发送响应这几个步骤。
既然tomcat主要是做的事情是接受请求连接、解析请求数据、处理请求和发送响应,那么在tomcat中就必然会有一部分是用来接受请求并处理请求的,我们暂且将这部分叫做"HTTP服务器相关部分", 用户浏览器发给tomcat服务端的是一个HTTP格式的请求,tomcat的HTTP服务器收到这个请求后,需要调用服务端的程序来处理,即我们编写的Servlet类,一般来说不同的请求需要由不同 的Servlet、Java类来处理。 倘若是在只有HTTP服务器这种模式中,HTTP即要接受请求连接又要对请求进行处理,即 HTTP服务器会直接调用具体业务类去处理请求,会造成极高程度的耦合性。
故tomcat为了解耦合,不让HTTP服务器直接调用业务类,而是把请求交给容器来处理,容器通过 Servlet接口去调用具体的业务类。因此Servlet接口和Servlet容器的出现,达到了HTTP服务器与 业务类解耦的目的。而Servlet接口和Servlet容器这一整套规范叫作Servlet规范。
Tomcat按照Servlet规范的要求实现了Servlet容器,同时它们也具有HTTP服务器的功 能。作为Java程序员,如果我们要实现新的业务功能,只需要实现一个Servlet,并把它 注册到Tomcat(Servlet容器)中,剩下的事情就由Tomcat帮我们处理了。
为了解耦,HTTP服务器不能直接调用Servlet,而是把请求交给Servlet容器来处理,那 Servlet容器又是怎么工作的呢?
当tomcat的HTTP服务器接收到客户某个资源请求时,HTTP服务器会用一个ServletRequest对象把客户的请求信息封 装起来,然后调用Servlet容器的service方法,Servlet容器拿到请求后,根据请求的URL 和Servlet的映射关系,找到相应的Servlet,如果Servlet还没有被加载,就用反射机制加载创建这个Servlet,并调用Servlet的init方法来完成初始化,接着调用Servlet的service方法 来处理请求,将处理结果放在ServletResponse对象中, 把ServletResponse对象返回给HTTP服务器,HTTP服务器会把响应发送给 客户端。
1)定位servlet
2)加载servlet
3)调用servlet
了解了上面的内容,我们可以知道tomcat要实现的核心功能有两个:
1) 处理Socket连接,负责网络字节流与Request和Response对象的转化。(接收请求,返回请求结果给浏览器)
2) 加载和管理Servlet,以及具体处理Request请求。 (处理请求)
为了实现这两个核心功能, Tomcat设计了两个核心组件连接器(Connector)和容器(Container)来分别做这 两件事情。连接器负责对外交流(接收请求),容器负责内部处理(处理请求)。
如图所示,在一个tomcat服务器中,有多个service服务,一个service服务都包含有多个连接器以及一个容器。当浏览器发起一个Socket连接请求时,该请求会被连接器接收到,然后连接器会将该请求封装为一个(暂且认为是)Request对象,并将该对象转交给容器进行处理。
容器拿到一个ServletRequest请求对象后会去容器中定位该请求对应的Servlet程序,若该Servlet程序还没有被加载就会先去加载这个Servlet程序,之后再调用Servlet程序的相应业务功能进行处理请求。处理完具体的业务后就将结果响应给连接器一个ServletResponse对象
连接器拿到容器响应的ServletResponse对象后要解析该对象,然后才响应回浏览器。
以上便是tomcat处理请求的主要流程,其中包括了两个核心组件连接器(Connector)和容器(Container)。这里的连接器可以看成是上面的HTTP服务器,相应的容器也可看成是servlet容器。
连接器作为tomcat的一个核心组件,其中又包含了哪些内容呢?
Coyote 是Tomcat的连接器整体框架的名称 , 是Tomcat服务器提供的供客户端访问的一个外部接口。也就是说客户端浏览器通过Coyote与tomcat服务器建立连接、发送请求并接收响应 。 那么Coyote是如何实现接收请求连接并响应数据回浏览器的呢?
上面我们介绍的HTTP原理中已经讲明,服务器与客户端传输数据要经过TCP的三次握手建立连接,之后才传输数据流。而我们的 Coyote 封装了底层的网络通信(Socket 请求及响应处理),同时也为Catalina容器(先简单理解为servlet容器)提供了统一 的接口,使Catalina 容器与具体的请求协议及IO操作方式完全解耦(连接器只管接收请求,回响数据;servlet容器只管处理请求)。
Coyote 将Socket 输入转换封装为 Request 对象,交由Catalina 容器进行处理,处理请求完成后, Catalina 通 过Coyote 提供的Response 对象将结果写入输出流 。
Coyote 作为独立的模块,只负责具体协议和IO的相关操作, 与Servlet 规范实现没有直 接关系,因此即便是 Request 和 Response 对象也并未实现Servlet规范对应的接口, 而 是在Catalina 中将他们进一步封装为ServletRequest 对象和 ServletResponse对象 。 这样就实现了解耦。
既然coyote只负责具体协议和IO的相关操作,那么我们就来看看它支持哪些协议与io操作,如下表所示
支持的IO模型
8.0之前tomcat默认采用的IO方式为BIO,tomcat8.5起移除了BIO支持,现在默认为NIO
支持的应用层协议:
Tomcat为了实现支持多种I/O模型和应用层协议这个需求,一个容器就有可能对接多个连接器。但是单独的连接器或者容器都不能对外提供服务,需要把它们组装 起来才能工作,组装后这个整体叫作Service组件。Service本身没有做什么重要的事情,只是在连接器和容器外面多包了一层,把它们组装在一起,真正工作的还是连接器与容器。Tomcat内可 能有多个Service,这样的设计也是出于灵活性的考虑。通过在Tomcat中配置多个 Service,可以实现通过不同的端口号来访问同一台机器上部署的不同应用。
了解了连接器中支持的io以及协议,现在我们来了解一下连接器中的各个组件以及他们是如何协调工作的。
从上图我们可以看出,连接器中的组件包括了三个部分: Adapter、EndPoint 、 Processor,其中 ProtocolHandler 由EndPoint 、 Processor构成。
首先浏览器发起了一个请求,因为HTTP协议应用层协议的底层传输协议是TCP协议,TCP连接又是基于Socekt进行传输的。所以这个浏览器的请求实际上也是一个Socekt请求,该请求首先会被连接器组件EndPoint 接收到,然后EndPoint 将Socket请求发送给Processor处理器;Processor处理器收到后就将请求转化为一个Http协议的请求,然后解析并封装为一个request对象,这个对象是Coyote实现的,不符合servlet规范,与ServletRequest对象不一样,ServletRequest对象是符合servlet规范的。且ServletRequest对象能调用容器的service方法而coyote封装的request对象不能,这个时候就需要用到Adapter适配器(适配模式)来转化为ServletRequest对象并且调用容器中的service方法进行处理。
由上可知,其中各个组件的作用及介绍如下:
1) EndPoint : Coyote 通信端点,即通信监听的接口,是具体Socket接收和发送处理器,是对传输层的抽象,因此EndPoint用来实现TCP/IP协议的。
2) Tomcat 并没有EndPoint 接口,而是提供了一个抽象类AbstractEndpoint , 里面定 义了两个内部类:Acceptor和SocketProcessor。Acceptor用于监听Socket连接请求。 SocketProcessor用于处理接收到的Socket请求,它实现Runnable接口,在Run方法里 调用协议处理组件Processor进行处理。为了提高处理能力,SocketProcessor被提交到 线程池来执行。这个线程池也叫作执行器(Executor)。
Processor : Coyote 协议处理接口 ,如果说EndPoint是用来实现TCP/IP协议的,那么 Processor用来实现HTTP协议,Processor接收来自EndPoint的Socket,读取字节流解 析成Tomcat的 Request和Response对象,并通过Adapter将其提交到容器处理, Processor是对应用层协议的抽象。
ProtocolHandler: Coyote 协议接口, 通过Endpoint 和 Processor , 实现针对具体协 议的处理能力。Tomcat 按照协议和I/O 提供了6个实现类 : AjpNioProtocol , AjpAprProtocol, AjpNio2Protocol ,
Http11NioProtocol ,Http11Nio2Protocol , Http11AprProtocol。我们在配置tomcat/conf/server.xml 时 , 至少要指定具体的 ProtocolHandler , 当然也可以指定协议名称 , 如 : HTTP/1.1 ,如果安装了APR,那么 将使用Http11AprProtocol , 否则使用 Http11NioProtocol 。
由于协议不同,客户端发过来的请求信息也不尽相同,Tomcat定义了自己的Request类 来“存放”这些请求信息。ProtocolHandler接口负责解析请求并生成Tomcat的 Request类。 但是这个Request对象不是标准的ServletRequest,也就意味着,不能用Tomcat 的 Request作为参数来调用容器。Tomcat设计者的解决方案是引入CoyoteAdapter,这是 适配器模式的经典运用,连接器调用CoyoteAdapter的Sevice方法,传入的是Tomcat的 Request对象,CoyoteAdapter负责将Tomcat的 Request转成ServletRequest,再调用容 器的Service方法。
上面介绍完了连接器,我们再来看看Catalina 容器 。 Tomcat是一个由一系列可配置的组件构成的Web容器,而Catalina是Tomcat的servlet容器。
Catalina 是Servlet 容器的一个实现,包含了所有的容器类组件,以及后续涉及到 的安全、会话、集群、管理等Servlet 容器架构的各个方面。它通过松耦合的方式集成 Coyote,以完成按照请求协议进行数据读写。同时,它还包括启动入口、Shell程序等。
Tomcat 的模块分层结构图
Tomcat 本质上就是一款 Servlet 容器, 因此Catalina 是 Tomcat 的核心中的核心 , 其他模块 都是为Catalina 提供支撑的。 比如 : 通过Coyote 模块提供链接通信,Jasper 模块提供 JSP引擎,Naming 提供JNDI 服务,Juli 提供日志服务。 这个图我们也可以在tomcat的源码中找到相应的包。
我们上面主要介绍的是容器以及连接器,其他内容以后再慢慢分析。
由于Catalina 是 Tomcat 的核心中的核心 , 其他模块 都是为Catalina 提供支撑的,这里的Catalina 集成了上面说到的各个组件,不再是简单的servlet容器了,可以模糊的认为Catalina它就是指tomcat。
正如上图所示,Catalina负责管理Server,而Server表示着整个服务器。Server下面有多个 服务Service,每个服务都包含着多个连接器组件Connector(Coyote 实现)和一个容器 组件Container。在Tomcat 启动的时候, 会初始化一个Catalina的实例。
接下来我们看看Container
Tomcat设计了4种容器,分别是Engine、Host、Context和Wrapper。这4种容器不是平行关系,而是父子关系。 Tomcat通过一种分层的架构,使得Servlet容器具有很好的灵 活性 。
各个组件的含义:(service.xml文件中也有相应的介绍)
<Server>
<Service>
<Connector>
<Engine>
<Host>
<Context>
Context>
Host>
Engine>
Connector>
Service>
Server>
那么,Tomcat是怎么管理这些容器的呢?这些容器具有父子关系,会形成一个树 形结构,是不是马上就能想到设计模式中的组合模式。没错,Tomcat就是用组合模式来 管理这些容器的。具体实现方法是,所有容器组件都实现了Container接口,因此组合模 式可以使得用户对单容器对象和组合容器对象的使用具有一致性。这里单容器对象指的 是最底层的Wrapper,组合容器对象指的是上面的Context、Host或者Engine。
这些可以在源码中体现:在源码中打开Container接口后按CTRL+H
1) 在windows系统中启动tomcat ,只要找到tomcat的安装路径中的bin目录,双击startup.bat (在linux 目录下 , 需要调用 bin/startup.sh)即可 , 在startup.bat 脚本中, 调用了bin目录下的catalina.bat脚本。
2) 在catalina.bat 脚本文件中,声明了MAINCLASS为BootStrap ,并调用了BootStrap 中的main方法。
3)在BootStrap 的main 方法中调用了 init 方法 , 来创建Catalina 及初始化类加载器。
4)在BootStrap 的main 方法中调用了 load 方法 , 在其中又调用了Catalina的load方 法。
5)在Catalina 的load 方法中 , 创建了一个server。而在server的init方法中又去调用了(可能多个)service的init方法。然后service的初始化方法又去调用Engine引擎初始化,然后相继初始化一个Host、Context。service的init还去初始化了一个Executor线程池,初始化了线程池后又去初始化了一个Connector连接器,接着初始化一个ProtocolHandler,然后在ProtocolHandler中初始化一个EndPoint,并在EndPoint中绑定tomcat要监听的端口。
6)在将所有相关的组件初始化完成之后,在bootstrap中又调用了一个start方法,依次启动刚才初始化的组件。
从上面的图中不难看出,几乎每个组件都有属于自己的init、start方法,这么多的组件,tomcat是如何识别他们的呢?
由于所有的组件均存在初始化、启动、停止等生命周期方法,拥有生命周期管理的特 性, 所以Tomcat在设计的时候, 基于生命周期管理抽象成了一个接口 Lifecycle ,而组 件 Server、Service、Container、Executor、Connector 组件 , 都实现了一个生命周期 的接口,从而具有了以下生命周期中的核心方法:
1) init():初始化组件
2) start():启动组件
3) stop():停止组件
4) destroy():销毁组件
由于我们提到的Server、Service、Engine、Host、Context继承Lifecycle接口的差不多都是接口,那么我们很自然的要问,这些子接口的实现类又是什么呢?所以我们现在就来介绍一下各个组件的默认实现类:
当前对于 Endpoint组件来说,在Tomcat中没有对应的Endpoint 接口, 但是有一个抽象类 AbstractEndpoint ,其下有三个实现类: NioEndpoint、 Nio2Endpoint、AprEndpoint , 这三个实现类,分别对应于前面讲解链接器 Coyote 时, 提到的链接器支持的三种IO模型:NIO,NIO2,APR , Tomcat8.5版本中,默认采 用的是 NioEndpoint。
下图中罗列了其他接口的默认实现类
协议接口 ProtocolHandler : Coyote协议接口,通过封装Endpoint和Processor , 实现针对具体 协议的处理功能。Tomcat按照协议和IO提供了6个实现类。
AJP协议:
1) AjpNioProtocol :采用NIO的IO模型。
2) AjpNio2Protocol:采用NIO2的IO模型。
3) AjpAprProtocol :采用APR的IO模型,需要依赖于APR库。
HTTP协议:
1) Http11NioProtocol :采用NIO的IO模型,默认使用的协议(如果服务器没有安装 APR)。
2) Http11Nio2Protocol:采用NIO2的IO模型。
3) Http11AprProtocol :采用APR的IO模型,需要依赖于APR库
|
|
|
|
|
|
Tomcat设计了这么多层次的容器,让人眼花缭乱,那它是怎么将每一个请求定位到tomcat服务器中部署某个项目的某个servlet程序中的,即它是如何判断一个请求由哪个Wrapper容器里的 Servlet来处理的呢?
答案是,Tomcat是用Mapper组件来完成这个任务的。 Mapper组件的功能就是将用户请求的URL定位到一个Servlet,它的工作原理是: Mapper组件里保存了Web应用的配置信息,其实就是容器组件与访问路径的映射关系, 比如Host容器里配置的域名、Context容器里的Web应用路径,以及Wrapper容器里 Servlet映射的路径,你可以简单地将这些配置信息就是一个多层次的Map 。
当一个请求到来时,Mapper组件通过解析请求URL里的域名和路径,再到自己保存的 Map里去查找,就能定位到一个Servlet。 注意,一个请求URL最后只会定位到一个 Wrapper容器,也就是一个Servlet。
下面的示意图中 , 就描述了 当用户的请求链接 http://www.ititit.cn:8080/servlet_demo01/bbs/findAll 之 后,根据请求路径找到最终处理业务逻辑的servlet 。
大致过程如下:
1)浏览器通过http/Tcp协议定位到服务器计算机(一般就是你的电脑),由tomcat的连接器去找到Engine,而在tomcat的server.xml文件中我们可以看到Engine下有一个默认的Host虚拟主机:localhost
server.xml文件下面有关于该名为:localhost的Host主机的信息
2)找到这个名为localhost的虚拟主机后会去它的appBase属性指定的webapps目录,也就是安装目录中的webapps目录(你可以修改但不建议)
我们再来看看浏览器请求的URL: http://localhost:8080/servlet_demo01/bbs/findAll ,那么在找到并进入到webapps目录后,就会跟据请求路径中的项目名servlet_demo01找到并进入与这个同名的项目
进入到具体的项目之后如何找到相应的servlet之后取决于项目的web.xml中的配置信息,在本例子中servlet的映射信息如下
找到该配置信息后,根据servlet-mapping标签中的servlet-name找到与之相同的servlet-name的servlet标签,然后根据该servlet中的servlet-class找到对应的servlet程序类,然后执行请求处理。然后将请求结果原路返回
上面只是描述了根据请求的URL如何查找到需要执行的Servlet , 那么下面我们再来解析一下 , 从Tomcat源码的设计架构层面来分析Tomcat的请求处理。
当有请求时Connector组件中的Endpoint会启动一个Socket去监听并接收客户端的请求,接收到请求后将该请求交给Processor处理。
Processor进行处理时,读取消息报文,解析请求行、请求体、请求头,封装成Request对象 、response对象(解析响应结果),然后Processor需要去调用Engine,而Engine需要的是ServletRequest对象,所以Processor会先将请求交给适配器CoyoteAdapter,CoyoteAdapter拿到请求后会去Mapper映射中查找到相应的信息进行路径映射,然后由CoyoteAdapter去请求Engine,然后依次请求Host、Context、Wrapper,在Wrapper里边构造FilterChain过滤链,然后执行FilterChain中的各个filter,当filter执行完后,才去执行servlet程序。
有空补上~~~