美文网首页
手写一个简易版的tomcat

手写一个简易版的tomcat

作者: 喊我小王吧 | 来源:发表于2020-12-13 23:55 被阅读0次

    @[TOC]

    手写一个简易版的tomcat

    前言

    使用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工程


    在这里插入图片描述

    然后定义一个启动类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每个代表一个版本

    • v1 简单的返回指定字符串
    • v2 返回静态页面
    • v3 单线程处理servelt请求(多个请求会阻塞)
    • 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>
    

    代码地址

    仓库

    相关文章

      网友评论

          本文标题:手写一个简易版的tomcat

          本文链接:https://www.haomeiwen.com/subject/duymgktx.html