我的架構夢:(十六)手寫Tomcat服務器

經過前面兩篇的分析,我們對Tomcat的系統架構與原理有了一定的認識與理解了,回顧請戳:

我的架構夢:(十四)Tomcat 系統架構與原理剖析
我的架構夢:(十五)Tomcat 服務器核心配置詳解

一、需求分析

名稱:tomcat-customize

tomcat-customize要做的事情:作爲一個服務器軟件提供服務的,也即我們可以通過瀏覽器客戶端發送http請求, tomcat-customize可以接收到請求進行處理,處理之後的結果可以返回瀏覽器客戶端。

1、提供服務,接收請求(Socket通信);
2、請求信息封裝成Request對象(Response對象);
3、客戶端請求資源,資源分爲靜態資源(html)和動態資源(Servlet);
4、資源返回給客戶端瀏覽器。

我們遞進式完成以上需求,提出V1.0、V2.0、V3.0、V4.0、V5.0版本的需求。

  • V1.0需求:瀏覽器請求http://localhost:8080,返回一個固定的字符串到⻚面Hello Tomcat Customize!
  • V2.0需求:封裝RequestResponse對象,返回html靜態資源文件;
  • V3.0需求:可以請求動態資源(Servlet);
  • V4.0需求:多線程改造(其他請求不受請求阻塞影響);
  • V5.0需求:線程池改造(高併發場景);

完成上述五個版本後,我們的代碼如下:

二、代碼詳情

1、Bootstrap 啓動類

/**
 * 自定義Tomcat的主類
 */
@Data
public class Bootstrap {

    /**
     * 定義socket監聽的端口號
     */
    private int port = 8080;

    /**
     * 自定義Tomcat啓動需要初始化展開的一些操作
     */
    public void start() throws Exception {

        // 加載解析相關的配置,web.xml
        loadServlet();

        // 定義一個線程池
        int corePoolSize = 10;
        int maximumPoolSize =50;
        long keepAliveTime = 100L;
        TimeUnit unit = TimeUnit.SECONDS;
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(50);
        ThreadFactory threadFactory = Executors.defaultThreadFactory();
        RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();

        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                corePoolSize,
                maximumPoolSize,
                keepAliveTime,
                unit,
                workQueue,
                threadFactory,
                handler
        );

        /**
         * 自定義Tomcat 1.0版本
         * 需求:瀏覽器請求http://localhost:8080,返回一個固定的字符串到頁面"Hello Tomcat Customize!"
         */
        ServerSocket serverSocket = new ServerSocket(port);
        System.out.println("Tomcat Customize start on port: " + port);

        /*while (true) {
            Socket socket = serverSocket.accept();
            // 有了socket,接收到請求,獲取輸出流
            OutputStream outputStream = socket.getOutputStream();
            String data = "Hello Tomcat Customize!";
            String responseText = HttpProtocolUtil.getHttpHeader200(data.getBytes().length) + data;
            outputStream.write(responseText.getBytes());
            socket.close();
        }*/

        /**
         * 自定義Tomcat 2.0版本
         * 需求:封裝Request和Response對象,返回html靜態資源文件
         */
        /*while (true) {
            Socket socket = serverSocket.accept();
            InputStream inputStream = socket.getInputStream();

            // 封裝Request對象和Response對象
            Request request = new Request(inputStream);
            Response response = new Response(socket.getOutputStream());

            response.outputHtml(request.getUrl());
            socket.close();
        }*/

        /**
         * 自定義Tomcat 3.0版本
         * 需求:可以請求動態資源(Servlet)
         */
        /*while (true) {
            Socket socket = serverSocket.accept();
            InputStream inputStream = socket.getInputStream();

            // 封裝Request對象和Response對象
            Request request = new Request(inputStream);
            Response response = new Response(socket.getOutputStream());

            HttpServlet httpServlet = servletMap.get(request.getUrl());
            // 靜態資源處理
            if (httpServlet == null) {
                response.outputHtml(request.getUrl());
            } else {
                // 動態資源servlet請求
                httpServlet.service(request, response);
            }

            socket.close();
        }*/

        /**
         * 自定義Tomcat 4.0版本
         * 需求:多線程改造(其他請求不受請求阻塞影響)
         */
        /*while (true) {
            Socket socket = serverSocket.accept();
            RequestProcessor requestProcessor = new RequestProcessor(socket, servletMap);
            requestProcessor.start();
        }*/

        /**
         * 自定義Tomcat 5.0版本
         * 需求:線程池改造
         */
        while (true) {
            Socket socket = serverSocket.accept();
            RequestProcessor requestProcessor = new RequestProcessor(socket, servletMap);
            threadPoolExecutor.execute(requestProcessor);
        }

    }

    private Map<String, HttpServlet> servletMap = new HashMap<>();

    /**
     * 加載解析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>riemann</servlet-name>
                Element servletnameElement = (Element) element.selectSingleNode("servlet-name");
                String servletName = servletnameElement.getStringValue();

                // <servlet-class>com.riemann.server.servlet.RiemannServlet</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 + "']");
                // /riemann
                String urlPattern = servletMapping.selectSingleNode("url-pattern").getStringValue();
                servletMap.put(urlPattern, (HttpServlet) Class.forName(servletClass).newInstance());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 自定義Tomcat的程序啓動入口
     * @param args
     */
    public static void main(String[] args) {
        Bootstrap bootstrap = new Bootstrap();
        try {
            // 啓動自定義Tomcat
            bootstrap.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

2、Http協議工具類

/**
 * http協議工具類,主要是提供響應頭信息,這裏我們只提供200和404的情況
 */
public class HttpProtocolUtil {

    /**
     * 爲響應碼200提供請求頭信息
     * @param contentLength
     * @return
     */
    public static String getHttpHeader200(long 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;
    }

}

3、Request、 Response封裝類

/**
 * 把請求信息封裝爲Request對象(根據InputSteam輸入流封裝)
 */
@Data
public class Request {

    private String method; // 請求方式,比如GET/POST

    private String url;  // 例如 /,/index.html

    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 inputStr = new String(bytes);
        // 獲取第一行請求頭信息
        String firstLine = inputStr.split("\\n")[0]; // GET / HTTP/1.1
        String[] strings = firstLine.split(" ");
        this.method = strings[0];
        this.url = strings[1];

        System.out.println("method: " + method);
        System.out.println("url: " + url);

    }
    
}
/**
 * 封裝Response對象,需要依賴於OutputStream
 *
 * 該對象需要提供核心方法,輸出html
 */
@Data
@AllArgsConstructor
public class Response {

    private OutputStream outputStream;

    /**
     * 使用輸出流輸出指定字符串
     * @param content
     * @throws IOException
     */
    public void output(String content) throws IOException {
        outputStream.write(content.getBytes());
    }

    /**
     * 輸出html格式
     * @param path url 隨後要根據url來獲取到靜態資源的絕對路徑,進一步根據絕對路徑讀取該靜態資源文件,最終通過輸出流輸出。
     *                 / -----> classes
     * @throws IOException
     */
    public void outputHtml(String path) throws IOException {
        // 獲取靜態資源文件的絕對路徑
        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());
        }
    }

}

4、靜態資源請求處理工具類

/**
 * 靜態資源工具類
 */
public class StaticResourceUtil {

    /**
     * 獲取靜態資源文件的絕對路徑
     * @param path
     * @return
     */
    public static String getAbsolutePath(String path) {
        String absolutePath = StaticResourceUtil.class.getResource("/").getPath();
        return absolutePath.replaceAll("\\\\", "/") + path;
    }

    /**
     * 讀取靜態資源文件輸入流,通過輸出流輸出
     * @param inputStream
     * @param outputStream
     * @throws IOException
     */
    public static void outputStaticResource(InputStream inputStream, OutputStream outputStream) throws IOException {
        int count = 0;
        while (count == 0) {
            count = inputStream.available();
        }

        int resourceSize = count;
        // 輸出http請求頭,然後再輸出具體內容
        outputStream.write(HttpProtocolUtil.getHttpHeader200(resourceSize).getBytes());

        // 讀取內容輸出
        long written = 0 ; // 已經讀取的內容長度
        int byteSize = 1024; // 計劃每次緩衝的長度
        byte[] bytes = new byte[byteSize];

        while (written < resourceSize) {
            if (written + byteSize > resourceSize) { // 說明剩餘未讀取大小不足一個1024長度,那就按真實長度處理
                byteSize = (int) (resourceSize - written); // 剩餘的文件內容長度
                bytes = new byte[byteSize];
            }
            inputStream.read(bytes);
            outputStream.write(bytes);

            outputStream.flush();

            written += byteSize;
        }
    }

}

5、動態資源請求

5.1 Servlet接口定義

public interface Servlet {

    void init() throws Exception;

    void destory() throws Exception;

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

}

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

}

5.3 業務類Servlet定義RiemannServlet

public class RiemannServlet extends HttpServlet {

    @Override
    public void doGet(Request request, Response response) {
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        String content = "<h1>RiemannServlet get</h1>";
        try {
            response.output((HttpProtocolUtil.getHttpHeader200(content.getBytes().length) + content));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

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

    @Override
    public void init() throws Exception {

    }

    @Override
    public void destory() throws Exception {

    }

}

6、多線程改造封裝的RequestProcessor類

@Data
@AllArgsConstructor
public class RequestProcessor extends Thread {

    private Socket socket;

    private Map<String, HttpServlet> servletMap;

    @Override
    public void run() {
        try {
            InputStream inputStream = socket.getInputStream();

            // 封裝Request對象和Response對象
            Request request = new Request(inputStream);
            Response response = new Response(socket.getOutputStream());

            HttpServlet httpServlet = servletMap.get(request.getUrl());
            // 靜態資源處理
            if (httpServlet == null) {
                response.outputHtml(request.getUrl());
            } else {
                // 動態資源servlet請求
                httpServlet.service(request, response);
            }

            socket.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

三、測試結果

1、訪問靜態資源

在這裏插入圖片描述

2、訪問動態資源

在這裏插入圖片描述
3、找不到資源錯誤頁面訪問

在這裏插入圖片描述

4、多線程訪問,其中演示動態資源阻塞,不影響其他資源的訪問。

在這裏插入圖片描述
動態資源由於給了休眠10秒,訪問的時候請求被阻塞。

在這裏插入圖片描述

但是不影響同時訪問其他資源,這裏是靜態資源。

在這裏插入圖片描述

四、代碼倉庫

https://github.com/riemannChow/perseverance/tree/master/handwriting-framework/tomcat/tomcat-customize

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章