從零手寫實現 tomcat-05-servlet 處理支援

老马啸西风發表於2024-05-09

創作緣由

平時使用 tomcat 等 web 伺服器不可謂不多,但是一直一知半解。

於是想著自己實現一個簡單版本,學習一下 tomcat 的精髓。

系列教程

從零手寫實現 apache Tomcat-01-入門介紹

從零手寫實現 apache Tomcat-02-web.xml 入門詳細介紹

從零手寫實現 tomcat-03-基本的 socket 實現

從零手寫實現 tomcat-04-請求和響應的抽象

從零手寫實現 tomcat-05-servlet 處理支援

從零手寫實現 tomcat-06-servlet bio/thread/nio/netty 池化處理

從零手寫實現 tomcat-07-war 如何解析處理三方的 war 包?

從零手寫實現 tomcat-08-tomcat 如何與 springboot 整合?

從零手寫實現 tomcat-09-servlet 處理類

從零手寫實現 tomcat-10-static resource 靜態資原始檔

從零手寫實現 tomcat-11-filter 過濾器

從零手寫實現 tomcat-12-listener 監聽器

整體思路

模擬實現 servlet 的邏輯處理,而不是侷限於上一節的靜態檔案資源。

整體流程

1)定義 servlet 標準的 介面+實現

2)解析 web.xml 獲取對應的 servlet 例項與 url 之間的對映關係。

3)呼叫請求

1. servlet 實現

api 介面

servlet 介面,我們直接引入 servlet-api 的標準。

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>${javax.servlet.version}</version>
</dependency>

抽象 servlet 定義

package com.github.houbb.minicat.support.servlet;

import com.github.houbb.minicat.constant.HttpMethodType;

import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public abstract class AbstractMiniCatHttpServlet extends HttpServlet {

    public abstract void doGet(HttpServletRequest request, HttpServletResponse response);

    public abstract void doPost(HttpServletRequest request, HttpServletResponse response);

    @Override
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) req;
        HttpServletResponse httpServletResponse = (HttpServletResponse) res;
        if(HttpMethodType.GET.getCode().equalsIgnoreCase(httpServletRequest.getMethod())) {
            this.doGet(httpServletRequest, httpServletResponse);
            return;
        }

        this.doPost(httpServletRequest, httpServletResponse);
    }

}

根據請求方式分別處理

簡單的實現例子

下面是一個簡單的處理實現:

  • MyMiniCatHttpServlet.java
package com.github.houbb.minicat.support.servlet;

import com.github.houbb.log.integration.core.Log;
import com.github.houbb.log.integration.core.LogFactory;
import com.github.houbb.minicat.dto.MiniCatResponse;
import com.github.houbb.minicat.util.InnerHttpUtil;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 僅用於測試
 *
 * @since 0.3.0
 */
public class MyMiniCatHttpServlet extends AbstractMiniCatHttpServlet {

    private static final Log logger = LogFactory.getLog(MyMiniCatHttpServlet.class);

    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        String content = "MyMiniCatServlet-get";

        MiniCatResponse miniCatResponse = (MiniCatResponse) response;
        miniCatResponse.write(InnerHttpUtil.http200Resp(content));
    }

    @Override
    public void doPost(HttpServletRequest request, HttpServletResponse response) {
        String content = "MyMiniCatServlet-post";

        MiniCatResponse miniCatResponse = (MiniCatResponse) response;
        miniCatResponse.write(InnerHttpUtil.http200Resp(content));
    }

}

2. web.xml 解析

說明

web.xml 需要解析處理。

比如這樣的:

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

    <servlet>
        <servlet-name>my</servlet-name>
        <servlet-class>com.github.houbb.minicat.support.servlet.MyMiniCatHttpServlet</servlet-class>
    </servlet>

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

</web-app>

解析方式

介面定義

package com.github.houbb.minicat.support.servlet;

import javax.servlet.Servlet;
import javax.servlet.http.HttpServlet;

/**
 * servlet 管理
 *
 * @since 0.3.0
 */
public interface IServletManager {

    /**
     * 註冊 servlet
     *
     * @param url     url
     * @param servlet servlet
     */
    void register(String url, HttpServlet servlet);

    /**
     * 獲取 servlet
     *
     * @param url url
     * @return servlet
     */
    HttpServlet getServlet(String url);

}

web.xml

web.xml 的解析方式,核心的處理方式:

    //1. 解析 web.xml
    //2. 讀取對應的 servlet mapping
    //3. 儲存對應的 url + servlet 示例到 servletMap
    private void loadFromWebXml() {
        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");
            //1, 找到所有的servlet標籤,找到servlet-name和servlet-class
            //2, 根據servlet-name找到<servlet-mapping>中與其匹配的<url-pattern>
            for (Element element : selectNodes) {
                /**
                 * 1, 找到所有的servlet標籤,找到servlet-name和servlet-class
                 */
                Element servletNameElement = (Element) element.selectSingleNode("servlet-name");
                String servletName = servletNameElement.getStringValue();
                Element servletClassElement = (Element) element.selectSingleNode("servlet-class");
                String servletClass = servletClassElement.getStringValue();

                /**
                 * 2, 根據servlet-name找到<servlet-mapping>中與其匹配的<url-pattern>
                 */
                //Xpath表示式:從/web-app/servlet-mapping下查詢,查詢出servlet-name=servletName的元素
                Element servletMapping = (Element) rootElement.selectSingleNode("/web-app/servlet-mapping[servlet-name='" + servletName + "']'");

                String urlPattern = servletMapping.selectSingleNode("url-pattern").getStringValue();
                HttpServlet httpServlet = (HttpServlet) Class.forName(servletClass).newInstance();

                this.register(urlPattern, httpServlet);
            }

        } catch (Exception e) {
            logger.error("[MiniCat] read web.xml failed", e);

            throw new MiniCatException(e);
        }
    }

解析之後的 HttpServlet 全部放在 servletMap 中。

然後在對應的 url 我們選取處理即可。

3. url 的處理

說明

根據 url 找到對應的 servlet 進行處理。

主要分為 3 大類:

1)url 不存在

2)url 為 html 等靜態資源

  1. servlet 的處理邏輯

設計

我們把這部分抽象為介面:

public void dispatch(RequestDispatcherContext context) {
    final MiniCatRequest request = context.getRequest();
    final MiniCatResponse response = context.getResponse();
    final IServletManager servletManager = context.getServletManager();
    // 判斷檔案是否存在
    String requestUrl = request.getUrl();
    if (StringUtil.isEmpty(requestUrl)) {
        emptyRequestDispatcher.dispatch(context);
    } else {
        // 靜態資源
        if (requestUrl.endsWith(".html")) {
            staticHtmlRequestDispatcher.dispatch(context);
        } else {
            // servlet 
            servletRequestDispatcher.dispatch(context);
        }
    }
}

servlet 例子

如果是 servlet 的話,核心處理邏輯如下:

// 直接和 servlet 對映
final String requestUrl = request.getUrl();
HttpServlet httpServlet = servletManager.getServlet(requestUrl);
if(httpServlet == null) {
    logger.warn("[MiniCat] requestUrl={} mapping not found", requestUrl);
    response.write(InnerHttpUtil.http404Resp());
} else {
    // 正常的邏輯處理
    try {
        httpServlet.service(request, response);
    } catch (Exception e) {
        logger.error("[MiniCat] http servlet handle meet ex", e);
        throw new MiniCatException(e);
    }
}

4. 讀取 request 的問題修復

問題

發現 request 讀取輸入流的時候,有時候讀取為空,但是頁面明明是正常請求的。

原始程式碼

private void readFromStream() {
    try {
        //從輸入流中獲取請求資訊
        int count = inputStream.available();
        byte[] bytes = new byte[count];
        int readResult = inputStream.read(bytes);
        String inputsStr = new String(bytes);
        logger.info("[MiniCat] readCount={}, input stream {}", readResult, inputsStr);
        if(readResult <= 0) {
            logger.info("[MiniCat] readCount is empty, ignore handle.");
            return;
        }
        //獲取第一行資料
        String firstLineStr = inputsStr.split("\\n")[0];  //GET / HTTP/1.1
        String[] strings = firstLineStr.split(" ");
        this.method = strings[0];
        this.url = strings[1];
        logger.info("[MiniCat] method={}, url={}", method, url);
    } catch (IOException e) {
        logger.error("[MiniCat] readFromStream meet ex", e);
        throw new RuntimeException(e);
    }
}

問題分析

問題其實出在 inputStream.available() 中,網路流(如 Socket 流)與檔案流不同,網路流的 available() 方法可能返回 0,即使實際上有資料可讀。這是因為網路通訊是間斷性的,資料可能分多個批次到達。

修正

由於 available() 方法在網路流中可能不準確,您可以嘗試不使用此方法來預分配位元組陣列。

相反,您可以使用一個固定大小的緩衝區,或者使用 read() 方法的迴圈來動態讀取資料。

    /**
     * 直接根據 available 有時候讀取不到資料
     * @since 0.3.0
     */
    private void readFromStreamByBuffer() {
        byte[] buffer = new byte[1024]; // 使用固定大小的緩衝區
        int bytesRead = 0;

        try {
            while ((bytesRead = inputStream.read(buffer)) != -1) { // 迴圈讀取資料直到EOF
                String inputStr = new String(buffer, 0, bytesRead);

                // 檢查是否讀取到完整的HTTP請求行
                if (inputStr.contains("\n")) {
                    // 獲取第一行資料
                    String firstLineStr = inputStr.split("\\n")[0];
                    String[] strings = firstLineStr.split(" ");
                    this.method = strings[0];
                    this.url = strings[1];

                    logger.info("[MiniCat] method={}, url={}", method, url);
                    break; // 退出迴圈,因為我們已經讀取到請求行
                }
            }

            if ("".equals(method)) {
                logger.info("[MiniCat] No HTTP request line found, ignoring.");
                // 可以選擇丟擲異常或者返回空請求物件
            }
        } catch (IOException e) {
            logger.error("[MiniCat] readFromStream meet ex", e);
            throw new RuntimeException(e);
        }
    }

開源地址

 /\_/\  
( o.o ) 
 > ^ <

mini-cat 是簡易版本的 tomcat 實現。別稱【嗅虎】(心有猛虎,輕嗅薔薇。)

開源地址:https://github.com/houbb/minicat

相關文章