手写一个简易版的tomcat

2020-12-13  本文已影响0人  喊我小王吧

@[TOC]

手写一个简易版的tomcat

前言

使用tomcat的时候当浏览器输入url之后,开始发送http请求,这个请求发送到哪儿呢,Url解析的过程中

思考

对于1,2步骤我们本地测试可以不用去扣这个,明白这么回事儿就可以,本地localhost实际上对应我们自己本机127.0.0.1

对于第三部,我们本地可以去通过一个socket去监听响应的端口,去获取到请求,然后再响应给客户端让客户端浏览器去解析我们返回的http报文,从而展示数据;

具体如下步骤:

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

具体实现

首先新建maven工程


在这里插入图片描述

然后定义一个启动类Bootstrap然后实现一个启动方法start,在这个方法中启动一个socket监听8080端口

package com.udeam.v1;

import com.udeam.util.HttpUtil;

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

/**
 * 启动类入库
 * 用于启动tomcat
 */
public class Bootstrap {

    /**
     * 监听端口号
     * 用于启动socket监听的端口号
     */
    private int port = 8080;

    /**
     * 启动方法
     */
    public void start() throws IOException {
        //返回固定字符串到客户端
        ServerSocket socket = new ServerSocket(port);
        System.out.println("--------- start port : " + port);
        while (true) {
            Socket accept = socket.accept();
            //获取输入流
            //InputStream inputStream = accept.getInputStream();
            //输出流
            OutputStream outputStream = accept.getOutputStream();
            System.out.println(" ------ 响应返回内容 : " + result);
            outputStream.write("hello world ...".getBytes());
            outputStream.flush();
            outputStream.close();
            socket.close();
        }
    }

    public static void main(String[] args) {
        try {
            new Bootstrap().start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

通过这个Socket返回hello world...给客户端
我们浏览器输入127
可以看到后台代码输出信息

在这里插入图片描述

前台浏览器显示信息

在这里插入图片描述

返回信息浏览器不认,响应无效,出现这种情况是浏览器只认识http报文,故此需要包装一个返回浏览器,然后浏览器才能解析

新建一个http包装类HttpUtil包装响应信息给浏览器
这里我们只返回200和404状态的

package com.udeam.util;

/**
 * 封装http响应
 */
public class HttpUtil {

    /**
     * 404 page
     */
    private static final String content = "<H2>404 page... </H2>";

    /**
     * 添加响应头信息
     * <p>
     * http响应体格式
     * <p>
     * 响应头(多参数空格换行)
     * 换行
     * 响应体
     */
    public static String addHeadParam(int len) {
        String head = "HTTP/1.1 200 OK \n";
        head += "Content-Type: text/html; charset=UTF-8 \n";
        head += "Content-Length: " + len + " \n" + "\r\n";
        return head;
    }

    /**
     * 4040响应
     *
     * @return
     */
    public static String resp_404() {
        String head = "HTTP/1.1 404 not  found \n";
        head += "Content-Type: text/html; charset=UTF-8 \n";
        head += "Content-Length: " + content.length() + " \n" + "\r\n";
        return head + content;
    }

    /**
     * 200响应
     *
     * @param content 响应内容
     * @return
     */
    public static String resp_200(String content) {
        return addHeadParam(content.length()) + content;
    }

}

然后再请求,可以看到成功返回信息

在这里插入图片描述

然后我们再去请求一个静态页面index.html

新建一个html页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
hello tomcat....
</body>
</html>

这次前台请求Url是http://localhost:8080/index.html
还是从Socket进行监听8080端口

请求静态的html,那如何去在后台找到这个资源呢?

通过url也就是/index.html去找到这个文件,后台文件我们去放到resource

那如何获取url呢?

浏览器在请求后台的时候发送的也是http请求,我们可以获取http请求报文
可以看一下,请求报文


在这里插入图片描述

从请求头中获取到url以及method等
获取输入流

    Socket accept = socket.accept();
            //获取输入流
            InputStream inputStream = accept.getInputStream();

然后对输入流进行解析,通过解析http请求头第一行得到url和method封装到Request对象中

/**
 * 封装的请求实体类
 */
public class Request {
    /**
     * 请求方式
     */
    private String method;

    /**
     * 请求url
     */
    private String url;

    /**
     * 输入流
     */
    public InputStream inputStream;

    public Request() {
    }

    //构造器输入流
    public Request(InputStream inputStream) throws IOException {
        this.inputStream = inputStream;

        //读取请求信息,封装属性
        int count = 0;
        //读取请求信息
        while (count == 0) {
            count = inputStream.available();
        }
        byte[] b = new byte[count];
        inputStream.read(b);
        String reqStr = new String(b);
        System.out.println("请求信息 : " + reqStr);
        //根据http请求报文 换行符截取
        String[] split = reqStr.split("\\n");

        //获取第一行请求头信息
        String s = split[0];
        //根据空格进行截取请求方式和url
        String[] s1 = s.split(" ");
        System.out.println("method : " + s1[0]);
        System.out.println("url : " + s1[1]);

        this.method = s1[0];
        this.url = s1[1];

    }

 //.... get  set省略
}

然后根据请求的url从磁盘找到静态资源读取到然后以流的形式返回给浏览器

这里封装返回对象Response

public class Response {

    /**
     * 响应流
     */
    private OutputStream outputStream;

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

    //输出指定字符串
    public void outPutStr(String content) throws IOException {
        outputStream.write(content.getBytes());
        outputStream.flush();
        outputStream.close();
    }
}

根据url获取静态资源

    public void outPutHtml(String url) throws IOException {
        //排除浏览器的/favicon.ico请求
        if (("/favicon.ico").equals(url)){
            return;
        }
        //获取静态资源的绝对路径
        String abPath = ResourUtil.getStaticPath(url);
        //查询静态资源是否存在
        File file = new File(abPath);
        if (file.exists()) {
            //输出静态资源
            ResourUtil.readFile(new FileInputStream(abPath), outputStream);
        } else {
            //404
            try {
                outPutStr(HttpUtil.resp_404());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

ResourUtil 工具类
封装解析读取静态资源

/**
 * 静态资源工具类
 */
public class ResourUtil {

    /**
     * 获取classes文件目录
     */
    private static URL url = ResourUtil.class.getClassLoader().getResource("\\\\");

    /**
     * 获取静态资源文件路径
     *
     * @param path
     * @return
     */
    public static String getStaticPath(String path) throws UnsupportedEncodingException {

        //获取目录的绝对路径
        try {
            String decode = URLDecoder.decode(url.getPath(), "UTF-8");
            String replace1 = decode.replace("\\", "/");
            String replace2 = replace1.replace("//", "");
            replace2 = replace2.substring(0,replace2.lastIndexOf("/")) + path;
            return replace2;
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 读取静态资源文件输入流
     *
     * @param inputStream
     */
    public static void readFile(InputStream inputStream, OutputStream outputStream) throws IOException {

        int count = 0;
        //读取请求信息
        while (count == 0) {
            count = inputStream.available();
        }
        int content = 0;
        //读取文件
        content = count;


        //输出头
        outputStream.write(HttpUtil.addHeadParam(content).getBytes());
        //输出内容
        long written = 0;
        int byteSize = 1024;
        byte[] b = new byte[byteSize];
        //读取
        while (written < content) {
            if (written + 1024 > content) {
                byteSize = (int) (content - written);
                b = new byte[byteSize];
            }
            inputStream.read(b);
            outputStream.write(b);
            outputStream.flush();
            written += byteSize;
        }
    }
}

socket中完整请求代码

    public void start() throws IOException {
        //返回固定字符串到客户端
        ServerSocket socket = new ServerSocket(port);
        System.out.println("--------- start port : " + port);

        while (true) {
            Socket accept = socket.accept();
            //获取输入流
            InputStream inputStream = accept.getInputStream();
            //封装请求和响应对象
            Request request = new Request(inputStream);
            Response response = new Response(accept.getOutputStream());
            response.outPutHtml(request.getUrl());
        }

    }

浏览器测试,可以看到正确返回


在这里插入图片描述

接下来实现定义请求动态资源,具体实现在java web中处理一个请求是使用servlet请求

tomcat处理servlet请求需要实现servlet规范

什么是servlet规范呢?

简单来说就是http请求在接收到请求之后将请求交给Servlet容器来处理,Servlet容器通过Servlet接口来调用不同的业务类,这一整套称作Servlet规范;

接口规范

/**
 * 自定义servlet规范
 */
public interface Servlet {


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

实现

/**
 * 实现servlet规范
 */
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 ("GET".equalsIgnoreCase( request.getMethod()
        )) {
            doGet(request, response);
        } else {
            doPost(request, response);
        }
    }
}

业务请求servlet

/**
 * 业务类servelt
 */
public class MyServlet extends HttpServlet {

    @Override
    public void init() throws Exception {
    }

    @Override
    public void doGet(Request request, Response response) {

        //动态业务请求
        String content = "<h2> GET 业务请求</h2>";
        try {
            response.outPutStr(HttpUtil.resp_200(content));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void doPost(Request request, Response response) {
        //动态业务请求
        String content = "<h2> Post 业务请求</h2>";
        try {
            response.outPutStr(HttpUtil.resp_200(content));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    @Override
    public void destory() throws Exception {

    }
}

定义完之后,如何请求呢,如何根据请求Ur去得到相应的servlet
在Java web中我们是在web.xml中进行配置,同样新建web.xml,配置servlet

<?xml version="1.0" encoding="UTF-8" ?>
<web-app>

    <!-- v3版本   单线程  多个请求会阻塞   -->
    <!-- <servlet>
         <servlet-name>test</servlet-name>
         <servlet-class>com.udeam.v3.service.MyServlet</servlet-class>
     </servlet>-->


    <!--   v4版本 多线程 不阻塞-->
    <servlet>
        <servlet-name>test</servlet-name>
        <servlet-class>com.udeam.v4.MyServlet</servlet-class>
    </servlet>


    <servlet-mapping>
        <servlet-name>test</servlet-name>
        <url-pattern>/test</url-pattern>
    </servlet-mapping>

</web-app>

解析web.xml文件

讲url和每一个servlet对应起来存储到map中

   /**
     * 加载解析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>test</servlet-name>
                Element servletnameElement = (Element) element.selectSingleNode("servlet-name");
                String servletName = servletnameElement.getStringValue();
                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 + "']");
                // /test
                String urlPattern = servletMapping.selectSingleNode("url-pattern").getStringValue();
                servletMap.put(urlPattern, (HttpServlet) Class.forName(servletClass).newInstance());

            }

        } catch (DocumentException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

    }

启动方法
根据url找到servlet去执行service方法


    private static final Map<String, HttpServlet> servletMap = new HashMap<>();
    
    public void start() throws IOException {

        ServerSocket socket = new ServerSocket(port);
        System.out.println("--------- start port : " + port);

        while (true) {
            Socket accept = socket.accept();
            //获取输入流
            InputStream inputStream = accept.getInputStream();
            //封装请求和响应对象
            Request request = new Request(inputStream);
            Response response = new Response(accept.getOutputStream());
            //静态资源
            if (request.getUrl().contains(".html")) {
                response.outPutHtml(request.getUrl());
            } else {
                if (!servletMap.containsKey(request.getUrl())) {
                    response.outPutStr(HttpUtil.resp_200(request.getUrl() + " is not found ... "));
                } else {
                    HttpServlet httpServlet = servletMap.get(request.getUrl());
                    try {
                        //处理请求
                        httpServlet.service(request, response);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }

    }

然后请求http://localhost:8080/test可以看到正确返回

在这里插入图片描述

这里在deget方法中增加睡眠停顿模拟业务请求时间


  try {
            Thread.sleep(10_000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

请求可以可以看到请求阻塞,这是因为同一个socket 当前test这个没有请求结束,第二个请求进来然后阻塞;
必须等到第一个请求结束后才能处理请求


在这里插入图片描述

然后再请求index.html


在这里插入图片描述
发现,并不是静态资源并不是立即返回,需要等到test请求结束后才能返回

故此需要对这个进行改造,让彼此请求互不干扰

我们可以使用多线程来进行解决,线程互不干扰,每个请求去执行

在Socket中添加方法

    //1 单线程处理
                    MyThread myThread = new MyThread(httpServlet, response, request);
                    new Thread(myThread).start();

线程是宝贵的资源,频繁创建和销毁线程对开销很大,故此使用线程池来解决



    /**
     * 参数可以配置在xml里面
     */
    private static final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 20, 1, TimeUnit.HOURS, new ArrayBlockingQueue<>(500));

   
                 //2 线程池执行
                    threadPoolExecutor.submit(myThread);
                    threadPoolExecutor.shutdown();

这样子就可以立即返回响应,互不干扰;

简易版的tomcat实现就可以实现了,代码的话没有像tomcat那样子可以将war包解析之类的..
而且代码耦合性也大,tomcat和业务代码在一个Maven中...

说明

分别在指定包下如v1,v2,v3,v4每个代表一个版本

其中需要用到解析xml依赖


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

代码地址

仓库

上一篇下一篇

猜你喜欢

热点阅读