Tomcat学习笔记(三)-手写一个简易版本的Tomcat

Scroll Down

Tomcat学习笔记(三)-手写一个简易版本的Tomcat

名称:Minicat

Minicat要做的事情:作为⼀个服务器软件提供服务的,也即我们可以通过浏览器客户端发送http请求,

Minicat可以接收到请求进⾏处理,处理之后的结果可以返回浏览器客户端。

  1. 提供服务,接收请求(Socket通信)
  2. 请求信息封装成Request对象(Response对象)
  3. 客户端请求资源,资源分为静态资源(html)和动态资源(Servlet)
  4. 资源返回给客户端浏览器

我们递进式完成以上需求,提出V1.0、V2.0、V3.0版本的需求

V1.0需求:浏览器请求http://localhost:8080,返回⼀个固定的字符串到⻚⾯"Hello Minicat!"

V2.0需求:封装Request和Response对象,返回html静态资源⽂件

V3.0需求:可以请求动态资源(Servlet)

Tomcat 1.0 开发

Tomcat 1.0基础开发

这里我们需要先使用Socket监听8080端口,当有请求访问时,默认就返回一个固定的字符串到前端

第一步,创建一个BootstarpV1类,用于启动Tomcat

public class BootstrapV1 {
    /**
     * 默认监听8080端口
     */
    private int port = 8080;
    
   public void start() throws Exception {
        //监听
        ServerSocket serverSocket = new ServerSocket(port);
        System.out.println("启动Minicat1.0成功,监听端口为:"+port);
        while(true){
            try(Socket socket = serverSocket.accept()) {
                OutputStream outputStream = socket.getOutputStream();
                System.out.println("收到访问请求!");
                String result = "Hello Minicat";
                //增加返回请求头的封装信息
                outputStream.write(getHttpHeader200(result.length()).getBytes());
                outputStream.write(result.getBytes());
                System.out.println("返回结果!");
            }
        }
    }
    
    public static void main(String[] args) throws Exception {
        BootstrapV1 bootstrap = new BootstrapV1();
        bootstrap.start();
    }
}

这里进行监听8080端口请求,监听到请求之后,正常需要将Hello Minicat输出到浏览器,但是这里直接访问的话,会出现如下错误

image-20200608162713363

而我们看到后台,其实是有接到请求的,那么就证明是我们返回格式的数据,不符合浏览器的规范导致的,需要对返回值参数进行封装,使得浏览器能够正常解析该数据

image-20200608162801504

Tomcat 1.0 返回参数封装

我们访问www.baidu.com网址,查看一下浏览器请求的过程,这里看到我们与后端的请求时,返回需要加上一个相应的请求头,这样浏览器才能解析具体的请求体

image-20200608163837500

我们这里针对一些必输的参数进行返回的请求头封装,新建一个HttpProtocolUtil类,用于对基础的返回请求头进行封装

public class HttpProtocolUtil {
    /**
     * 为响应码200提供请求头信息
     * @return
     */
    public static String getHttpHeader200(int contentLength) {
        return "HTTP/1.1 200 OK \n" +
                "Content-Type: text/html \n" +
                "Content-Length: " + contentLength + " \n" +
                "\r\n";
    }

    /**
     * 为响应码404提供请求头信息(此处也包含了数据内容)
     * @return
     */
    public static String getHttpHeader404() {
        String str404 = "<h1>404 not found</h1>";
        return "HTTP/1.1 404 NOT Found \n" +
                "Content-Type: text/html \n" +
                "Content-Length: " + str404.getBytes().length + " \n" +
                "\r\n" + str404;
    }
}

这时候修改原有代码,增加上返回头的代码

public void start() throws Exception {
        //监听
        ServerSocket serverSocket = new ServerSocket(port);
        System.out.println("启动Minicat1.0成功,监听端口为:"+port);
        while(true){
            Socket socket = serverSocket.accept();
            OutputStream outputStream = socket.getOutputStream();
            System.out.println("收到访问请求!");
            String result = "Hello Minicat";
            //增加返回请求头的封装信息
            outputStream.write(getHttpHeader200(result.length()).getBytes());
            outputStream.write(result.getBytes());
            System.out.println("返回结果!");
            socket.close();
        }
    }

此时返回结果正常

image-20200608164206534

这样,1.0的需求我们就完成了,下面我们开始做2.0的需求

Tomcat 2.0开发

封装Request和Response类

需要完成需求封装Request和Response对象,返回html静态资源⽂件

首先分析相对于静态的1.0Tomcat,2.0的Tomcat需要对浏览器的请求进行解析,需要知道请求的是哪个文件,然后我们后端才能找到对应的文件,将其返回,这里我们先访问http://localhost:8080/index.html,可以看到请求头信息上一些关键的数据

image-20200608164825964

GET /index.html HTTP/1.1

请求类型 请求文件地址 协议

我们这里需要对请求类型与静态资源地址进行封装,其他信息就暂时不做处理

首先建立Request类与Response类

public enum HttpMethod {
    /**
     * get请求
     */
    GET("get"),
    /**
     * post请求
     */
    POST("post");
    private String value;
    HttpMethod(String value) {
        this.value = value;
    }
}
public class Request {
    /**
     * 方法类型
     */
    private HttpMethod method;
    /**
     * url
     */
    private String url;
    /**
     * 请求的输入流
     */
    private InputStream inputStream;
    /**
     * 根据输入流进行初始化设置
     * @param inputStream
     */
    public Request(InputStream inputStream) throws IOException {
        this.inputStream = inputStream;
        // 从输入流中获取请求信息
        int count = 0;
        while (count == 0) {
            count = inputStream.available();
        }
        byte[] bytes = new byte[count];
        inputStream.read(bytes);
        //将请求信息转为字符串
        String requestValue = new String(bytes);
        //按照换行符来切割,只获取第一行信息即可
        String[] requestValues = requestValue.split("\n");
        //将第一行信息按照空格来进行切割,获取前两个参数
        String[] httpValue = requestValues[0].split(" ");
        String method = httpValue[0];
        String url = httpValue[1];
        this.method = HttpMethod.valueOf(method);
        this.url = url;
    }
}
public class Response {
    private OutputStream outputStream;
    public Response(OutputStream outputStream) {
        this.outputStream = outputStream;
    }
    public void outPutStaticFile(String path) throws Exception {
        // 获取静态资源文件的绝对路径
        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());
        }
    }

    private void output(String httpHeader404) throws IOException {
        outputStream.write(httpHeader404.getBytes());
    }
}

StaticResourceUtil工具类

public class StaticResourceUtil {

    /**
     * 获取静态资源文件的绝对路径
     * @param path
     * @return
     */
    public static String getAbsolutePath(String path) {
        //获取静态文件地址
        String absolutePath = StaticResourceUtil.class.getResource("/").getPath();
        return absolutePath.replaceAll("\\\\", File.separator) + path;
    }

    /**
     * 将静态文件从输入流中读取到输出流
     * @param fileInputStream
     * @param outputStream
     * @throws IOException
     */
    public static void outputStaticResource(FileInputStream fileInputStream, OutputStream outputStream) throws IOException {
        int  resourceSize= fileInputStream.available();
        // 输出http请求头,然后再输出具体内容
        outputStream.write(HttpProtocolUtil.getHttpHeader200(resourceSize).getBytes());
        //将文件中读取到的字节返回到前台
        byte[] bytes = fileInputStream.readAllBytes();
        outputStream.write(bytes);
    }
}

Tomcat 2.0启动类编写

public class BootstrapV2 {
    /**
     * 默认监听8080端口
     */
    private int port = 8080;
    
    public void start() throws Exception {
        //监听
        ServerSocket serverSocket = new ServerSocket(port);
        System.out.println("启动Minicat2.0成功,监听端口为:"+port);
        while(true){
            try(Socket socket = serverSocket.accept()) {
                System.out.println("收到访问请求!");
                InputStream inputStream = socket.getInputStream();
                Request request = new Request(inputStream);
                Response response = new Response(socket.getOutputStream());
                String url = request.getUrl();
                response.outPutStaticFile(url);
                System.out.println("返回结果!");
                socket.close();
            }
        }
    }
    
    public static void main(String[] args) throws Exception {
        BootstrapV2 bootstrap = new BootstrapV2();
        bootstrap.start();
    }
}

Tomcat 2.0测试

这里我们在resources文件夹下面增加一个index.html页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>static resouce</title>
</head>
<body>
Hello Minicat-静态资源文件访问!
</body>
</html>

启动Tomcat 2.0,看看能否达到我们预期的效果

访问:http://localhost:8080/index.html
image-20200608172839259

然后访问一下其他不存在的页面,看能否正常输出404:

image-20200608173923744

现在2.0的开发完成了,下面我们开发3.0的Tomcat

Tomcat 3.0开发

3.0需求:可以请求动态资源(Servlet)

那么我们的解决思路是,首先需要声明一个Servlet接口,然后新增一个web.xml配置文件,启动Tomcat3.0的时候就将web.xml中的配置的servlet加入到缓存当中,每次发起请求的时候,使用缓存中的servlet进行处理,匹配不到servlet就去匹配相应的静态文件,如果都匹配不上就报错

Tomcat3.0 基础开发

新增Servlet基类

public interface Servlet {

    void init() throws Exception;

    void destory() throws Exception;

    void service(Request request,Response response) throws Exception;
}

新增HttpServlet实现类

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(HttpMethod.GET.equals(request.getMethod())) {
            doGet(request,response);
        }else{
            doPost(request,response);
        }
    }
}

新增需要测试的类LagouServlet

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

    @Override
    public void doPost(Request request, Response response) {
        String content = "<h1>LagouServlet post</h1>";
        try {
            response.output((HttpProtocolUtil.getHttpHeader200(content.getBytes().length) + content));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void init() throws Exception {

    }

    @Override
    public void destory() throws Exception {

    }
}

新增web.xml配置文件并进行解析

在resources下面增加web.xml配置文件

<?xml version="1.0" encoding="UTF-8" ?>
<web-app>
    <servlet>
        <servlet-name>lagou</servlet-name>
        <servlet-class>server.LagouServlet</servlet-class>
    </servlet>


    <servlet-mapping>
        <servlet-name>lagou</servlet-name>
        <url-pattern>/lagou</url-pattern>
    </servlet-mapping>
</web-app>

pom.xml文件增加dom4j依赖

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

新建一个BootstrapV3

启动的时候读取配置文件进行初始化Servlet缓存

public class BootstrapV3 {
    /**
     * 默认监听8080端口
     */
    private int port = 8080;
    private Map<String,HttpServlet> servletMap = new HashMap<String,HttpServlet>();
    
    public void start() throws Exception {
        loadServlet();
        //监听
        ServerSocket serverSocket = new ServerSocket(port);
        System.out.println("启动Minicat2.0成功,监听端口为:"+port);
        while(true){
            try(Socket socket = serverSocket.accept()) {
                System.out.println("收到访问请求!");
                InputStream inputStream = socket.getInputStream();
                Request request = new Request(inputStream);
                Response response = new Response(socket.getOutputStream());
                String url = request.getUrl();
                // 静态资源处理
                if(servletMap.get(url) == null) {
                    response.outPutStaticFile(request.getUrl());
                }else{
                    // 动态资源servlet请求
                    HttpServlet httpServlet = servletMap.get(request.getUrl());
                    httpServlet.service(request,response);
                }
                System.out.println("返回结果!");
                socket.close();
            }
        }
    }
    /**
     * 加载解析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>lagou</servlet-name>
                Element servletnameElement = (Element) element.selectSingleNode("servlet-name");
                String servletName = servletnameElement.getStringValue();
                // <servlet-class>server.LagouServlet</servlet-class>
                Element servletclassElement = (Element) element.selectSingleNode("servlet-class");
                String servletClass = servletclassElement.getStringValue();
                // 根据servlet-name的值找到url-pattern
                Element servletMapping = (Element) rootElement.selectSingleNode("/web-app/servlet-mapping[servlet-name='" + servletName + "']");
                // /lagou
                String urlPattern = servletMapping.selectSingleNode("url-pattern").getStringValue();
                servletMap.put(urlPattern, (HttpServlet) Class.forName(servletClass).newInstance());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) throws Exception {
        BootstrapV3 bootstrap = new BootstrapV3();
        bootstrap.start();
    }
}

Tomcat3.0测试

访问http://localhost:8080/lagou,得到正确的结果

image-20200608175400515

Tomcat 3.0 使用线程池进行优化访问请求

这里如果我们一个请求速度很慢的话,就会直接导致整个线程阻塞,下面对线程阻塞进行测试

在servlet代码中增加线程睡眠时间

image-20200608175937472

这里发现前端请求这个servlet时,无法再请求静态页面或者其他内容了

http://localhost:8080/lagou的请求阻塞,导致我们无法请求到http://localhost:8080/index.html

image-20200608180058873

我们这里使用线程池对其进行异步处理,接收到请求使用使用子线程去处理具体的请求

这里继续请求http://localhost:8080/lagou,发现一直在等待

image-20200608180720831

同时访问静态文件,http://localhost:8080/index.html,发现可以成功进入,证明使用线程池优化是可以达到同时处理多个请求的目的

image-20200608180753357