手写一个简易版的 Tomcat!

业余草

共 9586字,需浏览 20分钟

 ·

2021-11-23 20:23


前言

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

  • 1 先通过域名解析请求得到ip
  • 2 然后通过ip找到对应的主机
  • 3 再通过响应的端口找到进程
  • 4 然后再去根据程序去处理这个请求,再到原路返回

思考

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

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

具体如下步骤:

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

具体实现

首先新建maven工程

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

404 page... 

"
;

    /**
     * 添加响应头信息
     * 


     * http响应体格式
     * 


     * 响应头(多参数空格换行)
     * 换行
     * 响应体
     */
    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;
    }

}

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

手写tomcat

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

新建一个html页面

html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Titletitle>
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());
    }

}

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

手写tomcat

接下来实现定义请求动态资源,具体实现在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 = "

 GET 业务请求

"
;
        try {
            response.outPutStr(HttpUtil.resp_200(content));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void doPost(Request request, Response response) {
        //动态业务请求
        String content = "

 Post 业务请求

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


<web-app>

    
    

    
    <servlet>
        <servlet-name>testservlet-name>
        <servlet-class>com.udeam.v4.MyServletservlet-class>
    servlet>

    <servlet-mapping>
        <servlet-name>testservlet-name>
        <url-pattern>/testurl-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 selectNodes = rootElement.selectNodes("//servlet");
        for (int i = 0; i < selectNodes.size(); i++) {
            Element element =  selectNodes.get(i);
            // test
            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 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可以看到正确返回

get请求

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

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

请求可以可以看到请求阻塞,这是因为同一个socket 当前test这个没有请求结束,第二个请求进来然后阻塞。

必须等到第一个请求结束后才能处理请求。

手撸tomcat

然后再请求index.html

tomcat的http请求测试

发现,并不是静态资源并不是立即返回,需要等到test请求结束后才能返回

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

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

在Socket中添加方法

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

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

/**
* 参数可以配置在xml里面
*/

private static final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10201, TimeUnit.HOURS, new ArrayBlockingQueue<>(500));


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

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

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

说明

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

  • v1 简单的返回指定字符串
  • v2 返回静态页面
  • v3 单线程处理servelt请求(多个请求会阻塞)
  • v4 多线程处理

其中需要用到解析xml依赖

<dependencies>
    <dependency>
        <groupId>dom4jgroupId>
        <artifactId>dom4jartifactId>
        <version>1.6.1version>
    dependency>
    <dependency>
        <groupId>jaxengroupId>
        <artifactId>jaxenartifactId>
        <version>1.1.6version>
    dependency>
dependencies>

至此,一个简单的 tomcat 就撸完了,如需本文源码,请加我微信:xttblog2,免费发送!

浏览 42
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报