Tomcat底层原理

Posted weixin_42412601

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Tomcat底层原理相关的知识,希望对你有一定的参考价值。

目录

前言

基于Tomcat7

1、Tomcat是一个Servlet容器。
2、使用Java代码模拟一个Tomcat容器:

class Tomcat
	List<Servlet> servlets;
	Connector connect;//处理请求,生成了Request

3、回顾servlet的定义

public class MyHttpServlet extends HttpServlet 
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException 
        resp.getWriter().println("http");
    

问题1:我们定义的servlet,它的实例是怎么生成的?doGet方法又是怎么调用到的呢?在哪执行的?

MyHttpServlet  myHttpServlet=new MyHttpServlet (); 
myHttpServlet.doGet();

问题2:调用doGet()方法时,HttpServletRequestHttpServletResponse都是接口,那么入参的实际类型是什么?
实际上HttpServletRequestHttpServletResponse接口的实现类,由servlet容器实现,比如:tomcat,jetty。我们把项目部署到tomcat中,tomcat就提供了一个HttpServletRequestHttpServletResponse的实现类,部署到jetty,jetty就提供了相应的实现类,这是一个规范。
其中,在Tomcat中,HttpServletRequest的实现类就是RequestFacade

4、RequestFacade-门面模式
RequestFacade实现了HttpServletRequest,充当门面模式中的外观类。RequestFacade屏蔽内部子系统的细节。
RequestFacade代表的是一个请求,实际上是RequestFacade的属性Request,才是真正代表的一个请求,外界的http请求信息,都封装在这个Request对象中,只是利用门面模式的外观类,把它屏蔽在里面了。

Request本身也是实现了接口HttpServletRequest

5、应用部署到Tomcat有几种部署方式。

  • war包部署
  • 文件夹部署
  • 描述符部署
<Context path="/ServletHello1" docBase="xxx" />	

好了,现在这个Context就引出来一个容器,在Tomcat里有一个接口就叫Context

Tomcat底层架构组成

Container容器是父接口,所有的子容器都必须实现这个接口,在TomcatContainer容器的设计是典型的责任链设计模式,其有四个子容器:Engine、Host、ContextWrapper。这四个容器之间是父子关系,Engine容器包含HostHost包含ContextContext包含Wrapper

我们在web项目中的一个Servlet类对应一个Wrapper,多个Servlet就对应多个Wrapper,当有多个Wrapper的时候就需要一个容器来管理这些Wrapper了,这就是Context容器了,Context容器对应一个工程,所以我们新部署一个工程到Tomcat中就会新创建一个Context容器。

Engine->Host->Context->Wrapper->Servlet

1、Context——servlet容器

Context继承自Container容器接口

context表示的就是应用,这个应用是一个servlet容器。

path:是访问时的根地址,表示访问的路径。就是项目路径,根据请求带的项目路径,来确定使用哪个Context来处理请求
docbase:表示应用程序的路径,注意斜杠的方向“/”。应用编译后的class文件的路径。

2、Host——servlet容器

Host表示虚拟主机,一个虚拟主机下可以定义很多个Context,即可以部署多个项目

  • name:表示虚拟主机的名字,就是对应请求的域名,根据域名来确定使用哪个虚拟主机
  • appBase:表示应用存放的目录
  • unpackWARs:表示是否需要解压
  • autoDeploy:热部署

3、Engine——servlet容器

Engine引擎包含多个Host,它的责任就是将用户请求分配给一个虚拟上机处理。

  • name:表示引擎的逻辑名称,在日志和错误消息中会用到,在同一台服务器上有多个Service时,name必须唯一。
  • defaultHost:指定默认主机,如果没有分配哪个主机来执行用户请求,由这个值所指定的主机来处理,这个值必须和<Host>元素中的其中一个相同。

4、Wrapper——servlet容器

Context是一个servlet容器,但是它并不是直接装servlet实例,可以简单的理解,Context包含了多个Wrapper

class Context
	List<Wrapper> wrappers;

Wrapper才是装了多个Servlet实例,注意装的是某一个类型的servlet实例,比如,我自定一了一个servlet,就叫MyServlet,那么就有一个Wrapper里装的都是MyServlet的实例。

class Wrapper
	List<servlet> servlet;//装的是某一个类型的servlet实例

  • 一般servlet都是单例的,所有访问同一个servlet的请求是共用同一个servlet实例的。
  • 定义的servlet实现了SingleThreadModel接口,每一个访问这个servlet的请求,单独有一个servlet实例,既然servlet支持了这个功能,肯定要去实现这个功能,因此,就有了wrapper

5、Pipiline管道

前面的4个容器都包含Pipiline管道

Tomcat中,对于每一个容器,都有一个公共的组件Pipiline管道,每个管道下可以有多个阀门Valve,一个阀表示一个具体的执行任务,在servlet容器的管道中,除了有一个基础阀BaseValve,还可以添加任意数量的阀。阀的数量指的是额外添加的阀数量,即不包括基础阀。可以通过编辑Tomcat的配置文件(server.xml)来动态地添加阀。

例如:

还可以自定义阀门

public class TestValve extends RequestFilterValve 
    @Override
    public void invoke(Request request, Response response) throws IOException, ServletException 
        
    
    @Override
    protected Log getLog() 
        return null;
    

下图显示了一条管道及其阀:

如果对servlet编程中的过滤器有所了解的话,那么应该不难想像管道和阀的工作机制。管道就像过滤器链一样,而阀则好似是过滤器。阀与过滤器类似,可以处理传递给它的request对象和response对象。当一个阀执行完成后,会调用下一个阀继续执行。基础阀总是最后一个执行的。

Tomcat的请求处理流程

问:Wrapper容器的管道中的最后一个阀门,是怎样把请求转发给对应的servlet的?


看一下阀门的StandardWrapperValveinvoke方法的核心代码:


allocate方法里的loadServlet方法,直接newInstance一个serlvet实例

servlet = (Servlet) instanceManager.newInstance(servletClass);

这也回答了前言里的问题1,servlet是怎么生成的,在哪生成的?
servlet是在Wrapper的基础阀里生成的。

servlet实例有了,那么doGet/doPost在哪执行呢?
回到阀门的StandardWrapperValveinvoke方法的核心代码:来到这一行代码,传进servlet实例,并且返回一个过滤器链

我们定义一个Filter的时候,可以像下面这样写:

执行过程:filter->servlet->再回到filter
因此,filterChain.doFilter(servletRequest, servletResponse);虽然是调用其他过滤器,但是过滤器调用完之后,必然要去调用servletdoget方法。
所以,上面才需要传入servlet实例,然后获取一个过滤器链,因为要用到这个servlet

继续往下走:

进入doFilter,再进入internalDoFilter方法:发现并没有执行,servletdoGet/dopost方法,执行的是servletservice方法

我们自定义servlet的时候,并没有service

往父类HttpServlet中找找:

因此,在哪里调用的doGetdoPost方法,与Tomcat没有关系,这个实际上是servlet规范所定义的。

Tomcat架构平视图


右边的部分,已经说过了,看看左边。

Request对象怎么生成的?

1、Request对象表示的是一个请求,Tomcat要生成一个Request对象,首先就要有数据,这个数据拿来的呢?
操作系统。Tomcat仅仅是操作系统上的一个应用程序,因此,它的数据开源于操作系统。

2、那操作系统的数据又从哪来的呢?
操作系统安装在服务器上面的,因此,操作系统的数据来源于服务器。

3、那服务器的数据又来源于哪里呢?
一个服务器通过网络将数据发给另一个服务器的。这就涉及到很多很多的协议了,服务器之间想要完成数据的传输和接受,就和计算机网络有关系了,跟各种协议有关系。

4、如果服务器A有数据,想把数据发给服务器B,但是仅仅有数据和IP(没有端口,端口是和应用程序对应的),服务器A能够保证数据安全可靠的发给服务器B吗
不能。可能数据非常大,数据在网络的传输过程中,会经过机房或者交换机,很有可能数据就会丢失,是不可靠的。

5、如何保证数据的可靠传输呢?
使用Tcp协议。该协议是一个可靠的协议,但是该协议,毕竟只是一个协议,这个协议肯定是需要去实现的。

6、Tcp协议由谁实现?
操作系统。linuxWindows操作系统,或者其他操作系统都会去实现Tcp协议。
Tcp协议在服务器之间建立连接时,会进行三次握手,linux源码就有关于Tcp三次握手的相关源码:



注意:Tcp协议只是保证数据在传输层可靠的传输,但是它并不关心数据长什么样子,也不关心数据的格式以及代表的意义,谁才关系数据的格式是怎么的,内容是怎么的呢?当然是使用数据的人和发送数据的人啊。浏览器和应用程序,因此,Http协议就有了,它是应用层协议,对我们要发送的数据进行规范,数据的格式,内容,数据的意义。

7、Http协议由谁实现?
浏览器、应用程序(包括Tomcat)

8、如果用java代码去实现一个浏览器,当用户在浏览器的地址栏输入地址后,按下回车键,代码执行的流程是怎么样的
1、肯定是要根据Http协议,去构造出符合Http协议的数据格式
2、发送数据,建立Tcp连接。
3、应用程序,接受数据

问题来了,java代码里怎么去建立Tcp连接,我们知道操作系统的源码里有建立Tcp连接的代码,那么Java能不能去调用操作系统的建立三次握手的代码,比如:tcp_connect()方法,以此来建立Tcp连接。

实际上建立Tcp连接的方法,java是不能直接调用的,因为这些方法是linux操作系统非常核心的方法,不会直接让你调的,实际上像这种情况,我们通常会想到,写一个API,重新定义一个方法,比如:

create_tcp()
//验证
xxxx
//验证通过之后,才让调这个方法
tcp_connect();

liunx里也一样,不会让我们直接调用tcp_connect方法,它提供了一个对外的接口,就是socket,别人不能直接访问tcp_connect,只能通过socket去访问。

因此,回到java代码里怎么建立Tcp连接?通过Socket接口,建立Tcp连接。

其实不仅仅Java应用程序,运行在操作系统上的各种程序,都只能通过Socket去建立Tcp连接。

Java Socket底层实现

使用Java Socket建立一个tcp连接,如下:

 public static void main(String[] args) throws IOException 
     Socket socket = new Socket();//tcp
     socket.connect(new InetSocketAddress("localhost",9090));

     DatagramSocket datagramSocket = new DatagramSocket();//udp
 

JavaSocket类底层是不是直接调的操作系统的Socket呢?它们有没有什么联系呢?
1、进入connect方法

2、进入createImpl

3、进入AbstractPlainSocketImplcreate

4、进入socketCreate,创建一个Socket

5、进入DualStackPlainSocketImplsocketCreate

发现socket0是一个native方法

nativesocket0,代码只能去open jdk中去看socket0是怎么实现的.
7、DualStackPlainSocketImpl.c文件:

8、net_util_md.c文件:

socket(domain,type,protocol)又是怎么实现的呢?
但是该方法的实现,是在open jdk中找不到的。那么它到底在哪里实现的
9、看到net_util_md.c文件的头文件

那么,在当前的windows操作系统中找找有没有这个头文件。
win10如下:

这个就是上面socket(domain,type,protocol)的真正实现

10、回头看Java在创建一个Socket连接的时候,socket.connect(new InetSocketAddress("localhost",9090))这行代码,会先去调用Jdk代码,jdk最终调用的是操作系统的代码

Connector组件

回到Tomcat架构平视图中的8,浏览器会负责去构造数据,发送数据,那么Tomcat接受数据后,需要解析数据,这个时候就要去实现Http协议。

Tomcat使用socket接受数据,然后就要取数据,这里就涉及到一个概念,叫做IO模型,就是你通过什么方式去取数据的呢,是以BIO还是NIO呢?

Connector组件,会从socket中去取数据,然后根据Http协议去解析数据,解析成Request对象。

1、BIO的方式取数据

这个Connector组件,在Tomcat中对应有一个类Connector,有一个方法setProtocol,入参就是上图的protocol属性(Tomcat启动会去解析server.xml

public void setProtocol(String protocol) 
    if (AprLifecycleListener.isAprAvailable()) 
        if ("HTTP/1.1".equals(protocol)) 
            setProtocolHandlerClassName
            ("org.apache.coyote.http11.Http11AprProtocol");
         else if ("AJP/1.3".equals(protocol)) 
            setProtocolHandlerClassName
            ("org.apache.coyote.ajp.AjpAprProtocol");
         else if (protocol != null) 
            setProtocolHandlerClassName(protocol);
         else 
            setProtocolHandlerClassName
            ("org.apache.coyote.http11.Http11AprProtocol");
        
     else 
    	//当`protocol` 属性设置为`Http1.1`时,对应的类是`org.apache.coyote.http11.Http11Protocol`
        if ("HTTP/1.1".equals(protocol)) 
            setProtocolHandlerClassName
            ("org.apache.coyote.http11.Http11Protocol");
         else if ("AJP/1.3".equals(protocol)) 
            setProtocolHandlerClassName
            ("org.apache.coyote.ajp.AjpProtocol");
         else if (protocol != null) 
            setProtocolHandlerClassName(protocol);
        
    

protocol 属性设置为Http1.1时,代码里对应的类是org.apache.coyote.http11.Http11Protocol,它对应的协议是Http1.1,对应的io模型是BIO
1、看看Http11Protocol源码,为啥说它对应的是BIO

2、看看JIoEndpoint

使用的工厂模式创建socket对象。

2、NIO的方式取数据

如果想使用NIO,只需要修改:protocol="HTTP/1.1"变为org.apache.coyote.http11.Http11NioProtocol

<Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol"
           connectionTimeout="20000"
           redirectPort="8443" />

启动Tomcat就户发现走的是下面的红框了


1、看看Http11NioProtocol。查看方法类似上面查看BIO

2、看看NioEndpoint

使用的是SocketChannel,明显使用的是NIO,学过nio就知道了

3、解析数据

不管是BIO还是NIO方式取数据,反正是获取到了对应的Socket。接下来就是解析数据了。

对于BIO方式:会在processSocket方法中处理数据,对应nio方式。猜想Tomcat取数据,应该是socket.getInputStream()来取数据,然后按照Http1.1的格式解析数据

1、processSocket方法

Http协议的格式:

2、包装socket,然后把这个socket连接交给线程池,去处理。这个线程池在Tomcat7中默认10条线程,private int minSpareThreads = 10;

3、进入SocketProcessor,是一个线程

4、process方法

5、AbstractHttp11Processorprocess,因为BIO对应的是Http11


解析请求行和请求头的方法里,都会把解析出来的数据,设置到Request对象里。

socket.getInputStream()取出来的数据,不是直接使用的,会放到缓存中。

以上是关于Tomcat底层原理的主要内容,如果未能解决你的问题,请参考以下文章

TOMCAT8源码分析——处理请求分析(下)

阀门的工作原理(图)

Tomcat底层原理

JVM17_Tomcat打破双亲委派机制执行顺序底层代码原理Tomcat|JDBC破坏双亲委派机制带来的面试题

Tomcat打破双亲委派机制执行顺序底层代码原理JVM04_Tomcat JDBC破坏双亲委派机制带来的面试

阀门的基础知识和常见的各种阀门的用途