目的:了解tomcat机制

手动实现 Tomcat 底层机制+ 自己设计 Servlet

目标: 不用 Tomcat, 不用系统提供的 Servlet, 模拟 Tomcat 底层实现并能调用我们自己设计的 Servle, 也能完成相同的功能

注:是和韩顺平老师学习的

一、Tomcat 整体架构分析

Tomcat 有三种运行模式(BIO, NIO, APR), 核心讲解的是 Tomcat 如何接收客户端请求,解析请求, 调用 Servlet , 并返回结果的机制流程, 采用 BIO 线程模型来模拟

在这里插入图片描述

二、手动实现 Tomcat 底层机制+ 自己设计 Servlet

2.1 基于 socket 开发服务端-流程

在这里插入图片描述

2.2 第一步 ,浏览器请求 http://localhost:8080/, 服务端返回hello 你好呀

创建MzqTomcatV01.java

package com.mzq.mytomcat;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

/**
 * @author MengZhiQiang
 * @version 1.0
 * @date 2022/4/2 21:08
 * 接收浏览器的请求,并返回信息
 */
public class MzqTomcatV01 {
   public static void main(String[] args) throws IOException {
      //1. 创建ServerSocket 在8080端口监听
      ServerSocket serverSocket = new ServerSocket(8080);
      System.out.println("=========mytomcat在8080端口监听=======");
      while (!serverSocket.isClosed()){
         //等待客户端连接
         //如果连接过来 就创建一个socket
         //这个socket就是服务端和浏览器的连接
         Socket socket = serverSocket.accept();

         //接收浏览器发送的请求 io
         //为了方便读取,使用字符流进行读取,将InputStream转为BufferedReader
         InputStream inputStream = socket.getInputStream();
         BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream,"utf-8"));
         String mes = null;
         //循环读取
         while ((mes= bufferedReader.readLine())!=null){
            if (mes.length()==0){
               break;
            }
            System.out.println(mes);
         }

         //我们tomcat会发送http响应方式
         OutputStream outputStream = socket.getOutputStream();
         //构建一个 http 响应的头
         //\r\n 表示换行
         //http 响应体,需要前面有两个换行 \r\n\r\n(中间空了1行)
         String respHeader = "HTTP/1.1 200 OK\r\n" +
                 "Content-Type: text/html;charset=utf-8\r\n\r\n";
         String resp = respHeader + "hello 你好呀";
         System.out.println("========我们给浏览器回应数据==========");
         System.out.println(resp);
         outputStream.write(resp.getBytes());
         outputStream.flush();
         outputStream.close();
         inputStream.close();
         socket.close();
      }
   }
}

运行结果:

在这里插入图片描述

在这里插入图片描述

三、 使用 BIO 线程模型,支持多线程

3.1 BIO 线程模型介绍

在这里插入图片描述

3.2 需求分析

浏览器请求 http://localhost:8080, 服务端返回 helloword ,后台mytomcat 使用 BIO 线程模型,支持多线程=> 对前面的开发模式进行改造

分析示意图:

在这里插入图片描述

代码实现:

MzqRequestHandler类

**
 * @author MengZhiQiang
 * @version 1.0
 * @date 2022/4/6 19:02
 * MzqRequestHandler 对象是一个线程对象
 * 处理一个http请求
 */
public class MzqRequestHandler implements Runnable {
    //定义一个Socket
    private Socket socket = null;

    //无参构造
    public MzqRequestHandler(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {


            InputStream inputStream = socket.getInputStream();

            //把inputStream -> BufferedReader -> 方便进行按行读取
            BufferedReader bufferedReader =
                    new BufferedReader(new InputStreamReader(inputStream, "utf-8"));

            //不同的线程在和浏览器和客户端交互
            System.out.println("当前线程= " + Thread.currentThread().getName());

            System.out.println("=========Mzqomcatv2 接收到的数据如下=========");
            String mes = null;

            while ((mes = bufferedReader.readLine()) != null) {
                //如果长度为0 ""
                if (mes.length() == 0) {
                    break; //退出
                }
                System.out.println(mes);
            }
            //构建一下http响应头
            //返回的http的响应体和响应头之间有两个换行 \r\n\r\n
            String respHeader = "HTTP/1.1 200 OK\r\n" +
                    "Content-Type: text/html;charset=utf-8\r\n\r\n";
            String resp = respHeader + "<h1>helloword</h1>";
            System.out.println("========hsptomcatv2返回的数据是=========");
            System.out.println(resp);
            //返回数据给我们的浏览器/客户端-> 封装成http响应
            OutputStream outputStream = socket.getOutputStream();
            resp.getBytes(); //是把字符串转成字节数组
            outputStream.write(resp.getBytes());
            outputStream.flush();
            outputStream.close();
            inputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //最后一定确保socket要关闭
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

启动:

public static void main(String[] args) throws IOException {
      ServerSocket serverSocket = new ServerSocket(8080);
      System.out.println("=========我在8080端口监听=========");
      while (!serverSocket.isClosed()) {
         //1. 接收到浏览器的连接后,如果成功,就会得到socket
         //2. 这个socket 就是 服务器和 浏览器的数据通道
         Socket socket = serverSocket.accept();
         //3. 创建一个线程对象,并且把socket给该线程
         //  这个是java线程基础
         MzqRequestHandler mzqRequestHandler =
                 new MzqRequestHandler(socket);
         new Thread(mzqRequestHandler).start();
      }

   }

运行结果:

在这里插入图片描述

四、实现自己的servlet,resquest,sponse

在这里插入图片描述

4.1 实现自己resquest

创建MzqRequest类:

package com.mzq.mytomcat.http;

/**
 * @author MengZhiQiang
 * @version 1.0
 * @date 2022/4/7 20:45
 */

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;

/**
 * 解读
 * 1. HspRequest 作用是封装http请求的数据
 * get /mzqCalServlet?num1=10&num2=30
 * 2. 比如 method(get) 、 uri(/mzqCalServlet) 、 还有参数列表 (num1=10&num2=30)
 * 3. HspRequest 作用就等价原生的servlet 中的HttpServletRequest
 * 4. 这里考虑的是GET请求
 */
public class MzqRequest {
    private String method;
    private String uri;

    //request对象有一个特别重要方法 获取uri参数
    public String getParameter(String name) {
        if (parametersMapping.containsKey(name)) {
            return parametersMapping.get(name);
        } else {
            return "";
        }
    }

    public String getMethod() {
        return method;
    }

    public void setMethod(String method) {
        this.method = method;
    }

    public String getUri() {
        return uri;
    }

    public void setUri(String uri) {
        this.uri = uri;
    }

    //存放参数列表 参数名-参数值=》HashMap
    private HashMap<String,String > parametersMapping = new HashMap<>();
    private InputStream inputStream = null;

    //构造器=> 对http请求进行封装
    //inputStream 是和 对应http请求的socket关联
    public MzqRequest(InputStream inputStream) {
        this.inputStream = inputStream;
        encapHttpRequest();
    }

    @Override
    public String toString() {
        return "MzqRequest{" +
                "method='" + method + '\'' +
                ", uri='" + uri + '\'' +
                ", parametersMapping=" + parametersMapping +
                '}';
    }

    /**
     * 将http请求的相关数据,进行封装,然后提供相关的方法,进行获取
     */
    private void encapHttpRequest() {
        System.out.println("MzqRequest init()");
        try {
            //inputstream -> BufferedReader
            BufferedReader bufferedReader =
                    new BufferedReader(new InputStreamReader(inputStream, "utf-8"));

            //读取第一行
            /**
             * GET /mzqCalServlet?num1=10&num2=30 HTTP/1.1
             * Host: localhost:8080
             */
            String requestLine = bufferedReader.readLine();
            //GET - /hspCalServlet?num1=10&num2=30 - HTTP/1.1
            String[] requestLineArr = requestLine.split(" ");
            //得到method
            method = requestLineArr[0];
            //解析得到 /mzqCalServlet
            //1. 先看看uri 有没有参数列表
            int index = requestLineArr[1].indexOf("?");
            if (index == -1) { //说明没有参数列表
                uri = requestLineArr[1];
            } else {
                //[0,index)
                uri = requestLineArr[1].substring(0, index);
                //获取参数列表->parametersMapping
                //parameters => num1=10&num2=30
                String parameters = requestLineArr[1].substring(index + 1);
                //num1=10 , num2=30 .... parametersPair= ["num1=10","num2=30" ]
                String[] parametersPair = parameters.split("&");
                //防止用户提交时 /mzqCalServlet?
                if (null != parametersPair && !"".equals(parametersPair)) {
                    //再次分割 parameterPair = num1=10
                    for (String parameterPair : parametersPair) {
                        //parameterVal ["num1", "10"]
                        String[] parameterVal = parameterPair.split("=");
                        if (parameterVal.length == 2) {
                            //放入到 parametersMapping
                            parametersMapping.put(parameterVal[0], parameterVal[1]);
                        }
                    }
                }
            }
            //这里不能关闭流 inputStream 和 socket关联
            //inputStream.close();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

4.2 实现自己response

创建MzqResponse类:

package com.mzq.mytomcat.http;

/**
 * @author MengZhiQiang
 * @version 1.0
 * @date 2022/4/7 20:45
 */

import java.io.OutputStream;

/**
 * 解读
 * 1. MzqResponse对象可以封装OutputStream(是socket关联)
 * 2. 即可以通过 MzqResponse对象 返回Http响应给浏览器/客户端
 * 3. MzqResponse对象 的作用等价于原生的servlet的 HttpServletResponse
 */
public class MzqResponse {
    private OutputStream outputStream = null;

    //写一个http的响应头 => 先死后活
    public static final String respHeader = "HTTP/1.1 200 OK\r\n" +
            "Content-Type: text/html;charset=utf-8\r\n\r\n";

    //如果有兴趣, 在编写更多的方法
    //比如 setContentType

    //在创建 HspResponse 对象时,传入的outputStream是和Socket关联的
    public MzqResponse(OutputStream outputStream) {
        this.outputStream = outputStream;
    }
    //当我们需要给浏览器返回数据时,可以通过HspResponse 的输出流完成
    public OutputStream getOutputStream() {
        return outputStream;
    }
}

4.3 实现自己servlet

Servlet 生命周期:

在这里插入图片描述

在这里插入图片描述

根据原生类图:

创建一个MzqServlet接口:

import com.mzq.mytomcat.http.MzqRequest;
import com.mzq.mytomcat.http.MzqResponse;

import java.io.IOException;

/**
 * @author MengZhiQiang
 * @version 1.0
 * @date 2022/4/7 21:06
 */
public interface MzqServlet {
    /**
     * 自己构建三个核心方法
     */
    void init() throws Exception;
    //(MzqRequest request, MzqResponse response为上面自己写的两个类
    void service(MzqRequest request, MzqResponse response) throws IOException;

    void destroy();
}

创建一个抽象类:使用的了模板设计模式

package com.mzq.mytomcat.servlet;

import com.mzq.mytomcat.http.MzqRequest;
import com.mzq.mytomcat.http.MzqResponse;

import java.io.IOException;

/**
 * @author MengZhiQiang
 * @version 1.0
 * @date 2022/4/7 21:08
 */
public abstract class MzqHttpServlet implements MzqServlet{
    @Override
    public void service(MzqRequest request, MzqResponse response) throws IOException {
        //老师说明 equalsIgnoreCase 比较字符串内容是相同,不区别大小写
        if("GET".equalsIgnoreCase(request.getMethod())) {
            //这里会有动态绑定
            this.doGet(request,response);
        } else if("POST".equalsIgnoreCase(request.getMethod())) {
            this.doPost(request,response);
        }
    }

    //这里我们使用的了模板设计模式
    //让MzqHttpServlet 子类 MzqCalServlet 实现

    public abstract void doGet(MzqRequest request, MzqResponse response);
    public abstract void doPost(MzqRequest request, MzqResponse response);
}

创建一个自己的Servlet类MzqMyServlet:

package com.mzq.mytomcat.servlet;

import com.mzq.mytomcat.http.MzqRequest;
import com.mzq.mytomcat.http.MzqResponse;
import com.mzq.utils.WebUtils;

import java.io.IOException;
import java.io.OutputStream;

/**
 * @author MengZhiQiang
 * @version 1.0
 * @date 2022/4/7 21:11
 */
public class MzqMyServlet extends MzqHttpServlet{

    @Override
    public void doGet(MzqRequest request, MzqResponse response) {
        //java基础的 OOP 的动态绑定机制..
        //写业务代码,完成计算任务
        int num1 = WebUtils.parseInt(request.getParameter("num1"), 0);
        int num2 = WebUtils.parseInt(request.getParameter("num2"), 0);

        int sum = num1 + num2;

        //返回计算结果给浏览器
        //outputStream 和 当前的socket关联
        OutputStream outputStream = response.getOutputStream();
        String respMes = MzqResponse.respHeader
                + "<h1>" + num1 + " + " + num2 + " = " + sum + " MyTomcatV3 - 反射+xml创建</h1>";
        try {
            outputStream.write(respMes.getBytes());
            outputStream.flush();
            outputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void doPost(MzqRequest request, MzqResponse response) {
        this.doGet(request, response);
    }

    @Override
    public void init() throws Exception {

    }

    @Override
    public void destroy() {

    }
}

根据需求编写WebUtils工具类:

public class WebUtils {

    /**
     * 将一个字符串数字,转成 int, 如果转换失败,就返回传入 defaultVal
     * @param strNum
     * @param defaultVal
     * @return
     */
    public static int parseInt(String strNum, int defaultVal) {

        try {
            return Integer.parseInt(strNum);
        } catch (NumberFormatException e) {
            System.out.println(strNum + " 格式不对,转换失败");
        }

        return defaultVal;
    }

    //判断uri是不是html文件
    public static boolean isHtml(String uri) {

        return uri.endsWith(".html");
    }

    //根据文件名来读取该文件->String
    public static String readHtml(String filename) {
        String path = com.mzq.utils.WebUtils.class.getResource("/").getPath();
        StringBuilder stringBuilder = new StringBuilder();

        try {
            BufferedReader bufferedReader = new BufferedReader(new FileReader(path + filename));
            String buf = "";
            while ((buf = bufferedReader.readLine()) != null) {
                stringBuilder.append(buf);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return stringBuilder.toString();
    }
}

4.4 实现通过xml+反射来初始化容器

使用dom4j技术:

<dependency>
      <groupId>dom4j</groupId>
      <artifactId>dom4j</artifactId>
      <version>1.1</version>
    </dependency>

创建一个初始化容器的类:

package com.mzq.mytomcat;

import com.mzq.mytomcat.servlet.MzqHttpServlet;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import javax.servlet.http.HttpSession;
import java.io.File;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author MengZhiQiang
 * @version 1.0
 * @date 2022/4/7 21:14
 * 实现通过xml+反射来初始化容器
 */
public class MzqTomcatV3 {
    //1. 存放容器 servletMapping
    // -ConcurrentHashMap
    // -HashMap
    // key            - value
    // ServletName    对应的实例

    public static final ConcurrentHashMap<String, MzqHttpServlet>
            servletMapping = new ConcurrentHashMap<>();



    //2容器 servletUrlMapping
    // -ConcurrentHashMap
    // -HashMap
    // key                    - value
    // url-pattern       ServletName

    public static final ConcurrentHashMap<String, String>
            servletUrlMapping = new ConcurrentHashMap<>();


    //你可以这里理解session, tomcat还维护一个容器
    public static final ConcurrentHashMap<String, HttpSession>
            sessionMapping = new ConcurrentHashMap<>();

    //变强..
    public static void main(String[] args) {
        MzqTomcatV3 hspTomcatV3 = new MzqTomcatV3();
        hspTomcatV3.init();
        //启动hsptomcat容器
        hspTomcatV3.run();
    }


    //启动HspTomcatV3容器
    public void run() {

        try {
            ServerSocket serverSocket = new ServerSocket(8080);
            System.out.println("=====mzqtomcatv3在8080监听======");
            while (!serverSocket.isClosed()) {
                Socket socket = serverSocket.accept();
                MzqRequestHandler hspRequestHandler =
                        new MzqRequestHandler(socket);
                new Thread(hspRequestHandler).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    //直接对两个容器进行初始化
    public void init() {
        //读取web.xml => dom4j =>
        //得到web.xml文件的路径 => 拷贝一份.
        String path = MzqTomcatV3.class.getResource("/").getPath();
        System.out.println("path= " + path);
        //使用dom4j技术完成读取
        SAXReader saxReader = new SAXReader();
        try {
            Document document = saxReader.read(new File(path + "web.xml"));
            System.out.println("document= " + document);
            //得到根元素
            Element rootElement = document.getRootElement();
            //得到根元素下面的所有元素
            List<Element> elements = rootElement.elements();
            //遍历并过滤到 servlet servlet-mapping
            for (Element element : elements) {
                if ("servlet".equalsIgnoreCase(element.getName())) {
                    //这是一个servlet配置
                    //使用反射将该servlet实例放入到servletMapping
                    Element servletName = element.element("servlet-name");
                    Element servletClass = element.element("servlet-class");
                    servletMapping.put(servletName.getText(),
                            (MzqHttpServlet) Class.forName(servletClass.getText().trim()).newInstance());
                } else if ("servlet-mapping".equalsIgnoreCase(element.getName())) {
                    //这是一个servlet-mapping
                    //System.out.println("发现 servlet-mapping");

                    Element servletName = element.element("servlet-name");
                    Element urlPatter = element.element("url-pattern");
                    servletUrlMapping.put(urlPatter.getText(), servletName.getText());

                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        //老韩验证,这两个容器是否初始化成功
        System.out.println("servletMapping= " + servletMapping);
        System.out.println("servletUrlMapping= " + servletUrlMapping);
    }
}

修改原先线程类MzqRequestHandler为:

package com.mzq.mytomcat;

import com.mzq.mytomcat.http.MzqRequest;
import com.mzq.mytomcat.http.MzqResponse;
import com.mzq.mytomcat.servlet.MzqHttpServlet;
import com.mzq.utils.WebUtils;

import java.io.*;
import java.net.Socket;

/**
 * @author MengZhiQiang
 * @version 1.0
 * @date 2022/4/6 19:02
 * MzqRequestHandler 对象是一个线程对象
 * 处理一个http请求
 */
public class MzqRequestHandler implements Runnable {
    //定义一个Socket
    private Socket socket = null;

    //无参构造
    public MzqRequestHandler(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            MzqRequest hspRequest = new MzqRequest(socket.getInputStream());
            //这里我们可以同HspResponse对象,返回数据给浏览器/客户端
            MzqResponse hspResponse = new MzqResponse(socket.getOutputStream());
            //1. 得到 uri => 就是 servletUrlMapping 的 url-pattern
            String uri = hspRequest.getUri();
            String servletName = MzqTomcatV3.servletUrlMapping.get(uri);
            if (servletName == null) {
                servletName = "";
            }
            //2. 通过uri->servletName->servlet的实例 , 真正的运行类型是其子类 HspCalServlet
            MzqHttpServlet hspHttpServlet =
                    MzqTomcatV3.servletMapping.get(servletName);
            //3. 调用service , 通过OOP的动态绑定机制,调用运行类型的 doGet/doPost
            if (hspHttpServlet != null) {//得到
                hspHttpServlet.service(hspRequest, hspResponse);
            } else {
                //没有这个servlet , 返回404的提示信息
                String resp = MzqResponse.respHeader + "<h1>404 Not Found</h1>";
                OutputStream outputStream = hspResponse.getOutputStream();
                outputStream.write(resp.getBytes());
                outputStream.flush();
                outputStream.close();
            }
            socket.close();

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //最后一定确保socket要关闭
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

五、测试

运行MzqTomcatV3中main方法(类似启动tomcat):

在这里插入图片描述

在这里插入图片描述

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐