手动实现Tomcat和自己设计Servlet

82 阅读8分钟

一、Maven的使用

Java传统项目:

image-20240124233654240

Maven的Java项目:

image-20240124233729797

创建Maven的Web项目:

创建Project

image-20240124233810102

选择Maven

image-20240124233827037

改名字

image-20240124234108925

修改maven依赖的配置

image-20240124234204953

补充:如何配置阿里maven镜像

  1. 把D:\program\JavaIDEA 2020.2\plugins\maven\lib\maven3\conf\settings.xml拷贝默认的maven配置目录
  2. C:\Users\Administrator.m2目录settings.xml
  3. 修改C:\Users\Administrator.m2\settings.xm,增加下面的部分
<mirrors>
    <!-- mirror
    | Specifies a repository mirror site to use instead of a given repository. The
    repository that
    | this mirror serves has an ID that matches the mirrorOf element of this mirror.
    IDs are used
    | for inheritance and direct lookup purposes, and must be unique across the
    set of mirrors. |
    <mirror>
    <id>mirrorId</id>
    <mirrorOf>repositoryId</mirrorOf>
    韩顺平 Java 工程师
    <name>Human Readable Name for this Mirror.</name>
    <url>http://my.repository.com/repo/path</url>
    </mirror>
    -->
    <mirror>
    <id>alimaven</id>
    <name>aliyun maven</name>
    <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
    <mirrorOf>central</mirrorOf>
    </mirror>
</mirrors>

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>com.hspedu</groupId>
  <artifactId>hsp-tomcat</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>war</packaging>

  <name>hsp-tomcat Maven Webapp</name>
  <!-- FIXME change it to the project's website -->
  <url>http://www.example.com</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>

  <!--
    1. dependency 表示依赖, 也就是我们这个项目需要依赖的 jar 包
    2. groupId 和 artifactId 被统称为坐标, 是为了去定位这个项目/jar
    3. groupId: 一般是公司 比如 com.baidu , 这里是 avax.servlet
    4. artifactId 一般是项目名, 这里是 javax.servlet-api
    5. 这样的化就可以定位一个 jar 包
    6. version 表示你引入到我们项目的 jar 包的版本是 3.1.0
    7. scope: 表示作用域,也就是你引入的 jar 包的作用范围
    8. provided 表示在 tomcat 本身是有这个 jar 的,因此在编译,测试使用,但是在打包发布就不用要带上
  -->
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>3.1.0</version>
      <scope>provided</scope>
    </dependency>
  </dependencies>
</project>

写计算器的html页面:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>计算器</title>
</head>
<body>
    <h1>计算器</h1>
    <form action="/cal/calServlet" method="get">
        num1:<input type="text" name="num1"><br/>
        num2:<input type="text" name="num2"><br/>
        <input type="submit" value="提交">
    </form>
</body>
</html>

Servlet页面:

public class CalServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        int num1 = Integer.parseInt(req.getParameter("num1"));
        int num2 = Integer.parseInt(req.getParameter("num2"));
        int result = num1 + num2;
        resp.setContentType("text/html; charset=utf-8");
        PrintWriter writer = resp.getWriter();
        writer.write("<h1>" + result + "</h1>");
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }
}

二、Tomcat整体架构分析

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

image-20240126084212063

1. 任务阶段1

编写Tomcat,要求能返回hello xxx

基于Socket开发的服务流程

image-20240126085218018

1.ServerSocket:在服务器监听指定端口,如果浏览器/客户端连接该端口,则建立连接,返回Socket对象

2.Socket:表示服务器和客户端/浏览器的连接,通过Socket可以得到InputStream和OutputStream流对象

实现第一阶段

需要模仿这个相信消息

public class OlivierTomcatVersion1 {
    public static void main(String[] args) throws IOException {
        //1.创建ServerSocket, 监听8080端口
        ServerSocket serverSocket = new ServerSocket(8080);
        System.out.println("OlivierTomcatVersion1在8080端口监听");

        while (!serverSocket.isClosed()){
            //如果serverSocket没有关闭,就等待客户端/服务器连接
            //如果有连接请求,就创建一个Socket对象
            //Socket就是服务器端和浏览器端的连接和通道
            Socket socket = serverSocket.accept();

            //先接收浏览器发送的数据
            //InputStream是字节流,要转换为BufferedReader字符流
            InputStream inputStream = socket.getInputStream();
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream,"utf-8"));

            String msg = null;
            System.out.println("======接收到浏览器发送的数据===");
            //循环读取
            while ((msg = bufferedReader.readLine())!= null){
                if (msg.length() == 0) {
                    break;
                }
                System.out.println(msg);
            }

            //自定义Tomcat回送--http响应方式
            OutputStream outputStream = socket.getOutputStream();
            //构建一个http响应的消息头
            String respHeader = "HTTP/1.1 200 OK\r\n" +
                    "Content-Type:text/html;charset=utf-8\r\n\r\n";
            //http响应体需要有两个换行\r\n
            String resp = respHeader + "<h1>hello tomcat</h1>";
            System.out.println("======我们的Tomcat给浏览器回送的数据===");
            System.out.println(resp);
            outputStream.write(resp.getBytes());

            outputStream.flush();
            outputStream.close();
            inputStream.close();
            socket.close();
        }
    }
}

2. 任务阶段2

使用BIO线程模型去支持多线程

BIO线程模型介绍

image-20240126130252673

我们使用HspRequestHandler,请求过来,猫会创建HspRequestHandler,Socket对象持有的输入流和输出流,将Socket传给HspRequestHandler,它不实际操作输入流和输出流,只起到一个分发作用

image-20240126130942876

HspRequestHandler代码:

/**
 * 这个对象是一个线程对象
 * 它是处理一个http请求的
 */
public class HspRequestHandler implements Runnable {
    //定义一个Socket对象
    private Socket socket = null;

    public HspRequestHandler(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        //这里我们可以对客户端/浏览器进行IO交互
        try {
            InputStream inputStream = socket.getInputStream();
            //将inputStream转换成bufferedReader,方便进行数据接收,也就是按行读取
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream,"utf-8"));
            System.out.println("-----OlivierTomcatVersion2接收到如下数据-----");
            String msg = null;
            while ((msg = bufferedReader.readLine()) != null){
                if (msg.length() == 0) {
                    break;  //确实读取到了,但是是个空串,就跳出循环
                }
                System.out.println(msg);
            }

            //构建以下http响应头
            String respHeader = "HTTP/1.1 200 OK\r\n" +
                    "Content-Type:text/html;charset=utf-8\r\n\r\n";
            String resp = respHeader + "<h1>hello tomcat2</h1>";
            //返回数据给浏览器,将其封装成http响应
            OutputStream outputStream = socket.getOutputStream();
            outputStream.write(resp.getBytes());
            outputStream.flush();
            outputStream.close();
            inputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                if (socket != null) {
                    socket.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

自己的Tomcat的代码:

public class OlivierTomcatVersion2 {
    public static void main(String[] args) throws IOException {
        //监听8080端口
        ServerSocket serverSocket = new ServerSocket(8080);
        System.out.println("OlivierTomcatVersion2在8080端口监听");
        while (!serverSocket.isClosed()){
            Socket socket = serverSocket.accept();
            Thread thread = new Thread(new HspRequestHandler(socket));
            thread.start();
        }
    }
}

依然存在的问题:现在确实有多个线程处理请求了,但是没有和Servlet和web.xml相关联

3. 任务阶段3

处理Servlet

先要回顾Servlet的生命周期

image-20240126135010464

新需求:浏览器请求http://localhost:8080/OlivierCalServlet,提交数据,完成计算任务,如果Servlet不存在,返回404

实现代码:

/**
 * HspRequest作用是封装http请求的数据
 * 封装其中的参数比如,get还是post,uti,还有参数列表num1=10&num2=20...
 * HspRequest作用就是等价于原生的Servlet中的HttpRequest
 */
public class HspRequest {
    private String method;
    private String uri;
    private HashMap<String, String> parametersMapping = new HashMap<>();

    public HspRequest(InputStream inputStream) throws IOException {
        //inputStream是和对应的http请求的socket相关联
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
        //读取第一行,类似这个形式GET /olivierServlet?num1=10&num2=25 HTTP/1.1
        String requestLine = bufferedReader.readLine();
        String[] requestLineArr = requestLine.split(" ");
        System.out.println("requestLineArr = " + requestLineArr);
        //这样获取的就是上面的GET,将这个参数放入method属性里
        method = requestLineArr[0];
        //解析出/olivierServlet uri,先看看有没有参数列表,放入uri中
        int index = requestLineArr[1].indexOf("?");
        System.out.println("index = " + index);
        if (index == -1) {
            uri = requestLineArr[1];
        } else {
            uri = requestLineArr[1].substring(0, index);
            //获取参数列表,放入parametersMapping
            String parameters = requestLine.split(" ")[1].substring(index + 1);
            String[] parametersPair = parameters.split("&");
            if (null != parametersPair && !"".equals(parametersPair)) {
                for (String parameterPair : parametersPair) {
                    String[] parameterVal = parameterPair.split("=");
                    if (parameterVal.length == 2) {
                        parametersMapping.put(parameterVal[0], parameterVal[1]);
                    }
                }
            }
        }
    }

    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;
    }

    public String getParameter(String name) {
        if (parametersMapping.containsKey(name)) {
            return parametersMapping.get(name);
        }else {
            return "";
        }
    }
}

添加HspResponse,添加处理结果的逻辑:

/**
 * HspResponse对象可以封装OutputStream,也是和Socket相关联
 * 也就是通过HspResponse对象,返回http响应给浏览器
 * HspResponse对象的作用等价于原生的servlet的HttpServletResponse
 */
public class HspResponse {
    private OutputStream outputStream = null;
    //http的响应头,这里只写成功的格式,如果愿意可以提供一个灵活的set方法
    public static final String RESPONSE_HEADER = "HTTP/1.1 200 OK\r\n" +
            "Content-Type:text/html;charset=utf-8\r\n\r\n";

    public HspResponse(OutputStream outputStream){
        this.outputStream = outputStream;
    }

    //当我们需要给浏览器返回数据时,可以通过HspResponse的输出流来完成
    public OutputStream getOutputStream() {
        return outputStream;
    }
}

修改handler代码

/**
 * 这个对象是一个线程对象
 * 它是处理一个http请求的
 */
public class HspRequestHandler implements Runnable {
    //定义一个Socket对象
    private Socket socket = null;

    public HspRequestHandler(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        //这里我们可以对客户端/浏览器进行IO交互
        try {
            InputStream inputStream = socket.getInputStream();

            HspRequest hspRequest = new HspRequest(inputStream);
            String num1 = hspRequest.getParameter("num1");
            String num2 = hspRequest.getParameter("num2");

            System.out.println("num1 = " + num1);
            System.out.println("num2 = " + num2);

            //构建以下http响应头
            String resp = HspResponse.RESPONSE_HEADER + "<h1>hspResponse返回的hi MyTomcat3</h1>";
            OutputStream outputStream = socket.getOutputStream();
            outputStream.write(resp.getBytes());
            outputStream.flush();
            outputStream.flush();
            inputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                if (socket != null) {
                    socket.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

4. 任务阶段4

该要添加Servlet处理请求的逻辑了

需求,仿照Servlet代码实现Servlet的三个主要方法:

  1. init方法
  2. service方法
  3. destroy方法
public void init() throws Exception;

public void service(HspRequest req, HspResponse res) throws Exception;

public void destroy();

第一步:先按顺序写好HspServlet,HspHttpServlet和HspCalServlet

HspServlet

public interface HspServlet {
    public void init() throws Exception;

    public void service(HspRequest req, HspResponse res) throws Exception;

    public void destroy();
}

HspHttpServlet

public abstract class HspHttpServlet implements HspServlet {
    @Override
    public void service(HspRequest req, HspResponse res) throws Exception {
        if ("GET".equalsIgnoreCase(req.getMethod())) {
            this.doGet(req, res);
        } else if ("POST".equalsIgnoreCase(req.getMethod())) {
            this.doPost(req, res);
        }
    }

    /**
     * 这里只是一个模板设计模式,让HspHttpServlet的子类去实现这个方法
     *
     * @param hspRequest
     * @param hspResponse
     */
    public abstract void doGet(HspRequest hspRequest, HspResponse hspResponse) throws IOException;

    /**
     * 这里只是一个模板设计模式,让HspHttpServlet的子类去实现这个方法
     *
     * @param hspRequest
     * @param hspResponse
     */
    public abstract void doPost(HspRequest hspRequest, HspResponse hspResponse);
}

HspCalServlet

public class HspCalServlet extends HspHttpServlet{
    @Override
    public void doGet(HspRequest hspRequest, HspResponse hspResponse) throws IOException {
        int number1 = WebUtils.parseInt(hspRequest.getParameter("num1"), 0);
        int number2 = WebUtils.parseInt(hspRequest.getParameter("num2"), 0);
        int result = number1 + number2;
        //返回数据
        OutputStream outputStream = hspResponse.getOutputStream();
        String mes = HspResponse.RESPONSE_HEADER + "<h1>" + number1 + " + " + number2 + " = " + result + " in HspCalServlet3</h1>";
        outputStream.write(mes.getBytes());
        outputStream.flush();
        outputStream.close();
    }

    @Override
    public void doPost(HspRequest hspRequest, HspResponse hspResponse) {
        doPost(hspRequest, hspResponse);
    }

    @Override
    public void init() throws Exception {

    }

    @Override
    public void destroy() {

    }
}

HspRequestHandler

public class HspRequestHandler implements Runnable {
    //定义一个Socket对象
    private Socket socket = null;

    public HspRequestHandler(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        //这里我们可以对客户端/浏览器进行IO交互
        try {
            InputStream inputStream = socket.getInputStream();
            HspRequest hspRequest = new HspRequest(inputStream);
            OutputStream outputStream = socket.getOutputStream();
            HspResponse hspResponse = new HspResponse(outputStream);

            //获取uri
            String uri = hspRequest.getUri();
            //获取servletName
            String servletName = OlivierTomcatVersion3.servletUrlMapping.get(uri);
            //或缺servlet实例
            HspHttpServlet servlet = OlivierTomcatVersion3.servletMapping.get(servletName);
            if (servlet != null) {
                servlet.service(hspRequest, hspResponse);
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (socket != null) {
                    socket.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

写一个OlivierTomcatVersion3功能

/**
 * 通过xml+反射初始化容器
 */
public class OlivierTomcatVersion3 {
    //1. 存放ServletMapping的容器
    public static final ConcurrentHashMap<String, HspHttpServlet> servletMapping = new ConcurrentHashMap<>();
    //2. 存放url-pattern和hashmap映射的
    public static final ConcurrentHashMap<String, String> servletUrlMapping = new ConcurrentHashMap<>();

    //直接对两个容器进行初始化
    public static void init() {
        //需要使用dom4j技术,去读取web.xml
        //这是找到target下文件资源的路径
        String path = OlivierTomcatVersion3.class.getResource("/").getPath();
        path = "D:\\Knowledge Files\\code in here\\hsp-tomcat\\target\\classes\\";
        System.out.println("path = " + path);
        //使用dom4j技术
        SAXReader saxReader = new SAXReader();
        try {
            Document document = saxReader.read(new File(path + "web.xml"));
            //得到根元素
            Element rootElement = document.getRootElement();
            //得到根元素下的所以元素
            List<Element> elements = rootElement.elements();
            //遍历并过滤,检查servlet和servletMapping
            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(), (HspHttpServlet) Class.forName(servletClass.getText()).newInstance());
                } else if ("servlet-mapping".equalsIgnoreCase(element.getName())) {
                    Element servletName = element.element("servlet-name");
                    Element urlPattern = element.element("url-pattern");
                    servletUrlMapping.put(urlPattern.getText(), servletName.getText().trim());
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        //验证两个容器是否初始化成功
        System.out.println("servletMapping -- " + servletMapping);
        System.out.println("servletUrlMapping -- " + servletUrlMapping);
    }

    //启动容器
    public static void run() throws IOException {
        init();
        //监听8080端口
        ServerSocket serverSocket = new ServerSocket(8080);
        System.out.println("OlivierTomcatVersion3在8080端口监听");
        while (!serverSocket.isClosed()) {
            Socket socket = serverSocket.accept();
            Thread thread = new Thread(new HspRequestHandler(socket));
            thread.start();
        }
    }

    public static void main(String[] args) throws IOException {
        run();
    }
}

WebUtils

public class WebUtils {
    //将字符串转成数字
    public static int parseInt(String strNum, int defaultVal){
        try {
            return Integer.parseInt(strNum);
        }catch (NumberFormatException e){
            System.out.println(strNum + "不能转成数字");
        }
        return defaultVal;
    }
}