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

文章目录

1.Tomcat整体架构分析

自己理解

pdf下载

2.第一阶段

1.实现功能

编写自己的Tomcat能接受浏览器的请求并返回结果

2.代码
1.TomcatV1.java
java 复制代码
package Tomcat;

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

/**
 * @author 孙显圣
 * @version 1.0
 * 第一个版本的,可以接收浏览器的请求并返回信息
 */
public class TomcatV1 {
    public static void main(String[] args) throws Exception {
        //在8080端口监听
        ServerSocket serverSocket = new ServerSocket(8081);
        System.out.println("Tomcat在8080端口监听");


        //只要不是手动关闭服务,则循环获取连接
        while (!serverSocket.isClosed()) {

            //获取连接
            Socket accept = serverSocket.accept();
            //获取输入流
            InputStream inputStream = accept.getInputStream();
            //使用转换流转为bufferedreader可以读取一行
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
            //字符串缓冲区
            String buff = null;
            while ((buff = bufferedReader.readLine()) != null) {
                //readline()在网络编程中,只有客户端的连接关闭了才会返回null,所以如果不设置别的判定条件退出,则会阻塞
                //由于请求信息最后有一个\r\n,读到这个\r\n会返回一个"",就可以退出了
                if (buff.equals("")) {
                    break;
                }
                System.out.println(buff);
            }

            //tomcat向浏览器发送http响应,\r\n是换行
            String respHead =
                    "HTTP/1.1 200 OK\r\n" +
                    "Content-Type: text/html;charset=utf-8\r\n\r\n"; //响应头下面要空一行才能写响应体所以两个换行
            String resp = respHead + "<h1>hello world!</h1>";

            //获取输出流
            OutputStream outputStream = accept.getOutputStream();
            //输出
            outputStream.write(resp.getBytes());

            //关闭
            outputStream.flush();
            outputStream.close();
            inputStream.close();
            accept.close();
        }



    }
}
3.调试阶段
1.阻塞在readLine导致无法返回结果
  1. 最开始我并没有添加,如果readLine()读到""就退出循环的逻辑
  2. 后来发现readLine()在网络编程中,只有浏览器端关闭连接才会返回null
  3. 当读到了请求的\r\n的时候就会返回一个""字符串,然后阻塞在这里等待输入
  4. 所以在循环中添加当读到\r\n即返回""的时候退出即可
4.结果演示

3.第二阶段

1.实现功能

BIO线程模型支持多线程

2.代码
1.RequestHander.java
java 复制代码
package Tomcat.handler;

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

/**
 * @author 孙显圣
 * @version 1.0
 * 处理http请求的线程类
 */
public class RequestHander implements Runnable{
    //定义一个socket属性
    private Socket socket = null;

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

    public void run() {
        //可以对客户端或浏览器进行交互
        try {
            //获取输入流
            InputStream inputStream = socket.getInputStream();
            //转换成BufferedReader,方便按行读取
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
            System.out.println("tomcatV2接受到浏览器的数据如下:");
            //接受数据
            //设置缓冲
            String buff = null;
            //循环读取
            while ((buff = bufferedReader.readLine()) != null) {
                //判断是否读取到了\r\n
                if (buff.equals("")) {
                    break; //读取完毕则退出循环,避免readLine阻塞
                }
                System.out.println(buff);
            }

            //获取输出流,返回信息到浏览器
            OutputStream outputStream = socket.getOutputStream();
            String respHead =
                    "HTTP/1.1 200 OK\r\n" +
                            "Content-Type: text/html;charset=utf-8\r\n\r\n"; //响应头下面要空一行才能写响应体所以两个换行
            String resp = respHead + "<h1>hello 孙显圣!</h1>";
            outputStream.write(resp.getBytes());

            //关闭
            outputStream.flush();
            outputStream.close();
            inputStream.close();
            socket.close();

        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            //确保关闭
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }

    }
}
2.TomcatV2.java
java 复制代码
package Tomcat;

import Tomcat.handler.RequestHander;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * @author 孙显圣
 * @version 1.0
 */
public class TomcatV2 {
    public static void main(String[] args) throws IOException {
        //在8080端口监听
        ServerSocket serverSocket = new ServerSocket(8080);
        System.out.println("TomcatV2在8080端口监听");

        //只要没有手动关闭则服务一直开启,循环监听
        while (!serverSocket.isClosed()) {

            Socket socket = serverSocket.accept();

            //将获取的socket交给线程类来处理
            RequestHander requestHander = new RequestHander(socket);
            Thread thread = new Thread(requestHander);
            thread.start(); //启动线程


        }
    }
}
3.调试阶段
1.发现每次按回车会接受到两次请求

原因就是每次请求还要请求一下页面的小图标

4.结果演示

4.第三阶段

1.实现功能
2.总体框架
3.代码实现
===1.封装Request
1.Request.java
java 复制代码
package Tomcat.http;

/**
 * @author 孙显圣
 * @version 1.0
 */

import java.io.*;
import java.util.HashMap;

/**
 * 1.Request的作用就是封装http请求的数据 GET /tomcatv2?a=9&b=3 HTTP/1.1
 * 2.比如method(get), uri(/tomcat/cal),还有参数列表(num1&num2)
 * 3.相当于原生的HttpServletRequest
 */
public class Request {
    private String method;
    private String uri;
    //存放参数列表
    private HashMap<String, String> parametersMapping = new HashMap<String, String>();

    //构造方法获取inputStream来封装信息
    public Request(InputStream inputStream) throws IOException {
        //转换成BufferedReader
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(
                inputStream, "utf-8"));
        //读取第一行
        String s = bufferedReader.readLine();
        //将内容封装到属性
        String[] split = s.split(" ");
        method = split[0];
        //判断是否有参数列表
        int index = split[1].indexOf("?");

        if (index == -1) { //没有参数列表
            uri = split[1];
        }
        else { //有参数列表
            uri = split[1].substring(0,index);
            String parameters = split[1].substring(index + 1, split[1].length());
            String[] split1 = parameters.split("&"); //分割参数
            //判断?后面是否有东西
            if (split1 != null && !"".equals(split1)) {
                //遍历参数
                for (String parameterPair : split1) {
                    String[] split2 = parameterPair.split("=");
                    parametersMapping.put(split2[0],split2[1]);
                }
            }

        }
        //不能关闭,否则socket也就关闭了
//        inputStream.close();
    }

    public String getMethod() {
        return method;
    }

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

}
2.RequestHander.java
java 复制代码
package Tomcat.handler;

import Tomcat.http.Request;

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

/**
 * @author 孙显圣
 * @version 1.0
 * 处理http请求的线程类
 */
public class RequestHander implements Runnable {
    //定义一个socket属性
    private Socket socket = null;

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

    public void run() {
        //可以对客户端或浏览器进行交互
        try {
            //获取输入流
            InputStream inputStream = socket.getInputStream();
            //封装到Request里
            Request request = new Request(inputStream);
            //获取数据
            System.out.println(request.getUri() + " " + request.getMethod());
            System.out.println(request.getParameter("num1") + " " + request.getParameter("num2")
            + " " + request.getParameter("num3"));

            //获取输出流,返回信息到浏览器
            OutputStream outputStream = socket.getOutputStream();
            String respHead =
                    "HTTP/1.1 200 OK\r\n" +
                            "Content-Type: text/html;charset=utf-8\r\n\r\n"; //响应头下面要空一行才能写响应体所以两个换行
            String resp = respHead + "<h1>hello 孙显圣!</h1>";
            outputStream.write(resp.getBytes());

            //关闭
            outputStream.flush();
            outputStream.close();
            inputStream.close();
            socket.close();

        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            //确保关闭
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }

    }
}
单元测试报错Sock is closed
原因

在Request.java中获取完参数顺便把inputStream关闭了,导致了Socket也一起关闭了,使得主线程在使用socket的时候出现了已经关闭的错误

修改之后
===2.封装Response
3.Response.java
java 复制代码
package Tomcat.http;

/**
 * @author 孙显圣
 * @version 1.0
 */

import java.io.OutputStream;

/**
 * 1.这个response对象可以封装OutputSream
 * 2.即可以通过这个对象返回http响应给浏览器
 * 3.相当于原生的HttpServletResponse
 */
public class Response {
    private OutputStream outputStream = null;
    //封装一个响应头
    public static final String respHeader = "HTTP/1.1 200 OK\r\n" +
            "Content-Type: text/html;charset=utf-8\r\n\r\n"; //响应头下面要空一行才能写响应体所以两个换行

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

    //以后再Servlet里面获取输出流
    public OutputStream getOutputStream() {
        return outputStream;
    }
}
4.RequestHander.java
java 复制代码
package Tomcat.handler;

import Tomcat.http.Request;
import Tomcat.http.Response;

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

/**
 * @author 孙显圣
 * @version 1.0
 * 处理http请求的线程类
 */
public class RequestHander implements Runnable {
    //定义一个socket属性
    private Socket socket = null;

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

    public void run() {
        //可以对客户端或浏览器进行交互
        try {
            //获取输入流
            InputStream inputStream = socket.getInputStream();
            //封装到Request里
            Request request = new Request(inputStream);
            //获取数据
            System.out.println(request.getUri() + " " + request.getMethod());
            System.out.println(request.getParameter("num1") + " " + request.getParameter("num2")
            + " " + request.getParameter("num3"));

            //获取输出流,返回信息到浏览器
            OutputStream outputStream = socket.getOutputStream();
            //封装到Response对象中
            Response response = new Response(outputStream);

            //获取输出流输出信息
            OutputStream outputStream1 = response.getOutputStream();
            outputStream1.write((response.respHeader + "<h1>response响应:你好,孙显圣</h1>").getBytes());


            //关闭
            outputStream.flush();
            outputStream.close();
            inputStream.close();
            socket.close();

        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            //确保关闭
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }

    }
}
单元测试无误
===3.设计servlet规范
5.Servlet.java
java 复制代码
package Tomcat.servlet;

import Tomcat.http.Request;
import Tomcat.http.Response;

import java.io.IOException;

/**
 * @author 孙显圣
 * @version 1.0
 * 保留三个核心方法
 */
public interface Servlet {
    void init() throws Exception;
    void service(Request request, Response response) throws IOException;
    void destroy();
}
6.HttpServlet.java
java 复制代码
package Tomcat.servlet;

import Tomcat.http.Request;
import Tomcat.http.Response;

import java.io.IOException;

/**
 * @author 孙显圣
 * @version 1.0
 */
public abstract class HttpServlet implements Servlet {
    public void service(Request request, Response response) throws IOException {
        //抽象模板设计模式,判断类型来决定调用什么方法
        if ("GET".equalsIgnoreCase(request.getMethod())) {
            //以后会反射创建子类实例,调用子类的service方法,子类没有,就从父类找,然后再使用动态绑定到子类的doGet方法
            this.doGet(request, response);
        } else if ("POST".equalsIgnoreCase(request.getMethod())) {
            this.doPost(request, response);
        }
    }

    public abstract void doGet(Request request, Response response);

    public abstract void doPost(Request request, Response response);

}
7.CalServlet.java
java 复制代码
package Tomcat.servlet;

import Tomcat.http.Request;
import Tomcat.http.Response;
import Tomcat.utils.WebUtils;

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

/**
 * @author 孙显圣
 * @version 1.0
 */
public class CalServlet extends HttpServlet {

    public void doGet(Request request, Response response) {
        //完成计算任务
        int num1 = WebUtils.parseInt(request.getParameter("num1"), 0);
        int num2 = WebUtils.parseInt(request.getParameter("num2"), 0);
        int sum = num1 + num2;
        //返回计算结果给浏览器
        OutputStream outputStream = response.getOutputStream();
        String resp = Response.respHeader + "<h1>" + num1 + " + " + num2 + " = " + sum + "</h1>";
        try {
            outputStream.write(resp.getBytes());
            //关闭
            outputStream.flush();
            outputStream.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public void doPost(Request request, Response response) {
        this.doGet(request, response);
    }

    public void init() throws Exception {

    }

    public void destroy() {

    }
}
8.WebUtils.java
java 复制代码
package Tomcat.utils;

/**
 * @author 孙显圣
 * @version 1.0
 */
public class WebUtils {
    //把String类型转换成int类型,如果是非整数则返回默认值
    public static int parseInt(String str, int defaultVal) {
        try {
            return Integer.parseInt(str);
        } catch (NumberFormatException e) {
            System.out.println("转换失败");
        }
        return defaultVal;
    }

}
9.RequestHander.java
java 复制代码
package Tomcat.handler;

import Tomcat.http.Request;
import Tomcat.http.Response;
import Tomcat.servlet.CalServlet;

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

/**
 * @author 孙显圣
 * @version 1.0
 * 处理http请求的线程类
 */
public class RequestHander implements Runnable {
    //定义一个socket属性
    private Socket socket = null;

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

    public void run() {
        //可以对客户端或浏览器进行交互
        try {
            //获取输入流
            InputStream inputStream = socket.getInputStream();
            //封装到Request里
            Request request = new Request(inputStream);

            //获取输出流,返回信息到浏览器
            OutputStream outputStream = socket.getOutputStream();
            //封装到Response对象中
            Response response = new Response(outputStream);

            CalServlet calServlet = new CalServlet();
            calServlet.service(request,response); //这个会调用他抽象父类的service方法

            inputStream.close();
            socket.close();

        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            //确保关闭
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }

    }
}
单元测试无误
===4.xml + 反射初始化容器
10.web.xml
11.TomcatV3.java
java 复制代码
package Tomcat;

import Tomcat.handler.RequestHander;
import Tomcat.servlet.HttpServlet;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author 孙显圣
 * @version 1.0
 */
public class TomcatV3 {
    //设置两个hashmap容器,在启动的Tomcat的时候就初始化
    //存放名字和实例
    public static final ConcurrentHashMap<String, HttpServlet>
            servletMapping = new ConcurrentHashMap<String, HttpServlet>();
    //存放路径和名字
    public static final ConcurrentHashMap<String, String>
            servletUrlMapping = new ConcurrentHashMap<String, String>();

    public static void main(String[] args) throws MalformedURLException, DocumentException, ClassNotFoundException, InstantiationException, IllegalAccessException {
        TomcatV3 tomcatV3 = new TomcatV3();
        tomcatV3.init();
        tomcatV3.run();
    }

    //启动TomcatV3容器
    public void run() {
        try {

            ServerSocket serverSocket = new ServerSocket(8080);
            System.out.println("TomcatV3在8080端口监听");

            while (!serverSocket.isClosed()) { //只要没有手动关闭服务,就循环获取连接
                Socket socket = serverSocket.accept();
                //将socket交给线程处理
                RequestHander requestHander = new RequestHander(socket);
                new Thread(requestHander).start(); //启动线程
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private void init() throws MalformedURLException, DocumentException, ClassNotFoundException, InstantiationException, IllegalAccessException {
        //获取该类的路径
        String path = TomcatV3.class.getResource("/").getPath();

        //使用dom4j读取web.xml文件
        //获取解析器
        SAXReader saxReader = new SAXReader();
        //读取文件
        Document read = saxReader.read(new File(path + "web.xml"));
        //获取根元素
        Element rootElement = read.getRootElement();
        //获取所有二级元素
        List<Element> servlet = rootElement.elements();

        //遍历所有二级元素,根据不同类型做处理
        for (Element element : servlet) {
            if ("servlet".equalsIgnoreCase(element.getName())) {
                //获取名字和全类名
                Element servletName = element.element("servlet-name");
                Element servletClass = element.element("servlet-class");

                //通过反射创建实例
                Class<?> aClass = Class.forName(servletClass.getText().trim()); //trim是清除空格
                HttpServlet httpServlet = (HttpServlet) aClass.newInstance();

                //将其放到容器中去
                servletMapping.put(servletName.getText(), httpServlet);

            } else if ("servlet-mapping".equalsIgnoreCase(element.getName())) {
                //获取名字和url
                Element servletName = element.element("servlet-name");
                Element servletUrl = element.element("url-pattern");

                //将其放到容器中去
                servletUrlMapping.put("/tomcat" + servletUrl.getText(), servletName.getText());
            }
        }
    }


}
12.RequestHander.java
java 复制代码
package Tomcat.handler;

import Tomcat.TomcatV3;
import Tomcat.http.Request;
import Tomcat.http.Response;
import Tomcat.servlet.CalServlet;
import Tomcat.servlet.HttpServlet;

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

/**
 * @author 孙显圣
 * @version 1.0
 * 处理http请求的线程类
 */
public class RequestHander implements Runnable {
    //定义一个socket属性
    private Socket socket = null;

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

    public void run() {
        //可以对客户端或浏览器进行交互
        try {
            //获取输入流
            InputStream inputStream = socket.getInputStream();
            //封装到Request里
            Request request = new Request(inputStream);

            //获取输出流,返回信息到浏览器
            OutputStream outputStream = socket.getOutputStream();
            //封装到Response对象中
            Response response = new Response(outputStream);
            String uri = request.getUri();
            String servletName = TomcatV3.servletUrlMapping.get(uri);
            if (servletName == null) {
                servletName = "";
            }
            HttpServlet httpServlet = TomcatV3.servletMapping.get(servletName);
            //判断是否得到了这个对象
            if (httpServlet != null) {
                httpServlet.service(request, response);
            } else {
                //没有得到则返回404
                String resp = Response.respHeader + "<h1>404 not found</h1>";
                response.getOutputStream().write(resp.getBytes());
            }

            inputStream.close();
            socket.close();

        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            //确保关闭
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }

    }
}
4.总体调试阶段
1.空指针异常
5.结果演示

5.课后作业

1.更新WebUtils.java
添加方法
java 复制代码
    //判断是不是html格式的文件,如果是就直接读取文件内容并且返回true
    public static boolean isHtml(String uri, Response response) {
        //使用正则表达式匹配html文件
        String regStr = "(/.*)*/(.*\\.html)";
        Pattern compile = Pattern.compile(regStr);
        Matcher matcher = compile.matcher(uri);
        if (!matcher.find()) { //没匹配到就直接返回false
            return false;
        }
        //得到html文件的路径
        String path = "D:\\Intelij IDEA Project\\java_web\\tomcat\\target\\classes\\" + matcher.group(2);
        System.out.println(path);
        //根据路径读取文件并存放到StringBuilder中
        StringBuilder stringBuilder = new StringBuilder();
        try {
            BufferedReader bufferedReader = new BufferedReader(new FileReader(path));
            String buf = null;
            while ((buf = bufferedReader.readLine()) != null) {
                stringBuilder.append(buf); //存放到stringBuilder中
            }
            //将stringBuilder的内容响应给浏览器
            String resp = Response.respHeader + stringBuilder.toString();
            response.getOutputStream().write(resp.getBytes());
        }catch (Exception e) {
            System.out.println("文件找不到!");
            return false; //返回false之后就会继续进行原来的逻辑,弹出404
        }

        //如果不出异常则说明响应成功
        return true;
    }
2.修改RequestHander.java
3.cal.html
html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="/tomcat/CalServlet" method="get">
    num1:<input type="text" name="num1">
    num2:<input type="text" name="num2">
    <input type="submit" value="提交">
</form>
</body>
</html>
4.调试阶段
  1. 一直显示我的cal.html文件找不到,调了半个小时
  2. 原因是String path = TomcatV3.class.getResource("/").getPath();使用这个来获取的路径没有空格,而我的资源路径是这个D:\\Intelij IDEA Project\\java_web\\tomcat\\target\\classes\\,中间带了空格,真的是醉了,深刻体会到文件夹起名不要带空格的重要性了
5.结果展示
相关推荐
Daniel 大东19 分钟前
idea 解决缓存损坏问题
java·缓存·intellij-idea
wind瑞25 分钟前
IntelliJ IDEA插件开发-代码补全插件入门开发
java·ide·intellij-idea
HappyAcmen25 分钟前
IDEA部署AI代写插件
java·人工智能·intellij-idea
马剑威(威哥爱编程)31 分钟前
读写锁分离设计模式详解
java·设计模式·java-ee
鸽鸽程序猿32 分钟前
【算法】【优选算法】前缀和(上)
java·算法·前缀和
修道-032332 分钟前
【JAVA】二、设计模式之策略模式
java·设计模式·策略模式
九圣残炎38 分钟前
【从零开始的LeetCode-算法】2559. 统计范围内的元音字符串数
java·算法·leetcode
当归10241 小时前
若依项目-结构解读
java
hlsd#1 小时前
关于 SpringBoot 时间处理的总结
java·spring boot·后端
iiiiiankor1 小时前
C/C++内存管理 | new的机制 | 重载自己的operator new
java·c语言·c++