手寫一個(gè)簡(jiǎn)易版的tomcat

@[TOC]

手寫一個(gè)簡(jiǎn)易版的tomcat

前言

使用tomcat的時(shí)候當(dāng)瀏覽器輸入url之后,開始發(fā)送http請(qǐng)求,這個(gè)請(qǐng)求發(fā)送到哪兒呢,Url解析的過程中

  • 1 先通過域名解析請(qǐng)求得到ip
  • 2 然后通過ip找到對(duì)應(yīng)的主機(jī)
  • 3 再通過響應(yīng)的端口找到進(jìn)程
  • 4 然后再去根據(jù)程序去處理這個(gè)請(qǐng)求,再到原路返回

思考

對(duì)于1,2步驟我們本地測(cè)試可以不用去扣這個(gè),明白這么回事兒就可以,本地localhost實(shí)際上對(duì)應(yīng)我們自己本機(jī)127.0.0.1

對(duì)于第三部,我們本地可以去通過一個(gè)socket去監(jiān)聽響應(yīng)的端口,去獲取到請(qǐng)求,然后再響應(yīng)給客戶端讓客戶端瀏覽器去解析我們返回的http報(bào)文,從而展示數(shù)據(jù);

具體如下步驟:

1)提供服務(wù),接收請(qǐng)求(可以使用Socket通信)
2)請(qǐng)求信息封裝成Request(Response)
3)客戶端請(qǐng)求資源,資源分為靜態(tài)資源(html)和動(dòng)態(tài)資源(Servlet)
4)資源返回給客戶端瀏覽器

具體實(shí)現(xiàn)

首先新建maven工程


在這里插入圖片描述

然后定義一個(gè)啟動(dòng)類Bootstrap然后實(shí)現(xiàn)一個(gè)啟動(dòng)方法start,在這個(gè)方法中啟動(dòng)一個(gè)socket監(jiān)聽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;

/**
 * 啟動(dòng)類入庫(kù)
 * 用于啟動(dòng)tomcat
 */
public class Bootstrap {

    /**
     * 監(jiān)聽端口號(hào)
     * 用于啟動(dòng)socket監(jiān)聽的端口號(hào)
     */
    private int port = 8080;

    /**
     * 啟動(dòng)方法
     */
    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(" ------ 響應(yīng)返回內(nèi)容 : " + 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();
        }
    }
}

通過這個(gè)Socket返回hello world...給客戶端
我們?yōu)g覽器輸入127
可以看到后臺(tái)代碼輸出信息

在這里插入圖片描述

前臺(tái)瀏覽器顯示信息

在這里插入圖片描述

返回信息瀏覽器不認(rèn),響應(yīng)無效,出現(xiàn)這種情況是瀏覽器只認(rèn)識(shí)http報(bào)文,故此需要包裝一個(gè)返回瀏覽器,然后瀏覽器才能解析

新建一個(gè)http包裝類HttpUtil包裝響應(yīng)信息給瀏覽器
這里我們只返回200和404狀態(tài)的

package com.udeam.util;

/**
 * 封裝http響應(yīng)
 */
public class HttpUtil {

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

    /**
     * 添加響應(yīng)頭信息
     * <p>
     * http響應(yīng)體格式
     * <p>
     * 響應(yīng)頭(多參數(shù)空格換行)
     * 換行
     * 響應(yīng)體
     */
    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響應(yīng)
     *
     * @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響應(yīng)
     *
     * @param content 響應(yīng)內(nèi)容
     * @return
     */
    public static String resp_200(String content) {
        return addHeadParam(content.length()) + content;
    }

}

然后再請(qǐng)求,可以看到成功返回信息

在這里插入圖片描述

然后我們?cè)偃フ?qǐng)求一個(gè)靜態(tài)頁(yè)面index.html

新建一個(gè)html頁(yè)面

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

這次前臺(tái)請(qǐng)求Url是http://localhost:8080/index.html
還是從Socket進(jìn)行監(jiān)聽8080端口

請(qǐng)求靜態(tài)的html,那如何去在后臺(tái)找到這個(gè)資源呢?

通過url也就是/index.html去找到這個(gè)文件,后臺(tái)文件我們?nèi)シ诺?code>resource下

那如何獲取url呢?

瀏覽器在請(qǐng)求后臺(tái)的時(shí)候發(fā)送的也是http請(qǐng)求,我們可以獲取http請(qǐng)求報(bào)文
可以看一下,請(qǐng)求報(bào)文


在這里插入圖片描述

從請(qǐng)求頭中獲取到url以及method等
獲取輸入流

    Socket accept = socket.accept();
            //獲取輸入流
            InputStream inputStream = accept.getInputStream();

然后對(duì)輸入流進(jìn)行解析,通過解析http請(qǐng)求頭第一行得到url和method封裝到Request對(duì)象中

/**
 * 封裝的請(qǐng)求實(shí)體類
 */
public class Request {
    /**
     * 請(qǐng)求方式
     */
    private String method;

    /**
     * 請(qǐng)求url
     */
    private String url;

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

    public Request() {
    }

    //構(gòu)造器輸入流
    public Request(InputStream inputStream) throws IOException {
        this.inputStream = inputStream;

        //讀取請(qǐng)求信息,封裝屬性
        int count = 0;
        //讀取請(qǐng)求信息
        while (count == 0) {
            count = inputStream.available();
        }
        byte[] b = new byte[count];
        inputStream.read(b);
        String reqStr = new String(b);
        System.out.println("請(qǐng)求信息 : " + reqStr);
        //根據(jù)http請(qǐng)求報(bào)文 換行符截取
        String[] split = reqStr.split("\\n");

        //獲取第一行請(qǐng)求頭信息
        String s = split[0];
        //根據(jù)空格進(jìn)行截取請(qǐng)求方式和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省略
}

然后根據(jù)請(qǐng)求的url從磁盤找到靜態(tài)資源讀取到然后以流的形式返回給瀏覽器

這里封裝返回對(duì)象Response

public class Response {

    /**
     * 響應(yīng)流
     */
    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();
    }
}

根據(jù)url獲取靜態(tài)資源

    public void outPutHtml(String url) throws IOException {
        //排除瀏覽器的/favicon.ico請(qǐng)求
        if (("/favicon.ico").equals(url)){
            return;
        }
        //獲取靜態(tài)資源的絕對(duì)路徑
        String abPath = ResourUtil.getStaticPath(url);
        //查詢靜態(tài)資源是否存在
        File file = new File(abPath);
        if (file.exists()) {
            //輸出靜態(tài)資源
            ResourUtil.readFile(new FileInputStream(abPath), outputStream);
        } else {
            //404
            try {
                outPutStr(HttpUtil.resp_404());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

ResourUtil 工具類
封裝解析讀取靜態(tài)資源

/**
 * 靜態(tài)資源工具類
 */
public class ResourUtil {

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

    /**
     * 獲取靜態(tài)資源文件路徑
     *
     * @param path
     * @return
     */
    public static String getStaticPath(String path) throws UnsupportedEncodingException {

        //獲取目錄的絕對(duì)路徑
        try {
            String decode = URLDecoder.decode(url.getPath(), "UTF-8");
            String replace1 = decode.replace("\\", "/");
            String replace2 = replace1.replace("http://", "");
            replace2 = replace2.substring(0,replace2.lastIndexOf("/")) + path;
            return replace2;
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 讀取靜態(tài)資源文件輸入流
     *
     * @param inputStream
     */
    public static void readFile(InputStream inputStream, OutputStream outputStream) throws IOException {

        int count = 0;
        //讀取請(qǐng)求信息
        while (count == 0) {
            count = inputStream.available();
        }
        int content = 0;
        //讀取文件
        content = count;


        //輸出頭
        outputStream.write(HttpUtil.addHeadParam(content).getBytes());
        //輸出內(nèi)容
        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中完整請(qǐng)求代碼

    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();
            //封裝請(qǐng)求和響應(yīng)對(duì)象
            Request request = new Request(inputStream);
            Response response = new Response(accept.getOutputStream());
            response.outPutHtml(request.getUrl());
        }

    }

瀏覽器測(cè)試,可以看到正確返回


在這里插入圖片描述

接下來實(shí)現(xiàn)定義請(qǐng)求動(dòng)態(tài)資源,具體實(shí)現(xiàn)在java web中處理一個(gè)請(qǐng)求是使用servlet請(qǐng)求

tomcat處理servlet請(qǐng)求需要實(shí)現(xiàn)servlet規(guī)范

什么是servlet規(guī)范呢?

簡(jiǎn)單來說就是http請(qǐng)求在接收到請(qǐng)求之后將請(qǐng)求交給Servlet容器來處理,Servlet容器通過Servlet接口來調(diào)用不同的業(yè)務(wù)類,這一整套稱作Servlet規(guī)范;

接口規(guī)范

/**
 * 自定義servlet規(guī)范
 */
public interface Servlet {


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

實(shí)現(xiàn)

/**
 * 實(shí)現(xiàn)servlet規(guī)范
 */
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);
        }
    }
}

業(yè)務(wù)請(qǐng)求servlet

/**
 * 業(yè)務(wù)類servelt
 */
public class MyServlet extends HttpServlet {

    @Override
    public void init() throws Exception {
    }

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

        //動(dòng)態(tài)業(yè)務(wù)請(qǐng)求
        String content = "<h2> GET 業(yè)務(wù)請(qǐng)求</h2>";
        try {
            response.outPutStr(HttpUtil.resp_200(content));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void doPost(Request request, Response response) {
        //動(dòng)態(tài)業(yè)務(wù)請(qǐng)求
        String content = "<h2> Post 業(yè)務(wù)請(qǐng)求</h2>";
        try {
            response.outPutStr(HttpUtil.resp_200(content));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    @Override
    public void destory() throws Exception {

    }
}

定義完之后,如何請(qǐng)求呢,如何根據(jù)請(qǐng)求Ur去得到相應(yīng)的servlet
在Java web中我們是在web.xml中進(jìn)行配置,同樣新建web.xml,配置servlet

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

    <!-- v3版本   單線程  多個(gè)請(qǐng)求會(huì)阻塞   -->
    <!-- <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和每一個(gè)servlet對(duì)應(yīng)起來存儲(chǔ)到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("http://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();

                // 根據(jù)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();
        }

    }

啟動(dòng)方法
根據(jù)url找到servlet去執(zhí)行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();
            //封裝請(qǐng)求和響應(yīng)對(duì)象
            Request request = new Request(inputStream);
            Response response = new Response(accept.getOutputStream());
            //靜態(tài)資源
            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 {
                        //處理請(qǐng)求
                        httpServlet.service(request, response);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }

    }

然后請(qǐng)求http://localhost:8080/test可以看到正確返回

在這里插入圖片描述

這里在deget方法中增加睡眠停頓模擬業(yè)務(wù)請(qǐng)求時(shí)間


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

請(qǐng)求可以可以看到請(qǐng)求阻塞,這是因?yàn)橥粋€(gè)socket 當(dāng)前test這個(gè)沒有請(qǐng)求結(jié)束,第二個(gè)請(qǐng)求進(jìn)來然后阻塞;
必須等到第一個(gè)請(qǐng)求結(jié)束后才能處理請(qǐng)求


在這里插入圖片描述

然后再請(qǐng)求index.html


在這里插入圖片描述

發(fā)現(xiàn),并不是靜態(tài)資源并不是立即返回,需要等到test請(qǐng)求結(jié)束后才能返回

故此需要對(duì)這個(gè)進(jìn)行改造,讓彼此請(qǐng)求互不干擾

我們可以使用多線程來進(jìn)行解決,線程互不干擾,每個(gè)請(qǐng)求去執(zhí)行

在Socket中添加方法

    //1 單線程處理
                    MyThread myThread = new MyThread(httpServlet, response, request);
                    new Thread(myThread).start();

線程是寶貴的資源,頻繁創(chuàng)建和銷毀線程對(duì)開銷很大,故此使用線程池來解決



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

   
                 //2 線程池執(zhí)行
                    threadPoolExecutor.submit(myThread);
                    threadPoolExecutor.shutdown();

這樣子就可以立即返回響應(yīng),互不干擾;

簡(jiǎn)易版的tomcat實(shí)現(xiàn)就可以實(shí)現(xiàn)了,代碼的話沒有像tomcat那樣子可以將war包解析之類的..
而且代碼耦合性也大,tomcat和業(yè)務(wù)代碼在一個(gè)Maven中...

說明

分別在指定包下如v1,v2,v3,v4每個(gè)代表一個(gè)版本

  • v1 簡(jiǎn)單的返回指定字符串
  • v2 返回靜態(tài)頁(yè)面
  • v3 單線程處理servelt請(qǐng)求(多個(gè)請(qǐng)求會(huì)阻塞)
  • 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>

代碼地址

倉(cāng)庫(kù)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 大家也可以關(guān)注我的公眾號(hào): 漿果捕鼠草,文章也會(huì)同步更新,當(dāng)然,公眾號(hào)還會(huì)有一些資源可以分享給大家~ 手寫迷你版 ...
    Suremotoo閱讀 1,151評(píng)論 0 1
  • 1.基礎(chǔ) 1.1 web概念 1).軟件架構(gòu) 1.c/s:客戶端/服務(wù)器端 2.b/s:瀏覽器/服務(wù)器端 2) ....
    Cairo_fb29閱讀 491評(píng)論 0 0
  • Tomcat作為Web服務(wù)器深受市場(chǎng)歡迎,有必要對(duì)其進(jìn)行深入的研究。在工作中,我們經(jīng)常會(huì)把寫好的代碼打包放在Tom...
    我叫劉半仙_liugh閱讀 484評(píng)論 1 7
  • 整篇文章分為兩大部分,Tomcat 系統(tǒng)架構(gòu)設(shè)計(jì)和 Tomcat 源碼剖析。 Tomcat系統(tǒng)架構(gòu)設(shè)計(jì) 1.前言 ...
    若兮緣閱讀 14,597評(píng)論 3 58
  • 漸變的面目拼圖要我怎么拼? 我是疲乏了還是投降了? 不是不允許自己墜落, 我沒有滴水不進(jìn)的保護(hù)膜。 就是害怕變得面...
    悶熱當(dāng)乘涼閱讀 4,502評(píng)論 0 13

友情鏈接更多精彩內(nèi)容