• Tomcat学习笔记


    Tomcat学习笔记

    转载请声明作者和出处!!!

    本文如有错误欢迎指正,感激不尽。

    一、Tomcat概述

    1.1 浏览器访问流程

    我们先从一个http请求的流程开始

    bs流程20220110

    浏览器访问服务器使用的是HTTP协议,HTTP协议是应用层协议,数据传输是TCP/IP协议。我们的tomcat就是一个HTTP服务器。

    1.2 tomcat总体架构

    上面我们的请求最终会走到HTTP服务器中,而我们的tomcat也是一个http服务器,因为tomcat能够接收并处理http请求。

    当请求走到tomcat后,接收到请求之后把请求交给Servlet容器来处理,Servlet 容器通过Servlet接口调⽤业务类。Servlet接口和Servlet容器这⼀整套内容叫作Servlet规范。因此tomcat既有http服务器的功能又按照Servlet规范实现了Servlet容器,所以tomcat又是一个Servlet容器。

    tomcat处理流程

    当请求到来时:

    1. HTTP服务器会把请求信息使用ServletRequest对象封装起来
    2. Servlet容器拿到请求后,根据URL和Service的对应关系,定位到到具体的Servlet中
    3. 如果Servlet还没有被加载,则通过反射创建这个Servlet,并调用Servlet的init方法来完成初始化。
    4. 接着调用对应的业务类方法处理请求,然后将结果用ServletResponse封装
    5. 把ServletResponse对象返回给HTTP服务器,HTTP服务器会把相应发送给客户端。

    tomcat请求流程

    tomcat总体架构

    通过上面的流程我们发现tomcat有两个很重要的功能,一个是与客户端交互,进行socket通信,将字节流和Request/Response进行转换另一个就是Servlet容器业务处理逻辑。

    tomcat架构20220110

    tomcat设计了两个核心组件:连接器(Connector)和容器(Container)来完成这两个功能。

    下面我们介绍这两个组件。

    1.3 连接器组件Coyote

    Coyote是tomcat中连接器的组件名称,客户端通过Coyote和服务器建立连接,发送请求并接受响应,同时它使Catalina与具体的请求协议和IO操作完全解耦。Coyote负责的时具体协议(应用层)和IO(传输层)的相关内容,它将Socket输入转换封装为Request对象,进一步封装后交由Catalina容器进行处理,处理完请求后,Catalina通过Coyote提供的Response对象将结果写入输出流。

    coyote20220111

    Tomcat Coyote支持的协议:

    应用层协议 描述
    HTTP/1.1 大部分web应用采用的协议,也是tomcat默认协议
    AJP 用于和WX集成(如Apache),以实现对静态资源的优化以及集群部署,当前支持AJP/1.3
    HTTP/2 HTTP2.0大幅度提升了Web性能。自8.5以及9.0版本之后支持

    Tomcat Coyote支持的IO模型:

    IO模型 描述
    NIO 非阻塞IO,采用Java NIO类库实现
    NIO2 异步IO,采用JDK7最新的NIO2类库实现
    APR 采用Apache可移植运行库实现,是C/C++编写的本地库。如果选择该方案,需要单独安装APR库

    在tomcat8.0前,默认采用BIO,之后改为NIO。⽆论 NIO、NIO2 还是 APR, 在性能⽅⾯均优于以往的BIO。 如果采⽤APR, 甚⾄可以达到 Apache HTTP Server 的影响性能。

    Coyote内部组件

    Coyoto组件20220111

    组件 作用描述
    EndPoint EndPoint是Coyote的通信端点,即通信监听的端口,是具体Socket接收和发送处理器,是传输层的对象,因此EndPoint用来实现TCP/IP协议的
    Processor Processor是Coyote协议处理接口,如果说Endpoint是用来实现TCP/IP协议的,那么Processor用来实现HTTP协议,Processor接收来自EndPoint的Socket,读取字节流解析成Tomcat Request和Response对象,并通过Adapter将其提交到容器处理,Processor是对应用层协议的抽象。
    ProtocolHandler Coyote协议接口,通过EndPoint和Processor,实现针对具体协议的处理能力。Tomcat按照协议和IO提供了6个实现类:AjpNioProtocol,AjpAprProtocol,AjpNio2Protocol,Http11NioProtocol,Http11Nio2Protocol,Http11AprProtocol
    Adapter 由于协议不同,客户端发过来的消息也不同,tomcat定义了自己的Request类,来封装这些请求信息。ProtocolHandler接口负责解析请求并生成Tomcat Request类。但是这个Tomcat Request对象不是标准的ServletRequest。所以引入了CoyoteAdapter,这是适配器模式的经典应用,连接器调用CoyoteAdapter的service方法,传入Tomcat Request对象,然后CoyoteAdapter将Tomcat Request对象转换成Servlet Request对象然后调用容器。

    1.4 Servlet容器Catalina

    Tomcat是由一系列可配置(conf/server.xml)的组件构成的web容器,而Catalina是Tomcat的servlet容器。从另一个角度,Tomcat本质就是一款Servlet容器,因为Catalina才是Tomcat的核心,其他模块都是为了Catalina支撑的。比如Coyote提供链接通信,Jasper提供JSP引擎,Naming提供JBDI服务,Juli提供日志服务。

    其实,可以认为整个Tomcat就是一个Catalina实例,Tomcat启动的时候会初始化这个实例,Catalina实例通过加载server.xml完成其他实例的创建,创建并管理Server,Server创建并管理多个服务,每个服务又可以有多个Connector和一个Container,各个组件介绍如下:

    TomcatServer20220112

    1. Catalina

      负责解析Tomcat的配置文件server.xml,以此来创建服务器server组件并进行管理

    2. Server

      服务器表示整个Catalina Servlet容器以及其他组件,负责组装并启动Servlet引擎,Tomcat连接器。Server通过实现Lifecycle接口,提供一种优雅的启动和关闭整个系统的方式

    3. Service

      服务是Server内部的组件,一个Server包含多个Service。它将若干个Connector组件绑定到一个Container

    4. Container

      容器,负责处理用户的Servlet请求,并返回对象给web用户的模块

    Container组件有以下几种具体的组件,分别是Engine、Host、Context和Wrapper,这4个组件是父子关系:

    1. Engine

      表示整个Catalina的Servlet引擎,用来管理多个虚拟站点,一个Service最多只能有一个Engine,但一个引擎可以包含多个Host

    2. Host

      代表一个虚拟主机,或者一个站点,可以给Tomcat配置多个虚拟主机地址,而一个虚拟主机可包含多个Context

    3. Context

      表示一个Web应用程序,一个Web应用可包含多个Wrapper

    4. Wrapper

      表示一个Servlet,Wrapper作为容器中的最底层不能包含子容器

    二、Tomcat核心配置

    2.1 配置详情

    上述的组件都可以在conf/server.xml文件中体现,Tomcat作为服务器的配置主要是在server.xml的配置,里面包含了Servlet容器的相关配置,即Catalina的配置。具体配置内容如下:

    server.xml

    <!--port:关闭服务器的监听端口
    	shutdown:关闭服务器的指令字符串
    -->
    <Server port="8005" shutdown="SHUTDOWN">
      <!-- 以日志的形式输出服务器、操作系统、JVM版本信息-->
      <Listener className="org.apache.catalina.startup.VersionLoggerListener" />
      <!-- 加载和销毁APR,如果找不到APR库就会输出日志,并不影响Tomcat启动-->
      <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
      <!-- 避免JRE内存泄漏问题-->
      <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
      <!-- 加载和销毁全局命名服务-->
      <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
      <!-- 在Context停止时重建Executor池中的线程,避免ThreadLocal相关内存泄漏-->
      <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />
       <!--定义了全局命名服务 -->
      <GlobalNamingResources>
        <Resource name="UserDatabase" auth="Container"
                  type="org.apache.catalina.UserDatabase"
                  description="User database that can be updated and saved"
                  factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
                  pathname="conf/tomcat-users.xml" />
      </GlobalNamingResources>
    
      <!--
    	定义一个Service服务,一个Server可以有多个Service
       -->
        <Service name="Catalina">
            <!--内容见下面-->
        </Service>
    </Server>
    

    service标签及内部具体组件

    <!-- 用于创建Service实例,默认使用 org.apahce.catalina.core.StandardService
    	默认情况下,仅指定Service的名称即name="Catalina"
    	service子标签:
    	Listener(用于为Service添加生命周期监听器)
    	Executor(用于配置共享线程池)
    	Connector(用于配置Service包含的链接器)
    	Engine(用于配置Service中链接器对应的Servlet容器引擎)
    -->
    <Service name="Catalina">
    
            <!--
    默认情况下,Service 并未添加共享线程池配置。 如果我们想添加⼀个线程池, 可以在<Service> 下添加如下配置:
    name:线程池名称,⽤于 Connector中指定
    namePrefix:所创建的每个线程的名称前缀,⼀个单独的线程名称为
    namePrefix+threadNumber
    maxThreads:池中最⼤线程数
    minSpareThreads:活跃线程数,也就是核⼼池线程数,这些线程不会被销毁,会⼀直存在
    maxIdleTime:线程空闲时间,超过该时间后,空闲线程会被销毁,默认值为6000(1分钟),单位毫秒
    maxQueueSize:在被执⾏前最⼤线程排队数⽬,默认为Int的最⼤值,也就是⼴义的⽆限。除⾮特殊情况,这个值不需要更改,否则会有请求不会被处理的情况发⽣
    prestartminSpareThreads:启动线程池时是否启动 minSpareThreads部分线程。默认值为false,即不启动
    threadPriority:线程池中线程优先级,默认值为5,值从1到10
    className:线程池实现类,未指定情况下,默认实现类org.apache.catalina.core.StandardThreadExecutor如果想使⽤⾃定义线程池⾸先需要实现org.apache.catalina.Executor接⼝
    		-->
    
        <Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
            maxThreads="150" minSpareThreads="4"/>
    
        
        <!-- 默认情况下,server.xml 配置了两个链接器,⼀个⽀持HTTP协议,⼀个⽀持AJP协议,⼤多数情况下,我们并不需要新增链接器配置,只是根据需要对已有链接器进⾏优化 
        -->
        <!-- 
    port:端⼝号,Connector ⽤于创建服务端Socket 并进⾏监听,以等待客户端请求链接。如果该属性设为0,Tomcat将会随机选择⼀个可⽤的端⼝号给当前Connector 使⽤
    
    protocol:当前Connector ⽀持的访问协议。 默认为 HTTP/1.1 , 并采⽤⾃动切换机制选择⼀个基于JAVA NIO的链接器或者基于本地APR的链接器(根据本地是否含有Tomcat的本地库判定)
    
    connectionTimeOut:Connector 接收链接后的等待超时时间, 单位为 毫秒。 -1 表示不超时。
    
    redirectPort:当前Connector 不⽀持SSL请求, 接收到了⼀个请求, 并且也符合security-constraint 约束,
    需要SSL传输,Catalina⾃动将请求重定向到指定的端⼝。
    
    executor:指定共享线程池的名称, 也可以通过maxThreads、minSpareThreads 等属性配置内部线程池。
    
    URIEncoding:⽤于指定编码URI的字符编码, Tomcat8.x版本默认的编码为 UTF-8 , Tomcat7.x版本默认为ISO-
    8859-1 -->
        <Connector port="8080" protocol="HTTP/1.1"
                   connectionTimeout="20000"
                   redirectPort="8443" disableUploadTimeout="true" useBodyEncodingForURI="true" URIEncoding="UTF-8" />
    <!-- Define an AJP 1.3 Connector on port 8009 -->
        <Connector protocol="AJP/1.3"
                   port="8009"
                   redirectPort="8443"  URIEncoding="UTF-8"/>
        
    <!--name: ⽤于指定Engine 的名称, 默认为Catalina
    defaultHost:默认使⽤的虚拟主机名称, 当客户端请求指向的主机⽆效时, 将交由默认的虚拟主机处
    理, 默认为localhost-->
        <Engine name="Catalina" defaultHost="localhost">
    
          <Realm className="org.apache.catalina.realm.LockOutRealm">
    
            <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
                   resourceName="UserDatabase"/>
          </Realm>
    <!--
    Host用于配置主机
    name: ⽤Host 的名称,即Engine指定的名称
    appBase:网站资源信息的、存放位置,默认是webapps
    unpackWARs:是否解压war包
    autoDeploy:是否自动部署
    -->
          <Host name="localhost"  appBase="webapps"
                unpackWARs="true" autoDeploy="true">
            <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
                   prefix="localhost_access_log" suffix=".txt"
                   pattern="%h %l %u %t &quot;%r&quot; %s %b" />
    		<!--Context 配置一个Web应用
    			docBase:web应用的路径,可以是磁盘上的绝对路径,也可以是相对于appBase的相对路径
    			path:Web应用的访问路径,如果我们Host名为localhost, 则该web应⽤访问的根路径为:http://localhost:8080/demo
    		-->
              <Context docBase="..." path="/demo"/>
          </Host>
        </Engine>
      </Service>
    

    2.2 案例演示

    server.xml中大部分配置保持默认即可,我们这里对host标签和context标签的配置进行演示:

    我们首先在host中增加两个网址,用于配置host标签,我这里之前弄eureka时添加过,所以就不添加了,如下:

    127.0.0.1 CloudEurekaServerA
    127.0.0.1 CloudEurekaServerB

    然后在server.xml中,修改host配置:

          <Host name="CloudEurekaServerA"  appBase="webapps"
                unpackWARs="true" autoDeploy="true">
    
            <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
                   prefix="localhost_access_log" suffix=".txt"
                   pattern="%h %l %u %t &quot;%r&quot; %s %b" />
          </Host>
    	  <Host name="CloudEurekaServerB"  appBase="webapps2"
                unpackWARs="true" autoDeploy="true">
    
            <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
                   prefix="localhost_access_log" suffix=".txt"
                   pattern="%h %l %u %t &quot;%r&quot; %s %b" />
          </Host>
    

    然后我们复制一份webapps命名为webapps2,修改两个中的ROOT下的index.jsp(随便改,能区分即可)便于查看,然后我们分别访问两个host,结果如下:

    image-20220112141730338

    image-20220112141749175

    我们下面在host中增加Context标签,我们可以将webapps复制到其他磁盘中,然后修改index.jsp,使用绝对路径配置(相对路径也可以,这里是为了演示方便),配置如下:

    <Host name="CloudEurekaServerA"  appBase="webapps"
                unpackWARs="true" autoDeploy="true">
    		<Context docBase="D:\webapps\ROOT" path="/demo" />
            <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
                   prefix="localhost_access_log" suffix=".txt"
                   pattern="%h %l %u %t &quot;%r&quot; %s %b" />
          </Host>
    

    结果如下,这样我们就可以通过加/路径名来跳转到别的项目中:

    image-20220112142553665

    三、手写简单Tomcat

    项目地址

    自定义Tomcat是可以作为一个服务器软件提供服务的,就是说我们可以使用浏览器客户端发送http请求,My_Tomcat可以接收请求并处理,处理成功后可以返回浏览器客户端。主要是简单的理解tomcat的主要工作流程。

    我们分步来实现,首先我们请求http://localhost:8080,返回一个固定的Hello World字符串,然后封装Request和Response,返回html静态文件即静态资源,最后我们使My_Tomcat可以请求动态资源。

    我写的时候每一步都用了BIO和NIO两种方式,用的时候注释掉一种即可。这里只展示主要代码,具体工具类代码请自行github查看。

    3.1 返回固定字符串

    第一步我们先让我们的项目能监听8080端口,并返回信息:

    我们新建项目然后创建Boostrap类,类信息如下,主要思路是通过监听8080端口然后返回信息,在返回信息时需要将HTTP请求体进行拼接我写在了HttpProtocolUtil类中:

    image-20220115122657579

    public class BootStrap {
        //暂时写死,可以修改到xml文件中进行读取
        private int port = 8080;
        public int getPort() {
            return port;
        }
        public void setPort(int port) {
            this.port = port;
        }
        /**
         * MYTomcat程序init和启动
         *
         * @author Loserfromlazy
         * @date 2022/1/12 15:35
         */
        public void start() throws IOException {
            //===================BIO
            /*ServerSocket serverSocket = new ServerSocket(port);
            System.out.println("My_Tomcat Listening on port 8080");
            while (true){
                Socket socket = serverSocket.accept();
                OutputStream outputStream = socket.getOutputStream();
                String data = "Hello,World!";
                String responseText = HttpProtocolUtil.getHttpHeader200(data.getBytes(StandardCharsets.UTF_8).length) + data;
                outputStream.write(responseText.getBytes(StandardCharsets.UTF_8));
                socket.close();
            }*/
            //==================NIO
            ServerSocketChannel serverSocketChannel= ServerSocketChannel.open();
            serverSocketChannel.bind(new InetSocketAddress(8080));
            serverSocketChannel.configureBlocking(false);
            Selector selector = Selector.open();
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            while (true){
                if (selector.select(2000)==0){
                    continue;
                }
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()){
                    SelectionKey key = iterator.next();
                    if (key.isAcceptable()){
                        SocketChannel socketChannel = serverSocketChannel.accept();
                        socketChannel.configureBlocking(false);
                        socketChannel.register(selector,SelectionKey.OP_READ);
                        String data = "Hello,World!";
                        String responseText = HttpProtocolUtil.getHttpHeader200(data.getBytes(StandardCharsets.UTF_8).length) + data;
                        ByteBuffer buffer = ByteBuffer.wrap(responseText.getBytes(StandardCharsets.UTF_8));
                        socketChannel.write(buffer);
                        iterator.remove();
                    }
                }
            }
        }
        /**
         * MYTomcat程序入口
         *
         * @author Loserfromlazy
         * @date 2022/1/12 15:24
         */
        public static void main(String[] args) {
            BootStrap bootStrap = new BootStrap();
            try {
                bootStrap.start();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    

    3.2 返回静态资源

    第二步,我们可以封装请求的Request和Response对象,然后返回静态资源

    这里我将上一步的Bootstrap改为Bootstrap1,然后新建一个Bootstrap,用于保存每一步的代码,后面也会这么做。

    在进行封装前,我们可以查看HTTP请求头信息,可以使用以下方法查看或者F12:

    public void start() throws IOException {
        //===================BIO
        /*ServerSocket serverSocket = new ServerSocket(port);
        System.out.println("My_Tomcat Listening on port 8080");
        while (true) {
            Socket socket = serverSocket.accept();
            InputStream inputStream = socket.getInputStream();
            int count = 0;
            while (count == 0) {
                count = inputStream.available();
            }
            byte [] bytes = new byte[count];
            inputStream.read(bytes);
            System.out.println(new String(bytes));
            socket.close();
        }*/
        //==================NIO
            ServerSocketChannel serverSocketChannel= ServerSocketChannel.open();
            serverSocketChannel.bind(new InetSocketAddress(8080));
            serverSocketChannel.configureBlocking(false);
            Selector selector = Selector.open();
            System.out.println("My_Tomcat Listening on port 8080");
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            while (true){
                if (selector.select(2000)==0){
                    continue;
                }
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()){
                    SelectionKey key = iterator.next();
                    if (key.isAcceptable()){
                        SocketChannel socketChannel = serverSocketChannel.accept();
                        socketChannel.configureBlocking(false);
                        socketChannel.register(selector,SelectionKey.OP_READ);
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        int count = 0;
                        while (count==0){
                            count = socketChannel.read(buffer);
                        }
                        buffer.flip();
                        System.out.println(new String(buffer.array()));
                        iterator.remove();
                    }
                }
            }
    }
    

    输出信息:

    GET / HTTP/1.1
    Host: localhost:8080
    Connection: keep-alive
    Cache-Control: max-age=0
    sec-ch-ua: " Not;A Brand";v="99", "Google Chrome";v="97", "Chromium";v="97"
    sec-ch-ua-mobile: ?0
    sec-ch-ua-platform: "Windows"
    Upgrade-Insecure-Requests: 1
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
    Sec-Fetch-Site: none
    Sec-Fetch-Mode: navigate
    Sec-Fetch-User: ?1
    Sec-Fetch-Dest: document
    Accept-Encoding: gzip, deflate, br
    Accept-Language: zh-CN,zh;q=0.9
    Cookie: Idea-3b85319b=6df6ddd1-3436-43bd-a172-56742a301d72
    

    拿到了请求头,我们就可以封装Request,我们这里就封装请求方法和URL。

    Request对象

    封装Request对象的思路就是将HTTP头中的信息保存在Requset对象中

    public class Request {
    
        private String method;//例如GET POST
        private String url;//例如 /index.html
    
        public String getMethod() {
            return method;
        }
    
        public void setMethod(String method) {
            this.method = method;
        }
    
        public String getUrl() {
            return url;
        }
    
        public void setUrl(String url) {
            this.url = url;
        }
    
        private InputStream inputStream;//根据传入的inputStream解析请求头
        private SocketChannel socketChannel;////根据传入的SocketChannel解析请求头(NIO)
    
        public Request() {
        }
    
        public Request(SocketChannel socketChannel) throws IOException {
            this.socketChannel = socketChannel;
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            int count = 0;
            while (count==0){
                count = socketChannel.read(buffer);
            }
            buffer.flip();
            String httpHeaderStr = new String(buffer.array());//http头协议
            String[] split = httpHeaderStr.split("\\n");
            String firstLine = split[0];//协议第一行
            String[] firstLineItem = firstLine.split(" ");
            this.method =firstLineItem[0];
            this.url = firstLineItem[1];
            System.out.println("method==>"+this.method+";url==>"+this.url);
        }
    
        public Request(InputStream inputStream) throws IOException {
            this.inputStream = inputStream;
    
            int count = 0;
            while (count == 0) {
                count = inputStream.available();
            }
            byte [] bytes = new byte[count];
            inputStream.read(bytes);
            String httpHeaderStr = new String(bytes);//http头协议
            String[] split = httpHeaderStr.split("\\n");
            String firstLine = split[0];//协议第一行
            String[] firstLineItem = firstLine.split(" ");
            this.method =firstLineItem[0];
            this.url = firstLineItem[1];
            System.out.println("method==>"+this.method+";url==>"+this.url);
    
        }
    }
    

    Response对象

    封装Response对象的思路就是通过构造的输出流输出我们需要输出的内容。

    public class Response {
    
        private OutputStream outputStream;
    
        private SocketChannel socketChannel;
    
        public Response(SocketChannel socketChannel) {
            this.socketChannel = socketChannel;
        }
    
        public Response() {
        }
    
        public Response(OutputStream outputStream) {
            this.outputStream = outputStream;
        }
    
        /**
         * 通过url获取静态资源的绝对路径,根据绝对路径获取静态文件,然后使用输出流输出
         *
         * @author Yuhaoran
         * @date 2022/1/17 13:23
         */
        public void outPutHtml(String url) throws IOException {
            String absolutePath = StaticResourceUtil.getAbsolutePath(url);
            File file = new File(absolutePath);
            if (file.exists() && file.isFile()){
                FileInputStream inputStream = new FileInputStream(file);
                //输出文件内容
                int count = 0;
                while (count == 0) {
                    count = inputStream.available();
                }
                int written = 0;
                int byteSize = 1024;
                byte[] bytes = new byte[byteSize];
                outputStream.write(HttpProtocolUtil.getHttpHeader200(count).getBytes(StandardCharsets.UTF_8));
                while (written < count) {
                    if (written + byteSize > count) {
                        bytes = new byte[count - written];
                    }
                    inputStream.read(bytes);
                    outputStream.write(bytes);
                    outputStream.flush();
                    written += byteSize;
                }
            }else {
                //输出404
                outputStream.write(HttpProtocolUtil.getHttpHeader404().getBytes(StandardCharsets.UTF_8));
            }
        }
    
        public void outPutHtmlByChannel(String url) throws IOException {
            String absolutePath = StaticResourceUtil.getAbsolutePath(url);
            File file = new File(absolutePath);
            if (file.exists() && file.isFile()){
                //输出文件内容
                FileInputStream inputStream = new FileInputStream(file);
                FileChannel fileChannel = inputStream.getChannel();
                ByteBuffer buffer = ByteBuffer.allocate((int) file.length());
                fileChannel.read(buffer);
                buffer.flip();
                byte[] bytes = HttpProtocolUtil.getHttpHeader200(buffer.capacity()).getBytes(StandardCharsets.UTF_8);
                ByteBuffer responseBuffer = ByteBuffer.wrap(bytes);
                socketChannel.write(responseBuffer);
                socketChannel.write(buffer);
            }else {
                //输出404
                byte[] bytes = HttpProtocolUtil.getHttpHeader404().getBytes(StandardCharsets.UTF_8);
                ByteBuffer buffer = ByteBuffer.wrap(bytes);
                socketChannel.write(buffer);
            }
        }
    
        public void outPut(String contect) throws IOException {
            outputStream.write(contect.getBytes(StandardCharsets.UTF_8));
        }
    
        public void outPutByChannel(String contect) throws IOException {
            ByteBuffer buffer = ByteBuffer.wrap(contect.getBytes(StandardCharsets.UTF_8));
            socketChannel.write(buffer);
        }
    }
    

    封装完成后我们就可以在主方法中使用:

    public class BootStrap2 {
        //暂时写死,可以修改到xml文件中进行读取
        private int port = 8080;
        public int getPort() {
            return port;
        }
        public void setPort(int port) {
            this.port = port;
        }
        /**
         * MYTomcat程序init和启动
         *
         * @author Loserfromlazy
         * @date 2022/1/15 12:38
         */
        public void start() throws IOException {
            //===================BIO
    //        ServerSocket serverSocket = new ServerSocket(port);
    //        System.out.println("My_Tomcat Listening on port 8080");
    //        while (true) {
    //            Socket socket = serverSocket.accept();
    //            InputStream inputStream = socket.getInputStream();
    //            OutputStream outputStream = socket.getOutputStream();
    //            Request request = new Request(inputStream);
    //            Response response = new Response(outputStream);
    //            response.outPutHtml(request.getUrl());
    //            socket.close();
    //        }
            //==================NIO
            ServerSocketChannel serverSocketChannel= ServerSocketChannel.open();
            serverSocketChannel.bind(new InetSocketAddress(8080));
            serverSocketChannel.configureBlocking(false);
            Selector selector = Selector.open();
            System.out.println("My_Tomcat Listening on port 8080");
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            while (true){
                if (selector.select(2000)==0){
                    continue;
                }
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()){
                    SelectionKey key = iterator.next();
                    if (key.isAcceptable()){
                        SocketChannel socketChannel = serverSocketChannel.accept();
                        socketChannel.configureBlocking(false);
                        socketChannel.register(selector,SelectionKey.OP_READ);
                        System.out.println(socketChannel.getRemoteAddress()+"发来请求");
                        Request request = new Request(socketChannel);
                        Response response = new Response(socketChannel);
                        response.outPutHtmlByChannel(request.getUrl());
                        socketChannel.close();
                        iterator.remove();
                    }
                }
            }
        }
        /**
         * MYTomcat程序入口
         *
         * @author Loserfromlazy
         * @date 2022/1/12 15:24
         */
        public static void main(String[] args) {
            BootStrap2 bootStrap = new BootStrap2();
            try {
                bootStrap.start();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    

    3.3 返回动态资源

    关于Xpath表达式可以在菜鸟上看一下语法很简单https://www.runoob.com/xpath/xpath-syntax.html

    动态资源就需要我们进行对Servlet的封装,代码如下,首先需要定义一下Servlet规范,如下面的Servlet接口,然后封装一个具体的HttpServlet,最后我们自己编写Servlet继承HttpServlet写自己的业务逻辑:

    public interface Servlet {
        void init() throws Exception;
    
        void destory() throws Exception;
    
        void service(Request request,Response response) throws Exception;
    }
    
    public abstract class HttpServlet implements Servlet{
        public abstract void doGet(Request request,Response response);
    
        public abstract void doPost(Request request,Response response);
    
        @Override
        public void service(Request request,Response response) throws Exception {
            if ("GET".equals(request.getMethod())){
                doGet(request,response);
            }else{
                doPost(request,response);
            }
        }
    }
    
    public class MyServlet extends HttpServlet{
        @Override
        public void doGet(Request request, Response response) {
            String content = "<h1>Hello My_Tomcat GET Method!</h1>";
            try {
                //BIO
                //response.outPut(HttpProtocolUtil.getHttpHeader200(content.getBytes(StandardCharsets.UTF_8).length)+content);
                //NIO
                response.outPutByChannel(HttpProtocolUtil.getHttpHeader200(content.getBytes(StandardCharsets.UTF_8).length)+content);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        @Override
        public void doPost(Request request, Response response) {
            String content = "<h1>Hello My_Tomcat POST Method!</h1>";
            try {
                //BIO
                //response.outPut(HttpProtocolUtil.getHttpHeader200(content.getBytes(StandardCharsets.UTF_8).length)+content);
                //NIO
                response.outPutByChannel(HttpProtocolUtil.getHttpHeader200(content.getBytes(StandardCharsets.UTF_8).length)+content);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        @Override
        public void init() throws Exception {
    
        }
        @Override
        public void destory() throws Exception {
    
        }
    }
    

    有了Servlet我们还需要web.xml,这里我们参考之前的Servlet即可:

    <?xml version="1.0" encoding="UTF-8" ?>
    <web-app>
    
        <servlet>
            <servlet-name>myServlet</servlet-name>
            <servlet-class>server.MyServlet</servlet-class>
        </servlet>
        <servlet-mapping>
            <servlet-name>myServlet</servlet-name>
            <url-pattern>/my</url-pattern>
        </servlet-mapping>
    
    </web-app>
    

    最后在主方法中使用即可:

    public class BootStrap {
        //暂时写死,可以修改到xml文件中进行读取
        private int port = 8080;
        public int getPort() {
            return port;
        }
        public void setPort(int port) {
            this.port = port;
        }
        /**
         * MYTomcat程序init和启动
         *
         * @author Loserfromlazy
         * @date 2022/1/15 12:38
         */
        public void start() throws Exception {
            //加载web.xml
            loadServlet();
            //===================BIO
            /*ServerSocket serverSocket = new ServerSocket(port);
            System.out.println("My_Tomcat Listening on port 8080");
            while (true) {
                Socket socket = serverSocket.accept();
                InputStream inputStream = socket.getInputStream();
                OutputStream outputStream = socket.getOutputStream();
                Request request = new Request(inputStream);
                Response response = new Response(outputStream);
                if (servletMap.get(request.getUrl())==null){
                    response.outPutHtml(request.getUrl());
                }else {
                    HttpServlet httpServlet = servletMap.get(request.getUrl());
                    httpServlet.service(request,response);
                }
    
                socket.close();
            }*/
            //==================NIO
            ServerSocketChannel serverSocketChannel= ServerSocketChannel.open();
            serverSocketChannel.bind(new InetSocketAddress(8080));
            serverSocketChannel.configureBlocking(false);
            Selector selector = Selector.open();
            System.out.println("My_Tomcat Listening on port 8080");
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            while (true){
                if (selector.select(2000)==0){
                    continue;
                }
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()){
                    SelectionKey key = iterator.next();
                    if (key.isAcceptable()){
                        SocketChannel socketChannel = serverSocketChannel.accept();
                        socketChannel.configureBlocking(false);
                        socketChannel.register(selector,SelectionKey.OP_READ);
                        System.out.println(socketChannel.getRemoteAddress()+"发来请求");
                        Request request = new Request(socketChannel);
                        Response response = new Response(socketChannel);
                        if (servletMap.get(request.getUrl())==null){
                            response.outPutHtmlByChannel(request.getUrl());
                        }else {
                            HttpServlet httpServlet = servletMap.get(request.getUrl());
                            httpServlet.service(request,response);
                        }
                        socketChannel.close();
                        iterator.remove();
                    }
                }
            }
        }
    
        private Map<String,HttpServlet> servletMap = new HashMap<>();
        /**
         * 加载解析web.xml
         * @author Loserfromlazy
         * @date 2022/1/18 13:18
         */
        private void loadServlet() {
            InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream("web.xml");
            SAXReader saxReader = new SAXReader();
            try {
                Document document = saxReader.read(resourceAsStream);
                Element rootElement = document.getRootElement();
    
                List<Element> selectNodes = rootElement.selectNodes("//servlet");
                for (Element element : selectNodes) {
                    //获取servlet-name
                    Node servletNameNode = element.selectSingleNode("servlet-name");
                    String servletName = servletNameNode.getStringValue();
                    //获取servlet-class
                    Node servletClassNode = element.selectSingleNode("servlet-class");
                    String servletClass = servletClassNode.getStringValue();
    
                    //根据servlet-name找到servlet-class
                    Node servletMapping = rootElement.selectSingleNode("/web-app/servlet-mapping[servlet-name = '" + servletName + "']");
                    Node urlPatternNode = servletMapping.selectSingleNode("url-pattern");
                    String urlPattern = urlPatternNode.getStringValue();
    
                    //存入map
                    servletMap.put(urlPattern, (HttpServlet) Class.forName(servletClass).newInstance());
                }
            }catch (Exception e){
                e.printStackTrace();
            }
    
        }
        /**
         * MYTomcat程序入口
         *
         * @author Loserfromlazy
         * @date 2022/1/12 15:24
         */
        public static void main(String[] args) {
            BootStrap bootStrap = new BootStrap();
            try {
                bootStrap.start();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    

    3.4 使用线程池的方式改造

    我们只需要创建一个线程池和一个线程类来包装我们的执行逻辑,然后将线程提交到线程池即可。

    这里以NIO的线程类为例:

    public class RequestProcess implements Runnable{
        private SocketChannel socketChannel;
        private Map<String,HttpServlet> servletMap = new HashMap<>();
        private Selector selector;
    
        public RequestProcess(SocketChannel socketChannel, Map<String, HttpServlet> servletMap, Selector selector) {
            this.socketChannel = socketChannel;
            this.servletMap = servletMap;
            this.selector = selector;
        }
    
        @Override
        public void run() {
            try {
                socketChannel.configureBlocking(false);
                socketChannel.register(selector, SelectionKey.OP_READ);
                System.out.println(socketChannel.getRemoteAddress()+"发来请求");
                Request request = new Request(socketChannel);
                Response response = new Response(socketChannel);
                if (servletMap.get(request.getUrl())==null){
                    response.outPutHtmlByChannel(request.getUrl());
                }else {
                    HttpServlet httpServlet = servletMap.get(request.getUrl());
                    httpServlet.service(request,response);
                }
                socketChannel.close();
            }catch (Exception e){
                e.printStackTrace();
            }
    
        }
    }
    

    四、Tomcat源码

    4.1 源码构建

    官方文档指定使用ant进行构建,但是使用maven也是能构建的。因为本质上就是一个Java项目。

    我们首先在官网上下载tomcat的源码,我的源码版本是8.5.75,下载apache-tomcat-8.5.75-src.zip即可下载完成后将zip包解压。解压后创建一个source文件夹,将conf和webapps文件夹拷贝到source文件夹下:

    image-20220216134840537

    然后在解压后的文件夹下创建pom.xml文件:

    image-20220216134921017

    pom文件内容如下:

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
    		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    		xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
    		http://maven.apache.org/xsd/maven-4.0.0.xsd">
    	<modelVersion>4.0.0</modelVersion>
    	 <groupId>org.apache.tomcat</groupId>
    	 <artifactId>apache-tomcat-8.5.75-src</artifactId>
    	 <name>Tomcat8.5</name>
    	 <version>8.5</version>
      <properties>
        <!-- 设置项目编码 -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
      </properties>
    
    	 <build>
    		 <!--指定源目录-->
    		 <finalName>Tomcat8.5</finalName>
    		 <sourceDirectory>java</sourceDirectory>
    		 <resources>
    			 <resource>
    				<directory>java</directory>
    			 </resource>
    		 </resources>
    		 <plugins>
    		 <!--引入编译插件-->
    			 <plugin>
    				 <groupId>org.apache.maven.plugins</groupId>
    					<artifactId>maven-compiler-plugin</artifactId>
    					<version>3.1</version>
    				 <configuration>
    					<encoding>UTF-8</encoding>
    					<source>8</source>
    					<target>8</target>
    				 </configuration>
    			 </plugin>
    		 </plugins>
    	 </build>
    
    	<!--tomcat 依赖的基础包-->
    	 <dependencies>
    		 <dependency>
    			 <groupId>org.easymock</groupId>
    			 <artifactId>easymock</artifactId>
    			 <version>3.4</version>
    		 </dependency>
    		 <dependency>
    			 <groupId>ant</groupId>
    			 <artifactId>ant</artifactId>
    			 <version>1.7.0</version>
    		 </dependency>
    		 <dependency>
    			 <groupId>wsdl4j</groupId>
    			 <artifactId>wsdl4j</artifactId>
    			 <version>1.6.2</version>
    		 </dependency>
    		 <dependency>
    			 <groupId>javax.xml</groupId>
    			 <artifactId>jaxrpc</artifactId>
    			 <version>1.1</version>
    		 </dependency>
    		 <dependency>
    			 <groupId>org.eclipse.jdt.core.compiler</groupId>
    			 <artifactId>ecj</artifactId>
    			 <version>4.5.1</version>
    		 </dependency>
    		 <dependency>
    			 <groupId>javax.xml.soap</groupId>
    			 <artifactId>javax.xml.soap-api</artifactId>
    			 <version>1.4.0</version>
    		 </dependency>
    	 </dependencies>
    </project>
    

    然后打开idea导入这个工程,idea会自动识别为maven工程。导入工程后启动BootStrap类,启动前加上启动参数:

    添加启动参数的方法如下:

    打开idea的启动类的配置编辑

    image-20220216135214702

    然后再下图的VM options中编写,如果没有这个文本框,就需要在下图右上角的这个Modify options中添加VM options然后在编写。

    image-20220216135401645

    VM参数如下:参数主要是用来指定tomcat的日志路径和产品目录路径。

    Bootstrap启动的时候使用了两个系统变量catalina.home和catalina.base,从官网和源码中的注释可以知道这两者的区别主要是:catalina.home是Tomcat产品的安装目录,而catalina.base是tomcat启动过程中需要读取的各种配置及日志的根目录。默认情况下catalina.base是和catalina.home是相同的。

    -Dcatalina.home=D:\tomcat\apache-tomcat-8.5.75-src\source
    -Dcatalina.base=D:\tomcat\apache-tomcat-8.5.75-src\source
    -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
    -Djava.util.logging.config.file=D:\tomcat\apache-tomcat-8.5.75-src\source\conf\logging.properties
    

    然后启动BootStrap类,打开localhost:8080,正常弹出tomcat首页即可。

    image-20220216143150933

    4.2 源码构建的问题

    以下是构件中编译遇到的各种问题:

    4.2.1 JSP报错

    如果打开8080弹出JSP报错(PS:我的是JSP报错加乱码),如下图:

    image-20220216142507101

    则需要手动添加JSP的编译器,在ContextConfig中的configureStart()方法中增加JSP编译器,如下图:

    image-20220216142645786

    重启后就可以解决JSP报错的问题。

    4.2.2 tomcat编译乱码问题

    如果也想我一样有日志乱码问题,首先可以试试网上的各种改UTF-8编码的操作,这里就不做赘述。如果不行就需要改源码解决,过程如下:

    但是我改了7、8个地方改成UTF-8都不行,所以我对日志进行了Debug,过程如下:

    首先看控制台的乱码如下图:

    image-20220216143320555

    然后在最开始出现乱码的地方,通过全局搜索找到这个VersionLoggerListener.log

    image-20220216143427884

    然后打上断点开始进行debug调试,一路跟代码,发现在getString方法中的bundle.getString(key)这一句执行完后,乱码就会出现。如下图:

    image-20220216143916714

    点进bundle.getString(key),发现是JDK中ResourceBundle的方法.这个类是用来做国际化的。

    最后在stackoverflow上找到了,如下图,Java9以上属性文件默认是UTF-8,但是Java8及以下默认是使用ISO-8859-1的,我的jdk版本就是8所以日志会有乱码问题。

    image-20220216145522949

    解决方法是:修改方法,手动进行转码(改的时候注意有很多getString,别改错了):

    image-20220216151134899

    重启后发现第一个乱码问题已解决。

    下一个乱码问题也是如此,最后跟到下面的地方,跟上面同理,手动进行转码

    image-20220216151251087

    乱码问题解决。

    PS:我改完这两个地方就没有乱码了

    4.3 源码流程分析

    建议自己跟一编源码,光看别人的源码分析是看不懂的,别人的源码分析只能提供分析思路

    4.3.1 tomcat启动流程

    首先我们在startip.bat或startup.sh中可以找到这个脚本会拉起catalina.bat或catalina.sh的脚本,如下图

    image-20220218152901720

    然后再catalina.bat或catalina.sh中会发现这个脚本会启动BootStrap类,也就是main方法,然后启动时会带一个start的参数,如下图:

    image-20220218153057792

    然后我们进入BootStrap的main,具体的流程图(流程图是自己边Debug边画的只有大概的流程)在下面。

    进入main方法后会先进入init方法

    image-20220218153804154

    这里会创建一个Catalina的实例

    image-20220218153849250

    然后回到main方法,继续往下走,我们会发现,会执行load()方法,然后执行start()方法。load方法主要是tomcat中的各种组件初始化,start方法是各种组件依次启动。

    image-20220218154210449

    我们先跟进load()方法,这里会调用Catalina的load方法:

    image-20220218154748322

    我们进入Catalina的load方法中,这个方法中主要关注的代码如下,它会拉取Server的init方法:

    getServer().init();
    

    我们点进init方法,会进入到LifeCycle的init方法,LifeCycle这个接口主要就是定义了整体生命周期,我们进入LifecycleBase的init方法中(默认进这个类,可以自行debug确认)

    image-20220218155155040

    进入后会走到initInternal()方法中,这个方法是抽象方法,debug跟进会进入到对应的实现方法中,因为我们调用的是Server.init()所以会进入StandardServer的initInternal()中。

    image-20220218155644956

    我们进入该方法,这个方法中主要关注下面的代码:

    // Initialize our defined Services
    for (Service service : services) {
        service.init();
    }
    

    service.init()会拉起Service的init方法,我们点进init(),会发现会回到LifeCycle的init方法,然后跟上面一样会进入到StandardService的initInternal()方法中,如下图

    image-20220218155407675

    最后会按照流程图一步一步走下去,会把Tomcat的组件逐级init。然后会回到BootStrap的start()方法中,start()方法与init()方法流程差不多,这里就不再赘述。

    Tomcat启动的整体流程图:

    tomcat启动流程

    4.3.2 Tomcat请求处理流程

    部署项目到源码

    如果需要查看Tomcat的请求流程,我们就需要有效的能进入我们Tomcat源代码工程的URL。所以我们可以新建一个JavaWeb工程,然后将这个JavaWeb工程部署到我们的Tomcat源码项目中。

    首先我们先新建一个webdemo工程,然后创建一个Servlet,如下:

    image-20220221094659286

    @WebServlet(name = "webdemo",value = "/web/webdemo")
    public class DemoServlet extends HttpServlet {
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            System.out.println("进入了GET方法");
        }
    
        @Override
        protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            System.out.println("进入了POST方法");
        }
    }
    

    然后我们将webdemo工程部署到tomcat中启动(注意是Tomcat不是我们的Tomcat源码工程),启动后会生成文件夹,如下图:

    image-20220221094826667

    然后我们将Tomcat生成的这个webdemo文件夹拷贝到我们的Tomcat源码工程中:

    image-20220221095019428

    然后就可以在源码中打断点查看Tomcat的请求流程了。

    以上面的工程为例,请求地址是http://localhost:8080/webdemo/web/webdemo,Tomcat处理的大体流程如下图:

    Tomcat请求流程20220221

    Tomcat请求流程

    首先,我们需要找到断点的入口,我们在NioEndPoint中的Poller类的run方法中打上断点。

    为什么在NioEndPoint中的Poller类的run方法中打上断点?

    Tomcat是从Connector接收请求的,在上面的启动流程源码分析中,如果debug跟代码的话,最后会发现在init流程中会执行socket的bind()方法在start流程中会执行socket的accept()方法。这两个方法都是在NioEndpoint中的,如下图:

    image-20220221150926632

    而在NioEndpoint的startInternal()方法中,在执行accept之前,会创建轮询器,

    image-20220221160351307

    我们进入到轮询器类中,发现其继承了Runnable,所以我们找到run方法,会发现,此方法会监听socket通道中的事件,因此这个轮询线程就是为了轮询监听socket通道事件的。因此我们在监听到事件之后的处理方法即processKey(sk, socketWrapper);上打断点开始debug。如下图:

    image-20220221160819369

    下面我们debug运行BootStrap然后浏览器发送一个请求,开始查看请求处理流程:

    这里只是演示请求处理流程,建议自己跟一遍源码。

    首先我们看到请求确实来到了processKey方法,如下图:

    image-20220222104226663

    我们跟进这个方法,在processKey中有一个processSocket方法这个方法会继续处理我们的socket和key的信息,我们跟进这个processSocket方法。

    image-20220222105709562

    进入之后我们会发现进入到了NioEndPoint的抽象父类的方法中,然后我们会发现在这里会用线程池执行一个线程,因此我们需要关注这个线程的run方法。

    image-20220222105953264

    我们进入到SocketProcessorBase类中的run方法,代码如下:

    @Override
    public final void run() {
        synchronized (socketWrapper) {
            // It is possible that processing may be triggered for read and
            // write at the same time. The sync above makes sure that processing
            // does not occur in parallel. The test below ensures that if the
            // first event to be processed results in the socket being closed,
            // the subsequent events are not processed.
            if (socketWrapper.isClosed()) {
                return;
            }
            doRun();
        }
    }
    

    我们在doRun()打上断点,让程序运行到这里。(因为是线程池执行这个线程,所以可以跳过来)

    然后我们继续跟进doRun()方法,在这个方法中,我们会发现他会执行handler的process方法。

    image-20220222110701615

    进入process方法之后,在这个方法中会得到一个Processor对象,然后调用Processor对象的process方法,如下图:

    image-20220222111046555

    继续跟,会进入到AbstractProcessorLight中(这个类是一个轻量级的抽象处理器实现,旨在作为从轻量级升级处理器到HTTP/AJP处理器的所有处理器实现的基础。)也就是说在这里会找到具体的处理器Processor去处理。

    我们在这个方法中能找到service方法如下,这个方法会继续处理我们我们第一步传过来的socketWrapper对象。

    state = service(socketWrapper);
    

    我们跟进这个方法,果然进入到了具体的处理器中,也就是会进入到具体的Http1.1协议处理器的service方法中:

    image-20220222111732184

    进入到service方法后,这个方法会将我们传过来的request和response对象转换成标准的HttpServlet对象,然后调用容器,getContainer方法会拿到service关联的Engine。

    这里会通过获取容器的管道然后调用管道中的第一个Value的invoke方法,如下图:

    image-20220222125009002

    我们跟进invoke方法:(此时会进入StandardEngineValve类) 此方法中会调用host,从host中管道取第一个Value执行invoke方法,去处理这个http请求。管道Pipeline其实就是一些按顺序执行Value#invoke()方法的对应的类的封装

    Pipeline是一个处理管道的标准实现,它将调用一系列已配置为按顺序调用的阀门。此实现可用于任何类型的容器。Pipeline里面是Value接口的各种实现类。

    getFirst方法:

    在调用pipeline的getFirst()方法时,如果first上有Value就返回该Value,否则就返回basic上的Value。

    protected Valve basic = null;
    protected Container container = null;
    protected Valve first = null;
    @Override
    public Valve getFirst() {
     if (first != null) {
         return first;
     }
    
     return basic;
    }
    

    basic和first都是Value,Value是一个接口,下面有很多实现类比如StandardEngineValve

    basic其实就是它本身的标准Value实现类:

    image-20220222143447158

    @Override
    public final void invoke(Request request, Response response)
        throws IOException, ServletException {
    
        // Select the Host to be used for this Request
        Host host = request.getHost();
        if (host == null) {
            // HTTP 0.9 or HTTP 1.0 request without a host when no default host
            // is defined.
            // Don't overwrite an existing error
            if (!response.isError()) {
                response.sendError(404);
            }
            return;
        }
        if (request.isAsyncSupported()) {
            request.setAsyncSupported(host.getPipeline().isAsyncSupported());
        }
    
        // Ask this Host to process this request
        host.getPipeline().getFirst().invoke(request, response);
    }
    

    PS:在获取host.getPipeline().getFirst().invoke(request, response);

    host的pipeline的顺序是AbstractAccessLogValve->ErrorReportValve ->StandardHostValve。host的pipeline的basic时StandardHostValve,如图:

    image-20220222142458756

    我们继续跟进host的invoke,接下来会走到AbstractAccessLogValve的invoke方法然后调用getNext().invoke(request, response);

    我们继续跟invoke会走到ErrorReportValve的invoke方法,此方法中继续调用getNext().invoke(request, response);

    我们继续跟,此时会进入StandardHostValve类,然后会从context中拿管道调用invoke。

    if (!response.isErrorReportRequired()) {
        context.getPipeline().getFirst().invoke(request, response);
    }
    

    我们继续跟,进入到AuthenticatorBase#invoke()方法然后调用getNext().invoke(request, response);

    我们继续跟invoke,会进入到StandardContextValve#invoke()方法

    这里会从wrapper中获取管道然后执行

    wrapper.getPipeline().getFirst().invoke(request, response);
    

    跟进invoke会进入StandardWrapperValve#invoke()方法

    在这里会创建一个过滤器链,代码如下:

    关于过滤器链的注解:filterchain的实现,用于管理针对特定请求的一组过滤器的执行。当一组定义的过滤器全部执行完毕后,对doFilter()的下一次调用将执行servlet的service()方法本身。

    ApplicationFilterChain filterChain =
            ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
    

    上面代码往下几行,创建完之后会执行这个过滤器链:

    filterChain.doFilter
        (request.getRequest(), response.getResponse());
    

    我们跟进doFilter方法:一直往下跟最后会执行service方法

    servlet.service(request, response);
    

    从connector调用容器到servlet.service的过程:

    image-20220222132651362

    这个方法就是我们的Servlet的的service,然后就会取执行我们自己的Servlet方法了。此时看servlet,就是上面我们部署的webdome项目中的servlet,也就是说tomcat一步一步的找到了我们的Servlet并执行它。

    image-20220222132014991

    整体流程:

    tomcat处理请求流程20220222

    五、Tomcat对HTTPS的支持

    https需要证书,我们可以用java自带的工具生成自己的测试用的证书。

    keytool -genkey -alias loserfromlazy -keyalg RSA -keystore miyao.keystore
    
    • -alias 别名
    • -keyalg 加密方式
    • -keystore 密钥库名称
    • ...其它具体参数请自行查阅

    image-20220222170641664

    这里注意名字与姓氏需要与域名保持一致,之后就会在当前目录下生成miyao.keystore文件

    然后在tomcat的server.xml中配置

    <Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol"
                   maxThreads="150" SSLEnabled="true">
            <SSLHostConfig>
                <Certificate certificateKeystoreFile="conf/miyao.keystore"
    						 certificateKeystorePassword="123456"
                             type="RSA" />
            </SSLHostConfig>
        </Connector>
    

    然后浏览器访问即可(注意需要忽略浏览器的安全警告):

    image-20220222172111497

    六、Tomcat调优

    Tomcat本身也是一个Java项目,所以调优分为JVM调优和Tomcat自身调优

    6.1 JVM调优

    jvm调优只需要我们将jvm参数放到Catalina.bat或Catalina.sh中即可。

    jvm优化主要是内存和垃圾回收策略。

    简单了解Java内存模型:

    Java内存模型20220223

    • 线程共享的数据区:主要是Java堆和方法区Java 堆的唯一目的就是存放对象实例,几乎所有的对象实例(和数组)都在这里分配内存
    • 方法区与Java堆一样,也是线程共享的并且不需要连续的内存,其用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
    • 虚拟机栈描述的是Java方法执行的内存模型,是线程私有的。每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息
    • 程序计数器是线程私有的一块较小的内存空间,其可以看做是当前线程所执行的字节码的行号指示器
    • 本地方法栈与Java虚拟机栈非常相似,也是线程私有的,区别是虚拟机栈为虚拟机执行 Java 方法服务,而本地方法栈为虚拟机执行 Native 方法服务

    内存优化主要是对堆进行优化,内存相关参数:

    参数 参数作用 备注
    -server 以服务端运行
    -Xms 最小堆内存 与-Xmx设置相同
    -Xmx 最大堆内存 可用内存的80%
    -XX:MetaspaceSize 元空间初始值
    -XX:MaxMetaspaceSize 元空间最大内存 默认无限
    -XX:NewRatio 年轻代和老年代大小比值 默认为2
    -XX:SurvivorRatio Eden区与Survivor区大小的壁纸 默认为8

    举例JAVA_OPTS=-server -Xms1024m -Xmx1024m -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m

    tomcat调优可以在bin目录下建立setenv.bat,然后在里面配置JAVA_OPTS即可,注意等号和-server之间不能有空格否则不生效,一定不要马虎。

    image-20220223152521137

    配置完重启tomcat即可。验证方式有很多,比如可以通过jmap命令或者tomcat内置状态检查等等。http://localhost:8080/manager/status

    注意使用上面的网址需要在/conf/tomcat-users.xml中配置用户名密码

    <role rolename="admin-gui"/>
    <role rolename="manager-gui"/>
    <role rolename="manager-jmx"/>
    <role rolename="manager-script"/>
    <role rolename="manager-status"/>
    <user username="admin" password="admin" roles="admin-gui,manager-gui,manager-jmx,manager-script,manager-status"/>
    

    简单了解一下垃圾收集器:

    • 串行收集器 Serial Collector

      单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。

    • 并行收集器 Parallel Collector

      Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。

      Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间 +垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

    • 并发收集器 Concurrent Collector

    • CMS收集器 Concurrent Mark Sweep Collector

      CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。

    • G1收集器 Garbage-First Garbage Collector

      适用于大容量的多核服务器,在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合

    GC策略相关参数:

    参数 描述
    -XX:+UseSerialGC 启动创行收集器
    -XX:+UseParallelGC 使用并行收集器,使用此配置,那么-XX:+UseParallelOldGC默认启动
    -XX:+UseParNewGC 年轻代采用并行收集器
    -XX:ParallelGCThreads 年轻代和年老代垃圾回收启用的线程数
    -XX:+UseConcMarkSweepGC 对于年老代使用CMS垃圾收集器,启动后,-XX:+UseParNewGC自动启用
    -XX:+UseG1GC 启用G1收集器,G1是服务器类型的收集器,用于多核大内存机器,在满足高吞吐率的情况下,高概率满足GC暂停时间的目标

    6.2 Tomcat自身调优

    1. 禁用AJP连接器

      注释禁用掉server.xml中:

          <!-- Define an AJP 1.3 Connector on port 8009 -->
      
          <Connector protocol="AJP/1.3"
                     port="8009"
                     redirectPort="8443"  URIEncoding="UTF-8"/>
      
    2. 调整IO模式

      tomcat8以前默认使用BIO,tomcat8以后默认NIO模式

      当tomcat并发性能有较高要求或出现瓶颈时,可以使用ARP模式从操作系统级别解决异步IO问题,(需要在操作系统上安装ARP和Native)

    3. 调整配置tomcat线程池,见2.1配置详解

    作者:Loserfromlazy
    本文版权归作者和博客园共有,欢迎转载,但必须给出原文链接,并保留此段声明,否则保留追究法律责任的权利。
  • 相关阅读:
    【数据相关】如何进行数据标注(1)
    【机器视觉硬件】机器视觉硬件学习笔记3——镜头
    【机器视觉硬件】工业相机的分类
    【机器视觉硬件】工业相机的主要参数
    【机器视觉硬件】工业相机的主要接口类型
    【机器视觉硬件】机器视觉硬件学习笔记2——工业相机
    【知识相关】机器学习之独热编码(One-Hot)详解(代码解释)
    【机器视觉硬件】硬件学习笔记1——光源
    学习笔记分享之汇编---3. 堆栈&标志寄存器
    学习笔记分享之汇编---1. 通用寄存器
  • 原文地址:https://www.cnblogs.com/yhr520/p/15792651.html
Copyright © 2020-2023  润新知