手动实现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.结果展示
相关推荐
chuanauc23 分钟前
Kubernets K8s 学习
java·学习·kubernetes
一头生产的驴39 分钟前
java整合itext pdf实现自定义PDF文件格式导出
java·spring boot·pdf·itextpdf
YuTaoShao1 小时前
【LeetCode 热题 100】73. 矩阵置零——(解法二)空间复杂度 O(1)
java·算法·leetcode·矩阵
zzywxc7871 小时前
AI 正在深度重构软件开发的底层逻辑和全生命周期,从技术演进、流程重构和未来趋势三个维度进行系统性分析
java·大数据·开发语言·人工智能·spring
YuTaoShao3 小时前
【LeetCode 热题 100】56. 合并区间——排序+遍历
java·算法·leetcode·职场和发展
程序员张33 小时前
SpringBoot计时一次请求耗时
java·spring boot·后端
llwszx6 小时前
深入理解Java锁原理(一):偏向锁的设计原理与性能优化
java·spring··偏向锁
云泽野7 小时前
【Java|集合类】list遍历的6种方式
java·python·list
二进制person7 小时前
Java SE--方法的使用
java·开发语言·算法
小阳拱白菜8 小时前
java异常学习
java