拉勾教育学习-笔记分享の"渗透"Tomcat

289 阅读15分钟

【文章内容输出来源:拉勾教育Java高薪训练营】
--- 所有脑图均本人制作,未经允许请勿滥用 ---
试试来手写实现一个mini版的Tomcat"

一、 Tomcat 系统架构与原理剖析

Part1 - 准备工作

下载软件包和源代码

Tomcat官网

从表面开始分解

bin目录

conf目录

lib目录
因为Tomcat是基于Java开发的工具,所以在lib目录中放置了其自身依赖的基础Jar包
logs目录
存放打印的日志
temp目录
存放临时目录
webapps目录
Tomcat用来发布项目的空间,会将这个文件下的war包和项目发布
work目录
追溯到早期,JSP页面被解析时成Servlet(Java类),这个过程中产生的过程文件就会被放在这个目录中
LICENSE
规定你在传播过程中需要遵从的规范

Part2 - 浏览器访问服务器的流程

Part3 - Tomcat系统总体架构

身份I HTTP服务器

之所以叫Tomcat是HTTP服务器 --> 能够接收并且处理http请求
对应组件——Connector连接器:负责对外交流 具体组件名称——Coyote

身份II Servlet容器

对应组件——Container连接器:负责对内处理 具体组件名称——Catalina

Part4 - Tomcat 连接器组件 Coyote

客户端通过Coyote与服务器建⽴连接、发送请求并接受响应 。
(1)Coyote 封装了底层的⽹络通信(Socket 请求及响应处理)
(2)Coyote 使Catalina 容器(容器组件)与具体的请求协议及IO操作⽅式完全解耦
(3)Coyote 将Socket 输⼊转换封装为 Request 对象,进⼀步封装后交由 Catalina 容器进⾏处理,处 理请求完成后, Catalina 通过Coyote 提供的Response 对象将结果写⼊输出流
(4)Coyote 负责的是具体协议(应⽤层)和IO(传输层)相关内容

支持的IO模型与协议

应用层

  1. HTTP/1.1默认 --> 大部分Web应用采用的访问协议
  2. AJP --> 用于和WX继承(Apache)
  3. HTTP/2 --> 大幅提升Web性能的第二代协议,8.5+版本自适应

传输层

  1. NIO默认 --> 同步非阻塞I/O,采用 Java NIO 类库实现
  2. NIO2 --> 异步非阻塞I/O, 采用 JDK7 NIO2 类库实现
  3. APR --> 采用 Apache 可移植运行库实现,是C/C++编写的本地库

Coyote 内部组件及流程

EndPoint
通信端点——通信监听的接口(实现TCP/IP协议)
具体Socket接收和发送处理器,是对传输层的抽象,因此EndPoint⽤来实现TCP/IP协议的
Processor
协议处理接口(实现HTTP协议)
接收来自 EndPoint 的 Socket,读取字节流解析成Tomcat Request和 Response 对象
并通过 Adapter 将其提交到容器处理
ProtocolHandler
协议接口
通过Endpoint 和 Processor , 实现针对具体协议的处理能力。
Tomcat 按照协议和I/O 提供了6个实现类 :
AjpNioProtocol ,AjpAprProtocol, AjpNio2Protocol , Http11NioProtocol ,Http11Nio2Protocol ,Http11AprProtocol
Adapter
将 Tomcat Request 转成 ServletRequest
由于协议不同,客户端发过来的请求信息也不尽相同
Tomcat设计者的解决方案是引入 CoyoteAdapter 转化从 ProtocolHandler 传来的非标准请求

Part4 - Tomcat Servlet容器 Catalina

Tomcat 本质上就是⼀款 Servlet 容器

Catalina 才是 Tomcat 的核心,其他模块都是为Catalina 提供⽀撑的
可以认为整个Tomcat就是⼀个Catalina实例

1. Tomcat 启动的时候会初始化这个实例
2. Catalina实例通过加载server.xml完成其他实例的创建并管理⼀个Server
3. Server创建并管理多个服务
4. 每个服务⼜可以有多个Connector和⼀个Container

组件具体结构

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

二、 Tomcat 服务器核心配置详解

下面着重说明 Service 标签中的内容

Executor

默认情况下,Service 并未添加共享线程池配置。 如果我们想添加⼀个线程池, 可以在 <Executor> 下添加如下配置

  <!--The connectors can use a shared executor, you can define one or more named thread pools-->
  <!-- name : 线程池名称 -->
  <!-- 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"
      maxIdleTime="60000" maxQueueSize="Integer.MAX_VALUE"
      prestartminSpareThreads="false" threadPriority="5"
      className="org.apache.catalina.core.StandardThreadExecutor"/>

Connector

⽤于创建链接器实例
默认情况下,server.xml 配置了两个链接器,⼀个⽀持HTTP协议,⼀个⽀持AJP协议

  <!-- port : 端口号,如果设置为0,Tomcat将会随机选择⼀个可⽤的端⼝号给当前Connector 使⽤ -->
  <!-- protocol : 当前Connector ⽀持的访问协议 -->
  <!-- connectionTimeout : Connector 接收链接后的等待超时时间,单位为 毫秒。 -1 表示不超时。 -->
  <!-- redirectPort : 当前Connector 不⽀持SSL请求,接收到了⼀个请求,并且也符合security-constraint 约束,需要SSL传输,Catalina⾃动将请求重定向到指定的端⼝。 -->
  <!-- executor: : 指定共享线程池的名称, 也可以通过maxThreads、minSpareThreads 等属性配置内部线程池 -->
  <!-- URIEncoding : ⽤于指定编码URI的字符编码, Tomcat8.x版本默认的编码为 UTF-8 , Tomcat7.x版本默认为ISO8859-1 -->
  <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />
  <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />

Engine

Servlet 引擎

 <!-- name : ⽤于指定Engine 的名称, 默认为Catalina -->
 <!-- defaultHost : 默认使⽤的虚拟主机名称 -->
 <Engine name="Catalina" defaultHost="localhost">

Host

⽤于配置⼀个虚拟主机

<Host name="localhost"  appBase="webapps" unpackWARs="true" autoDeploy="true">

Context

⽤于配置⼀个Web应⽤

    <!-- docBase : Web应⽤⽬录或者War包的部署路径。可以是绝对路径,也可以是相对于 Host appBase的相对路径 -->
    <!-- path : Web应⽤的Context 路径。
    	如果我们Host名为localhost, 则该web应⽤访问的根路径为:http://localhost:8080/web_demo。 
    -->
    <Context docBase="/Users/yingdian/web_demo" path="/web3"></Context>

三、 手写实现迷你版 Tomcat

码云-源码提供

Part 1 简易交互 V1.0


主启动类

public class BootStrapV1 {
    
    /* 定义socket监听的端口号 */
    private int port = 8080;
    
    public int getPort() {
        return port;
    }
    
    public void setPort(int port) {
        this.port = port;
    }
    
    /**
     * MiniCat启动需要初始化展开的一些操作
     */
    public void start() {
        
        /*
            MiniCat 1.0版本
            需求:浏览器请求http://localhost:8080,返回一个固定的字符串到页面"Hello MiniCat!"
         */
        try {
            ServerSocket serverSocket = new ServerSocket(port);
            System.out.println("===> MiniCat start on port:" + port);
    
            while(true) {
                Socket socket = serverSocket.accept();
                // 有了 socket,接收到请求,获取输出流输出内容
                OutputStream os = socket.getOutputStream();
                String data = "Hello MiniCat";
                String responseText = HttpProtocolUtilV1.getHttpHeader200(data.getBytes().length) + data;
                os.write(responseText.getBytes());
                socket.close();
            }
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    
    }
    
    public static void main(String[] args) {
        BootStrapV1 bootStrapV1 = new BootStrapV1();
        bootStrapV1.start();
    }
    
}

封装协议工具

public class HttpProtocolUtilV1 {
    
    /**
     * 为响应码 200 提供请求头信息
     *
     * @return
     */
    public static String getHttpHeader200(long contentLength) {
        return "HTTP/1.1 200 OK \n" +
                "Content-Type: text/html;charset=utf-8 \n" +
                "Content-Length: " + contentLength + "\n" +
                "\r\n";
    }
    
    /**
     * 为响应码 404 提供请求头信息
     *
     * @return
     */
    public static String getHttpHeader404() {
        String str404 = "<h1>404 Not Found</h1>";
        return "HTTP/1.1 200 OK \n" +
                "Content-Type: text/html;charset=utf-8 \n" +
                "Content-Length: " + str404.getBytes().length + "\n" +
                "\r\n";
    }
    
}

Part 2 升级封装 V2.0


主启动类

public class BootStrapV2 {
    
    /* 定义socket监听的端口号 */
    private int port = 8080;
    public int getPort() {
        return port;
    }
    public void setPort(int port) {
        this.port = port;
    }
    
    /**
     * MiniCat启动需要初始化展开的一些操作
     */
    public void start() {
        
        /*
         * 完成 MiniCat 2.0版本
         * 需求:封装Request和Response对象,返回html静态资源文件
         */
        try {
            ServerSocket serverSocket = new ServerSocket(port);
            System.out.println("===> MiniCat start on port:" + port);
            
            while(true) {
                Socket socket = serverSocket.accept();
                InputStream inputStream = socket.getInputStream();
                // 从输入流获取请求信息
                // 封装Request对象和Response对象
                Request request = new Request(inputStream);
                Response response = new Response(socket.getOutputStream());
                response.outputHtml(request.getUrl());
                socket.close();
            }
            
        } catch (IOException e) {
            e.printStackTrace();
        }
        
    }
    
    public static void main(String[] args) {
        BootStrapV2 bootStrapV2 = new BootStrapV2();
        bootStrapV2.start();
    }
    
}

请求封装类

public class Request {

    private String method; // 请求方式-[GET/POST]
    private String url; // 资源定位
    private InputStream inputStream; // 输入流
    
    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;
    }
    public InputStream getInputStream() {
        return inputStream;
    }
    public void setInputStream(InputStream inputStream) {
        this.inputStream = inputStream;
    }
    
    public Request() {}
    
    public Request(InputStream inputStream) throws IOException {
        this.inputStream = inputStream;
    
        /* 以下 while 针对网络间断进行判定,没有数据则一直读 */
        int count = 0;
        while (count == 0) {
            count = inputStream.available();
        }
        byte[] bytes = new byte[count];
        inputStream.read(bytes);
    
        String inputStr = new String(bytes);
        // 获取首行请求头信息
        String firstStr = inputStr.split("\\n")[0];
        String[] s = firstStr.split(" ");
        this.method = s[0];
        this.url = s[1];
        System.out.println("===> url = " + this.url);
        System.out.println("===> method = " + this.method);
    }
}

响应封装类

public class Response {
    
    private OutputStream outputStream;
    
    public Response() {
    }
    
    public Response(OutputStream outputStream) {
        this.outputStream = outputStream;
    }
    
    // 使用输出流输出指定字符串
    public void output(String content) throws IOException {
        outputStream.write(content.getBytes());
    }
    
    /**
     * @param path 随后要根据url来获取到静态资源的绝对路径,进一步根据绝对路径读取该静态资源文件,
     *             最终通过输出流输出
     */
    public void outputHtml(String path) throws IOException {
        // 获取静态资源文件的绝对路径
        String absoluteResourcePath = StaticResourceUtil.getAbsolutePath(path);
    
        // 输入静态资源文件
        File file = new File(absoluteResourcePath);
        if(file.exists() && file.isFile()) {
            // 读取静态资源文件,输出静态资源
            StaticResourceUtil.outputStaticResource(new FileInputStream(file),outputStream);
        }else{
            // 输出404
            output(HttpProtocolUtil.getHttpHeader404());
        }
    }
}

静态资源定位工具类

public class StaticResourceUtil {
    
    /**
     * 获取静态资源文件的绝对路径
     * @param path
     * @return
     */
    public static String getAbsolutePath(String path) {
        String absolutePath = StaticResourceUtil.class.getResource("/").getPath();
        return absolutePath.replaceAll("\\\\","/") + path;
    }
    
    /**
     * 读取静态资源文件输入流,通过输出流输出
     */
    public static void outputStaticResource(InputStream inputStream, OutputStream outputStream) throws IOException {
        
        int count = 0;
        while(count == 0) {
            count = inputStream.available();
        }
        
        int resourceSize = count;
        // 输出http请求头,然后再输出具体内容
        outputStream.write(HttpProtocolUtil.getHttpHeader200(resourceSize).getBytes());
        
        // 读取内容输出
        long written = 0 ;// 已经读取的内容长度
        int byteSize = 1024; // 计划每次缓冲的长度,一点一点读
        byte[] bytes = new byte[byteSize];
        
        /* 没写完就一直写 */
        while(written < resourceSize) {
            // 说明剩余未读取大小不足一个1024长度,那就按真实长度处理
            if(written  + byteSize > resourceSize) {
                byteSize = (int) (resourceSize - written);  // 剩余的文件内容长度
                bytes = new byte[byteSize];
            }
            
            inputStream.read(bytes);
            outputStream.write(bytes);
            
            outputStream.flush();
            written+=byteSize;
        }
    }
    
}

静态页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>static resource</title>
</head>
<body>
<h1>This is a static resource.</h1>
</body>
</html>

Part 3 结合Servlet V3.0


顶层Servlet接口

public interface Servlet {
    
    void init() throws Exception;
    void destroy() 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".equalsIgnoreCase(request.getMethod())) {
            doGet(request, response);
        } else {
            doPost(request, response);
        }
    }
}

继承顶层抽象类的 自定义Servlet

public class MyServlet extends HttpServlet {
    @Override
    public void doGet(Request request, Response response) {
        String content = "<h1>MyServlet get</h1>";
        try {
            response.output((HttpProtocolUtil.getHttpHeader200(content.getBytes().length) + content));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    @Override
    public void doPost(Request request, Response response) {
        String content = "<h1>MyServlet post</h1>";
        try {
            response.output((HttpProtocolUtil.getHttpHeader200(content.getBytes().length) + content));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    @Override
    public void init() throws Exception { }
    
    @Override
    public void destroy() throws Exception { }
}

依赖引入

<dependencies>
    <dependency>
        <groupId>dom4j</groupId>
        <artifactId>dom4j</artifactId>
        <version>1.6.1</version>
    </dependency>
    <dependency>
        <groupId>jaxen</groupId>
        <artifactId>jaxen</artifactId>
        <version>1.1.6</version>
    </dependency>
</dependencies>

web.xml

<?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-servlet</url-pattern>
    </servlet-mapping>
</web-app>

主启动类

public class BootStrapV3 {
    
    /* 用于存储 web.xml 中servlet的映射关系 */
    private Map<String,HttpServlet> servletMap = new HashMap<String,HttpServlet>();
    
    /* 定义socket监听的端口号 */
    private int port = 8080;
    public int getPort() {
        return port;
    }
    public void setPort(int port) {
        this.port = port;
    }
    
    /**
     * MiniCat启动需要初始化展开的一些操作
     */
    public void start() {
    
        // 加载解析相关的配置,web.xml
        loadServlet();
        
        /*
         * 完成 MiniCat 3.0版本
         * 需求:可以请求动态资源(Servlet)
         */
        try {
            ServerSocket serverSocket = new ServerSocket(port);
            System.out.println("===> MiniCat start on port:" + port);
            
            while(true) {
                Socket socket = serverSocket.accept();
                InputStream inputStream = socket.getInputStream();
                // 从输入流获取请求信息
                // 封装Request对象和Response对象
                Request request = new Request(inputStream);
                Response response = new Response(socket.getOutputStream());
    
                // 静态资源处理
                if(servletMap.get(request.getUrl()) == null) {
                    response.outputHtml(request.getUrl());
                }else{
                    // 动态资源servlet请求
                    HttpServlet httpServlet = servletMap.get(request.getUrl());
                    httpServlet.service(request,response);
                }
                socket.close();
            }
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    
    }
    
    /**
     * 加载解析web.xml,初始化Servlet
     */
    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 (int i = 0; i < selectNodes.size(); i++) {
                Element element =  selectNodes.get(i);
                // ==> 拿到 <servlet-name>MyServlet</servlet-name>
                Element servletNameElement = (Element) element.selectSingleNode("servlet-name");
                String servletName = servletNameElement.getStringValue();
                // ==> 拿到 <servlet-class>server.MyServlet</servlet-class>
                Element servletClassElement = (Element) element.selectSingleNode("servlet-class");
                String servletClass = servletClassElement.getStringValue();
                
                // 根据 servlet-name 的值找到 url-pattern (使用XPath表达式)
                Element servletMapping = (Element) rootElement.selectSingleNode("/web-app/servlet-mapping[servlet-name='" + servletName + "']");
                // 拿到 /my-servlet
                String urlPattern = servletMapping.selectSingleNode("url-pattern").getStringValue();
                servletMap.put(urlPattern, (HttpServlet) Class.forName(servletClass).newInstance());
                
            }
        } catch (DocumentException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        
    }
    
    public static void main(String[] args) {
        BootStrapV3 bootStrapV2 = new BootStrapV3();
        bootStrapV2.start();
    }
    
}

Part 4 多线程改造 V4.0

现在我们强行在doGet中添加休眠(模拟线程堵塞)

因此需要多线程来解决当前问题:

重点改造

主启动类

public class BootStrapV4 {
    
    /* 用于存储 web.xml 中servlet的映射关系 */
    private Map<String,HttpServlet> servletMap = new HashMap<String,HttpServlet>();
    
    /* 定义socket监听的端口号 */
    private int port = 8080;
    public int getPort() {
        return port;
    }
    public void setPort(int port) {
        this.port = port;
    }
    
    /**
     * MiniCat启动需要初始化展开的一些操作
     */
    public void start() {
    
        // 定义一个线程池
        int corePoolSize = 10;
        int maximumPoolSize =50;
        long keepAliveTime = 100L;
        TimeUnit unit = TimeUnit.SECONDS;
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(50);
        ThreadFactory threadFactory = Executors.defaultThreadFactory();
        RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
    
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                corePoolSize,
                maximumPoolSize,
                keepAliveTime,
                unit,
                workQueue,
                threadFactory,
                handler
        );
    
        // 加载解析相关的配置,web.xml
        loadServlet();
        
        /*
         * 完成 MiniCat 4.0版本
         * 需求:多线程请求
         */
        try {
            ServerSocket serverSocket = new ServerSocket(port);
            System.out.println("===> MiniCat start on port:" + port);
            
            System.out.println("=========>>>>>>使用线程池进行多线程改造");
            // 多线程改造 (且使用线程池节约资源)
            while(true) {
                Socket socket = serverSocket.accept();
                RequestProcessor requestProcessor = new RequestProcessor(socket,servletMap);
                threadPoolExecutor.execute(requestProcessor);
            }
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    
    }
    
    /**
     * 加载解析web.xml,初始化Servlet
     */
    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 (int i = 0; i < selectNodes.size(); i++) {
                Element element =  selectNodes.get(i);
                // ==> 拿到 <servlet-name>MyServlet</servlet-name>
                Element servletNameElement = (Element) element.selectSingleNode("servlet-name");
                String servletName = servletNameElement.getStringValue();
                // ==> 拿到 <servlet-class>server.MyServlet</servlet-class>
                Element servletClassElement = (Element) element.selectSingleNode("servlet-class");
                String servletClass = servletClassElement.getStringValue();
                
                // 根据 servlet-name 的值找到 url-pattern (使用XPath表达式)
                Element servletMapping = (Element) rootElement.selectSingleNode("/web-app/servlet-mapping[servlet-name='" + servletName + "']");
                // 拿到 /my-servlet
                String urlPattern = servletMapping.selectSingleNode("url-pattern").getStringValue();
                servletMap.put(urlPattern, (HttpServlet) Class.forName(servletClass).newInstance());
                
            }
        } catch (DocumentException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        
    }
    
    public static void main(String[] args) {
        BootStrapV4 bootStrapV2 = new BootStrapV4();
        bootStrapV2.start();
    }
    
}

线程处理类

public class RequestProcessor extends Thread {
    
    private Socket socket;
    private Map<String, HttpServlet> servletMap;
    
    public RequestProcessor(Socket socket, Map<String, HttpServlet> servletMap) {
        this.socket = socket;
        this.servletMap = servletMap;
    }
    
    @Override
    public void run() {
        try{
            InputStream inputStream = socket.getInputStream();
        
            // 封装Request对象和Response对象
            Request request = new Request(inputStream);
            Response response = new Response(socket.getOutputStream());
        
            // 静态资源处理
            if(servletMap.get(request.getUrl()) == null) {
                response.outputHtml(request.getUrl());
            }else{
                // 动态资源servlet请求
                HttpServlet httpServlet = servletMap.get(request.getUrl());
                httpServlet.service(request,response);
            }
            socket.close();
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Part 5 多线程改造 V5.0

本次改造是课程结束后的大作业,作业内容如下:

开发Minicat V4.0,在已有Minicat基础上进⼀步扩展,模拟出webapps部署效果
磁盘上放置⼀个webapps目录,webapps中可以有多个项目,比如demo1,demo2,demo3...
具体的项目比如demo1中有serlvet(也即为:servlet是属于具体某⼀个项⽬的servlet),这样的话在
Minicat初始化配置加载,以及根据请求url查找对应serlvet时都需要进⼀步处理

项目结构

整体思路

  1. 新建两个web项目 demo01 demo02(WEB-INF中含有classes/lib)
  2. 在MiniCat项目中新建 server.xml 通过 <Server>等标签指定外部引入的 web 应用
  3. 解析 server.xml 文件信息到MiniCat内存容器中
  4. 解析 demo01 demo02 中的 web.xml 并将对应的外部Servlet信息获取
  5. 通过在容器中查询 url <--> Servlet 映射关系 从而在访问对应 url时 执行外部 Servlet

代码逻辑图解

实现效果

四、 Tomcat 源码构建及核心流程源码剖析

Part 1 源代码准备工作

step A 下载源代码并整理目录

pom.xml

<?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.50-src</artifactId>
    <name>Tomcat8.5</name>
    <version>8.5</version>
    <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>11</source>
                    <target>11</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>

step B 导入IDEA

新建空项目

导入下载好的src项目

step C 给 tomcat 的源码程序启动类 Bootstrap 配置 VM 参数

tomcat 源码运⾏也需要加载配置⽂件

以下是我当前路径配置,读者需修改,且最好不要有中文路径
-Dcatalina.home=/Users/Administrator/Desktop/Tomcat&Nginx资料/Tomcat&Nginx资料/软件包及源码包/apache-tomcat-8.5.50-src/source
-Dcatalina.base=/Users/Administrator/Desktop/Tomcat&Nginx资料/Tomcat&Nginx资料/软件包及源码包/apache-tomcat-8.5.50-src/source
-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager

step D 找到BootStrap的main入口 启动

访问8080端⼝时,我们会遇到这个问题

原因是Jsp引擎Jasper没有被初始化,从⽽⽆法编译JSP
我们需要在tomcat的源码ContextConfig类中的configureStart⽅法中增加⼀⾏代码将 Jsp 引擎初始化

context.addServletContainerInitializer(new JasperInitializer(), null);

Part 2 源码剖析启动时序

时序图(来源:拉勾教育-JAVA高薪训练营)

Tomcat中的各容器组件都会涉及创建、销毁等,
因此设计了⽣命周期接口 Lifecycle 进⾏统⼀规范,各容器组件实现该接口

Part 3 从Main入口开始跟踪源码

待跟进

五、 Tomcat 类加载机制剖析

Java类(.java)—> 字节码⽂件(.class) —> 字节码⽂件需要被加载到jvm内存当中(这个过程就是⼀个类加载的过程)

类加载器 ClassLoader,说白了也是⼀个类,jvm启动的时候先把类加载器读取到内存当中去,其他的类

Part 1 从JVM的类加载机制开始

Tomcat 类加载机制是在 JVM 类加载机制基础之上进行了⼀些变动

分类

  1. 引导类加载器 BootstrapClassLoader
    c++编写,加载java核⼼库 java.*,比如rt.jar中的类,构造ExtClassLoader和AppClassLoader
  2. 扩展类加载器 ExtClassLoader
    java编写,加载扩展库 JAVA_HOME/lib/ext⽬录下的jar中的类,如classpath中的jrejavax.*或者java.ext.dir指定位置中的类
  3. 系统类加载器 SystemClassLoader/AppClassLoader
    默认的类加载器,搜索环境变量 classpath 中指明的路径

用户可以自定义类加载器,加载顺序如下

  1. 用户自己的的类加载器,把加载请求传给⽗加载器,⽗加载器再传给其⽗加载器,⼀直到加载器树的顶层
  2. 最顶层的类加载器⾸先针对其特定的位置加载,如果加载不到就转交给⼦类
  3. 如果⼀直到底层的类加载都没有加载到,那么就会抛出异常 ClassNotFoundException

Part 2 双亲委派机制

定义

当某个类加载器需要加载某个.class⽂件时,它⾸先把这个任务委托给他的上级类加载器
递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类

作用

  • 防止重复加载同⼀个.class。
    通过委托去向上⾯问⼀问,加载过了,就不⽤再加载⼀遍。保证数据安全
  • 保证核⼼.class不能被篡改
    通过委托⽅式,不会去篡改核⼼.class,即使篡改也不会去加载,即使加载也不会是同⼀个.class对象了

Part 3 Tomcat 的类加载机制

没有严格的遵从双亲委派机制,也可以说打破了双亲委派机制

(来源:拉勾教育-JAVA高薪训练营)

  • 引导类加载器 和 扩展类加载器 的作⽤不变
  • 系统类加载器正常情况下加载的是 CLASSPATH 下的类,但是 Tomcat 的启动脚本并未使⽤该变量,⽽是加载tomcat启动的类,⽐如bootstrap.jar,通常在catalina.bat或者catalina.sh中指定。位于CATALINA_HOME/bin下
  • Common 通⽤类加载器加载Tomcat使⽤以及应⽤通⽤的⼀些类,位于CATALINA_HOME/lib下,⽐如servlet-api.jar
  • Catalina ClassLoader ⽤于加载服务器内部可⻅类,这些类应⽤程序不能访问
  • Shared ClassLoader ⽤于加载应⽤程序共享类,这些类服务器不会依赖
  • Webapp ClassLoader,每个应⽤程序都会有⼀个独⼀⽆⼆的Webapp ClassLoader,他⽤来加载本应⽤程序 /WEB-INF/classes 和 /WEB-INF/lib 下的类。

tomcat 8.5 默认改变了严格的双亲委派机制
⾸先从 Bootstrap Classloader加载指定的类
---> 如果未加载到,则从 /WEB-INF/classes加载
-----> 如果未加载到,则从 /WEB-INF/lib/* .jar 加载
-------> 如果未加载到,则从 /WEB-INF/lib/* .jar 加载

六、 Tomcat 对 Https 的支持及 Tomcat 性能优化策略

Https是用来加强数据传输安全的

Part 1 HTTPS

HTTP VS HTTPS

  • HTTPS协议使⽤时需要到电⼦商务认证授权机构(CA)申请SSL证书
  • HTTP默认使⽤8080端⼝,HTTPS默认使⽤8443端⼝
  • HTTPS则是具有SSL加密的安全性传输协议,对数据的传输进⾏加密,效果上相当于HTTP的升级版
  • HTTP的连接是⽆状态的,不安全的;
    HTTPS协议是由SSL+HTTP协议构建的可进⾏加密传输、身份认证的⽹络协议,⽐HTTP协议安全

HTTPS 工作原理

(来源:拉勾教育-JAVA高薪训练营)

Part 2 Tomcat 对 HTTPS 的支持

待跟进

Part 3 Tomcat 性能优化策略

待跟进

虚拟机运行优化

待跟进

配置优化

待跟进