手搓Tomcat

发布于:2025-09-13 ⋅ 阅读:(16) ⋅ 点赞:(0)

目录

Tomcat是什么?

前置工作准备

构建并启动Tomcat

处理Socket逻辑顺序

获取输入流并读取数据封装到Request

自定义Servlet对象

暂存响应体

按Http协议发送响应数据

部署Tomcat



Tomcat是什么?


Tomcat 是一个 Web 应用服务器(准确说是 Servlet 容器JSP 引擎),是目前 Java Web 开发中最常用的中间件之一。

本质:

  • Tomcat 是由 Apache 基金会开发和维护的开源 Web 服务器。

  • 它实现了 ServletJSP 规范,是 Java EE 规范的一部分。

  • Tomcat 本身不是完整的 Java EE 应用服务器(如 JBoss、GlassFish),但足以支撑大部分 Web 应用。

核心功能:

  • Socket 监听:在指定端口(默认 8080)监听来自浏览器的 HTTP 请求。

  • 请求解析:解析 HTTP 请求报文,把它封装成 HttpServletRequest 对象。

  • Servlet 管理:根据 URL 匹配到对应的 Servlet,调用其 service() 方法。

  • 响应返回:将 HttpServletResponse 的内容拼装成完整的 HTTP 响应报文,并写回浏览器。

  • 静态资源处理:直接返回 HTML、CSS、JS、图片等静态文件。

假如浏览器访问 http:// localhost:8080/test,随后根据 URL 发送 HTTP 报文给服务器:

GET /test HTTP/1.1
Host: localhost:8080

随后Tomcat通过 ServerSocket 在端口 8080 监听,收到请求后解析出请求方法(GET)、路径(/test)、协议版本(HTTP/1.1)、头部信息等。

之后Tomcat 根据配置找到 /test 对应的 Servlet,然后调用 Servlet 的 service() 方法,service 再根据请求方法选择doGet()或doPost()等方法。在 Servlet 中执行业务逻辑,例如查询数据库、处理数据等。

最后Servlet 使用  HttpServletResponse 设置响应头、状态码、响应体内容。Tomcat 将这些内容封装成完整的 HTTP 响应报文:

HTTP/1.1 200 OK
Content-Type: text/html;charset=utf-8
Content-Length: 12

hello Eleven

浏览器收到响应后渲染页面。

所以实际上 Tomcat 是前端和后端之间的“桥梁”,它把低层的 TCP/HTTP 通信细节封装起来,让开发者只需要处理业务逻辑。

而我们想手写一个Tomcat主要是去尝试使用Socket处理Http协议,之后再模拟 Servlet 调用流程。


前置工作准备


首先我们引入 javax.servlet-api 依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.5.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>cn.tomcat.com</groupId>
    <artifactId>tomcat-eleven</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>tomcat-eleven</name>
    <description>tomcat-eleven</description>

    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.1</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>3.8.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

之后我们先构造一个Tomcat启动类,然后创建一个start()方法用来启动Tomcat:

package cn.tomcat.com;


import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TomcatElevenApplication {

    /**
     * 启动
     */
    public void start(){
        
    }

    public static void main(String[] args) {
        TomcatElevenApplication tomcatElevenApplication = new TomcatElevenApplication();
        tomcatElevenApplication.start();
    }

}

构建并启动Tomcat


首先我们需要知道,浏览器向服务发起 HTTP 请求时,实际上是通过 TCP 建立连接,另外 http 协议的本质上是基于 TCP 协议的应用层协议:

  1. 浏览器执行 http://localhost:8080/test

  2. 它会向 localhost8080 端口发送一个 TCP 三次握手

  3. 建立连接后,浏览器会将 HTTP 报文(如 GET /index.html HTTP/1.1)通过 TCP 流发送过去。

而 Socket 是 Java 与底层 TCP 网络通信的接口,所以我们首先去启用 Socket 去监听,而 serverSocket.accept() 方法是阻塞方法,直到有连接到来才会继续执行。

然后为了保证主线程执行结束扔可以继续接受请求,我们使用while循环不断地去监听请求并处理,之后从线程池获取线程来处理socket的方法,所以这里我们去新建一个SocketProcesser类来去封装线程任务,以便交给线程池或新线程来执行,所以这个类就需要去引入Runnable。

首先完善启动Tomcat方法:

/**
 * 线程池
 */
private final ExecutorService executorService = Executors.newFixedThreadPool(10);    

/**
 * 启动
 */
public void start(){
    try {
        // socket 连接 TCP
        ServerSocket serverSocket = new ServerSocket(8080);
        while(true){
            // 监听
            Socket socket = serverSocket.accept();
            // 处理 socket
            executorService.execute(() -> new SocketProcesser(socket).run());
        }
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

然后我们创建SocketProcesser类引入Runnable:

package cn.tomcat.com;


import javax.servlet.ServletException;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;

/**
 * 处理 socket
 */
public class SocketProcesser implements Runnable {

    private Socket socket;
    public SocketProcesser(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        processSocket(socket);
    }

    /**
     * 处理 socket
     * @param socket
     */
    private void processSocket(Socket socket) {
        // 处理逻辑... 
    }
}

而在processSocket方法内就可以添加对于Socket的处理逻辑了。


处理Socket逻辑顺序


逻辑顺序:

从 Socket 读取客户端请求 → 解析 HTTP 报文 → 封装为 Request/Response → 调用 Servlet → 返回响应


获取输入流并读取数据封装到Request


首先我们调用 socket.getInputStream() 从 TCP 连接 中获取输入流,准备接收浏览器发送的 HTTP 请求数据,之后创建字节数组 byte[] bytes = new byte[1024] 存储数据并使用 inputStream.read(bytes) 方法阻塞式读取数据,返回读到的字节数:

try (InputStream inputStream = socket.getInputStream()) {
    byte[] bytes = new byte[1024];
    int read = inputStream.read(bytes);

    if (read <= 0) return;
    // ...
}

之后将byte数据转换为字符串:

// 转成字符串
String requestText = new String(bytes, 0, read);
System.out.println("原始请求:\n" + requestText);

现在我们可以去浏览器访问 http://localhost:8080/test 地址来查看后端打印:

下面是Http协议结构:

可以发现在前面有请求方法+空格+URL地址+空格+协议版本,所以我们可以将这些数据封装到Request对象中:

package cn.tomcat.com;

import javax.servlet.http.HttpServletRequest;
import java.io.OutputStream;
import java.net.Socket;

public class Request {

    private String method; // 请求方法
    private String url; // 请求路径
    private String protocol; // 请求协议
    private Socket socket;  // socket连接

    public Request(String method, String url, String protocol, Socket socket) {
        this.method = method;
        this.url = url;
        this.protocol = protocol;
        this.socket = socket;
    }
    // Getter And Setter ...
}

之后我们将解析出来并封装Request:

第一行是请求行:

GET /test HTTP/1.1

用空格拆分:

  • parts[0] = "GET" → 请求方法。

  • parts[1] = "/test" → 请求路径(URL)。

  • parts[2] = "HTTP/1.1" → 协议版本。

// 按行拆分,第一行是请求行
String[] lines = requestText.split("\r\n");
if (lines.length > 0) {
    String requestLine = lines[0]; // 例如: GET /test HTTP/1.1
    String[] parts = requestLine.split(" ");
    if (parts.length >= 3) {
        String method = parts[0];    // GET
        String url = parts[1];       // /test
        String protocol = parts[2];  // HTTP/1.1
        // 封装到 Request 对象
        Request request = new Request(method, url, protocol, socket);
    }
}

自定义Servlet对象


Tomcat底层是使用HttpServlet,而内部实现了service(),doGet(),doPost()等方法。而在 Servlet 规范中,doGetdoPostdoPutdoDelete 等方法是用来处理不同 HTTP 请求方法 的回调方法。它们是 HttpServlet 类提供的钩子方法,当 Tomcat 收到特定类型的 HTTP 请求时,会调用这些方法,让开发者在其中编写自己的业务逻辑。

在底层,Tomcat 调用service()方法传入 Request + Response,其方法内部根据请求方法判断到底是去使用doGet还是doPost等方法:

所以我们来自定义一个 Servlet 对象去继承 HttpServlet 来实现这些方法:

service方法可以不用重构,我们先以doGet方法重写为例。

package cn.tomcat.com;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;


@WebServlet  // 在不编写 web.xml 的情况下注册 Servlet
public class ElevenServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println(req.getMethod());

        // 需要先告诉浏览器一下响应体多少个字节
        resp.addHeader("Content-Length", "12");
        resp.addHeader("Content-Type", "text/html;charset=utf-8");

        // 响应数据
        resp.getOutputStream().write("hello Eleven".getBytes());
    }
}

而原本的Service方法需要我们去传递ServletRequest与ServletResponse:

所以我们的request与response需要分别去实现HttpServletRequest与HttpServletResponse,这里我们不想全重写了,就直接通过抽象类来实现方法,之后request与response分别去继承抽象类就OK了:

package cn.tomcat.com;

import javax.servlet.*;
import javax.servlet.http.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.Principal;
import java.util.*;

public class AbstractHttpServletRequest implements HttpServletRequest {
    // 省略一堆的重写方法 ...
}
package cn.tomcat.com;

import java.net.Socket;

public class Request extends AbstractHttpServletRequest {

    private String method; // 请求方法
    private String url; // 请求路径
    private String protocol; // 请求协议
    private Socket socket;  // 客户端 socket

    public Request(String method, String url, String protocol, Socket socket) {
        this.method = method;
        this.url = url;
        this.protocol = protocol;
        this.socket = socket;
    }

    // GETTER AND SETTER
    // 这里强调HttpServletRequest实现的是StringBuffer getRequestURL()方法
    // 所以我们需要更改回去请求路径方法
    // 其他的也同理需要修改
    public StringBuffer getRequestURL() {
        return new StringBuffer(url);
    }
    // ...
}

response对象也同理:

package cn.tomcat.com;

import javax.servlet.*;
import javax.servlet.http.*;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.*;

public class AbstractHttpServletResponse implements HttpServletResponse {
    // 省略一堆的重写方法 ...
}

而响应信息主要有 响应状态码 + 状态描述信息 + 响应头headers,另外一个请求对应一个响应,所以在添加一个Request属性 :

package cn.tomcat.com;

import javax.servlet.ServletOutputStream;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;

public class Response extends AbstractHttpServletResponse {

    private int status = 200;
    private String msg = "OK";
    private Map<String,String> headers = new HashMap<>();
    private Request request;


    public Response(Request request) throws IOException {
        this.request = request;
        this.socketOutputStream = request.getSocket().getOutputStream();
    }

    @Override
    public void setStatus(int i, String s) {
        this.status = i;
        this.msg = s;
    }

    @Override
    public int getStatus() {
        return status;
    }

    public String getMsg() {
        return msg;
    }

    @Override
    public void addHeader(String s, String s1) {
        headers.put(s, s1);
    }
}

这回我们就可以正常去使用service方法了:

package cn.tomcat.com;


import javax.servlet.ServletException;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;

/**
 * 处理 socket
 */
public class SocketProcesser implements Runnable {

    private Socket socket;
    public SocketProcesser(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        processSocket(socket);
    }

    /**
     * 处理 socket
     * @param socket
     */
    private void processSocket(Socket socket) {
        try (InputStream inputStream = socket.getInputStream()) {
            byte[] bytes = new byte[1024];
            int read = inputStream.read(bytes);

            if (read <= 0) return;

            // 转成字符串
            String requestText = new String(bytes, 0, read);
            System.out.println("原始请求:\n" + requestText);

            // 按行拆分,第一行是请求行
            String[] lines = requestText.split("\r\n");
            if (lines.length > 0) {
                String requestLine = lines[0]; // 例如: GET /test HTTP/1.1
                String[] parts = requestLine.split(" ");
                if (parts.length >= 3) {
                    String method = parts[0];    // GET
                    String url = parts[1];       // /test
                    String protocol = parts[2];  // HTTP/1.1

                    // 封装到 Request 对象
                    Request request = new Request(method, url, protocol, socket);

                    // 封装到 Response 对象
                    Response response = new Response(request);

                    // 匹配Servlet
                    ElevenServlet servlet = new ElevenServlet();
                    // 调用Servlet的service方法,帮助我们判断到底要调用doGet还是doPost等方法
                    servlet.service(request, response);

                    // TODO 发送响应数据
                   
                }
            }

        } catch (IOException e) {
            // 也需要构造一个Response去返回异常提示
            throw new RuntimeException(e);
        } catch (ServletException e) {
            throw new RuntimeException(e);
        }
    }
}

但是我们发现运行后会产生空指针异常,这是因为我们将HttpServletResponse内部方法重写,导致我们在doGet方法内部调用的 resp.getOutputStream() 方法没有重写,而该方法表示的意思的将二进制数据写入 HTTP 响应体,并发送给客户端,所以接下来我们需要完善该方法。


暂存响应体


查看我们抽象类可以发现,这个方法返回了ServletOutputStream对象:

而ServletOutputStream是个抽象类,所以我们也肯定要自己去重写一个ServletOutputStream:

而write()方法的实现如下:

所以我们应先去重写write()方法,为了让doGet全部执行结束判断是否异常之后在调用write方法,我们需要将这个响应体存储,:

package cn.tomcat.com;

import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import java.io.IOException;

public class ResponseServletOutputStream extends ServletOutputStream {

    private byte[] bytes = new byte[1024]; // 缓冲区
    private int pos = 0; // 缓冲区的位置

    @Override
    public void write(int b) throws IOException {
        bytes[pos] = (byte) b;
        pos++;
    }

    public byte[] getBytes() {
        return bytes;
    }

    public int getPos() {
        return pos;
    }
}

之后重写方法:

package cn.tomcat.com;

import javax.servlet.ServletOutputStream;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;

public class Response extends AbstractHttpServletResponse {
    
    // ...
    @Override
    public ResponseServletOutputStream getOutputStream() throws IOException {
        return responseServletOutputStream;
    }

}

随后就该去执行发送响应码了。


按Http协议发送响应数据


我们发送响应数据是通过Complete方法,所以需要重写:

package cn.tomcat.com;

import javax.servlet.ServletOutputStream;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;

public class Response extends AbstractHttpServletResponse {
    
    // ...
    /**
     * 完成响应
     */
    public void complete() throws IOException {
        sendResponseLine();
        sendResponseHeaders();
        sendResponseBody();
    }

}

在这里面我们先定义三个方法来按照Http协议规范一次发送响应行、响应头、响应体。

而发送,我们还需要使用socket对象:

package cn.tomcat.com;

import javax.servlet.ServletOutputStream;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;

public class Response extends AbstractHttpServletResponse {

    private int status = 200;
    private String msg = "OK";
    private Map<String,String> headers = new HashMap<>();
    private Request request;
    private OutputStream socketOutputStream;
    private ResponseServletOutputStream responseServletOutputStream = new ResponseServletOutputStream();


    public Response(Request request) throws IOException {
        this.request = request;
        this.socketOutputStream = request.getSocket().getOutputStream();
    }
}

那么接下来我们就尝试写发送响应行:

响应行格式: HTTP/1.1 + ' ' + 200 + ' ' + OK

public class Response extends AbstractHttpServletResponse {

    private byte SP = ' ';  // 空格
    private byte CR = '\r'; // 回车
    private byte LF = '\n'; // 换行

    // ...

    /**
     * 发送响应行
     */
    private void sendResponseLine() throws IOException {
        socketOutputStream.write(request.getProtocol().getBytes());
        socketOutputStream.write(SP);
        socketOutputStream.write(status);
        socketOutputStream.write(SP);
        socketOutputStream.write(msg.getBytes());
        socketOutputStream.write(CR);
        socketOutputStream.write(LF);
    }
}

发送响应头:

HTTP 协议规定:

Content-Type: text/html;charset=utf-8
Content-Length: 123
自定义头: 值

每个响应头占一行,格式为 键: 值,行尾以 \r\n 结束。
响应头结束后,还需要再写入一个空行(即仅包含 \r\n),表示头部部分结束,后面就是响应体。

private void sendResponseHeaders() throws IOException {

    if(!headers.containsKey("Content-Length")) {
        addHeader("Content-Length", String.valueOf(getOutputStream().getPos()));
    }
    if(!headers.containsKey("Content-Type")) {
        addHeader("Content-Type", "text/html;charset=utf-8");
    }

    for (Map.Entry<String,String> entry : headers.entrySet()) {
        String key = entry.getKey();
        String value = entry.getValue();
        socketOutputStream.write(key.getBytes());  // 写入键
        ocketOutputStream.write(":".getBytes());   // 写入:
        socketOutputStream.write(value.getBytes());// 写入值
        socketOutputStream.write(CR);              // 回车
        socketOutputStream.write(LF);              // 换行
    }
    // 头部结束后,再写一个空行
    socketOutputStream.write(CR);
    socketOutputStream.write(LF);
}

发送响应体:

而发送响应体就直接使用write方法传递:

private OutputStream socketOutputStream;
private ResponseServletOutputStream responseServletOutputStream = new ResponseServletOutputStream(); // 响应体

@Override
public ResponseServletOutputStream getOutputStream() throws IOException {
    return responseServletOutputStream;
}
/**
 * 发送响应体
 */
private void sendResponseBody() throws IOException {
    socketOutputStream.write(getOutputStream().getBytes());
}

完整代码:

package cn.tomcat.com;

import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;

public class Response extends AbstractHttpServletResponse {

    private byte SP = ' ';  // 空格
    private byte CR = '\r'; // 回车
    private byte LF = '\n'; // 换行

    private int status = 200;
    private String msg = "OK";
    private Map<String,String> headers = new HashMap<>();
    private Request request;
    private OutputStream socketOutputStream;
    private ResponseServletOutputStream responseServletOutputStream = new ResponseServletOutputStream(); // 响应体


    public Response(Request request) throws IOException {
        this.request = request;
        this.socketOutputStream = request.getSocket().getOutputStream();
    }

    @Override
    public void setStatus(int i, String  s) {
        this.status = i;
        this.msg = s;
    }

    @Override
    public int getStatus() {
        return status;
    }

    public String getMsg() {
        return msg;
    }

    @Override
    public void addHeader(String s, String s1) {
        headers.put(s, s1);
    }

    @Override
    public ResponseServletOutputStream getOutputStream() throws IOException {
        return responseServletOutputStream;
    }

    /**
     * 完成响应
     */
    public void complete() throws IOException {
        sendResponseLine();
        sendResponseHeaders();
        sendResponseBody();
    }

    /**
     * 发送响应体
     */
    private void sendResponseBody() throws IOException {
        socketOutputStream.write(getOutputStream().getBytes());
    }

    /**
     * 发送响应头
     */
    private void sendResponseHeaders() throws IOException {

        if(!headers.containsKey("Content-Length")) {
            addHeader("Content-Length", String.valueOf(getOutputStream().getPos()));
        }
        if(!headers.containsKey("Content-Type")) {
            addHeader("Content-Type", "text/html;charset=utf-8");
        }

        for (Map.Entry<String,String> entry : headers.entrySet()) {
            String key = entry.getKey();
            String value = entry.getValue();
            socketOutputStream.write(key.getBytes());
            socketOutputStream.write(":".getBytes());
            socketOutputStream.write(value.getBytes());
            socketOutputStream.write(CR);
            socketOutputStream.write(LF);
        }
        socketOutputStream.write(CR);
        socketOutputStream.write(LF);
    }

    /**
     * 发送响应行
     */
    private void sendResponseLine() throws IOException {
        socketOutputStream.write(request.getProtocol().getBytes());
        socketOutputStream.write(SP);
        socketOutputStream.write(status);
        socketOutputStream.write(SP);
        socketOutputStream.write(msg.getBytes());
        socketOutputStream.write(CR);
        socketOutputStream.write(LF);
    }
}

最后调用complete方法:

package cn.tomcat.com;


import javax.servlet.ServletException;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;

/**
 * 处理 socket
 */
public class SocketProcesser implements Runnable {

    private Socket socket;
    public SocketProcesser(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        processSocket(socket);
    }

    /**
     * 处理 socket
     * @param socket
     */
    private void processSocket(Socket socket) {
        try (InputStream inputStream = socket.getInputStream()) {
            byte[] bytes = new byte[1024];
            int read = inputStream.read(bytes);

            if (read <= 0) return;

            // 转成字符串
            String requestText = new String(bytes, 0, read);
            System.out.println("原始请求:\n" + requestText);

            // 按行拆分,第一行是请求行
            String[] lines = requestText.split("\r\n");
            if (lines.length > 0) {
                String requestLine = lines[0]; // 例如: GET /test HTTP/1.1
                String[] parts = requestLine.split(" ");
                if (parts.length >= 3) {
                    String method = parts[0];    // GET
                    String url = parts[1];       // /test
                    String protocol = parts[2];  // HTTP/1.1

                    // 封装到 Request 对象
                    Request request = new Request(method, url, protocol, socket);

                    // 打印封装结果
                    System.out.println("方法: " + request.getMethod());
                    System.out.println("路径: " + request.getRequestURL());
                    System.out.println("协议: " + request.getProtocol());

                    // 封装到 Response 对象
                    Response response = new Response(request);

                    // 匹配Servlet
                    ElevenServlet servlet = new ElevenServlet();
                    // 调用Servlet的service方法,帮助我们判断到底要调用doGet还是doPost等方法
                    servlet.service(request, response);

                    // 发送响应数据
                    response.complete();
                }
            }

        } catch (IOException e) {
            // 也需要构造一个Response去返回异常提示
            throw new RuntimeException(e);
        } catch (ServletException e) {
            throw new RuntimeException(e);
        }
    }
}

部署Tomcat


用过Tomcat的知道,Tomcat 的 webapps/ 目录是部署入口,所以我们先建立一个webapps目录:

这个hello就相当于一个项目,或者也可以称作一个Jar包,而在classes下就可以放入一些类,而tomcat关心的是这些项目或者类中哪里有servlet,然后根据servlet去匹配方法处理请求。

首先将我们的ElevenServlet.class文件放在classes/eleven/ElevenServlet.class下来伪造一个Servlet,之后将原本的ElevenServlet删除。那么现在就相当于我在tomcat下面部署了一个hello应用,而这个应用下面还有ElevenServlet,而在tomcat启动前首先需要完成部署App:

public class TomcatElevenApplication {
    
    // ...
    public static void main(String[] args) {
        TomcatElevenApplication tomcatElevenApplication = new TomcatElevenApplication();
        tomcatElevenApplication.deployApps(); // 部署APP
        tomcatElevenApplication.start();
    }
}

如何实现该方法呢?

首先肯定需要找到tomcat下有哪些应用,先拿到webApps文件夹,然后遍历内部应用,随后准备使用deployApp来比那里应用内的所有类:

/**
 * 遍历webapps目录
 */
private void deployApps() {
    File webApps = new File(System.getProperty("user.dir"), "/webapps");
    if(webApps.exists()){
        for(String app : webApps.list()){
            deployApp(webApps, app);
        }
    }
}

之后编写deployApp方法,主要目的是判断当前应用下有哪些类继承了HttpServlet,然后在该类拿到@WebServlet注解值,并存储起来方便处理Socket时使用。

我们先创建存储类:

package cn.tomcat.com;


import javax.servlet.Servlet;
import java.util.HashMap;
import java.util.Map;

/**
 * 应用上下文
 */
public class Context {

    /**
     * 应用名称
     */
    private String appName;
    /**
     * url 映射
     */
    private Map<String, Servlet> urlPatternMap = new HashMap<String, Servlet>();

    public Context(String appName) {
        this.appName = appName;
    }

    /**
     * 添加servlet
     * @param urlPattern
     * @param servlet
     */
    public void addServlet(String urlPattern, Servlet servlet) {
        urlPatternMap.put(urlPattern, servlet);
    }

    /**
     * 根据url获取servlet
     * @param urlPattern
     * @return
     */
    public Servlet getByUrlPattern(String urlPattern) {
        for (String key : urlPatternMap.keySet()) {
            if (urlPattern.contains(key)) {
                return urlPatternMap.get(key);
            }
        }
        return null;
    }
}

之后按照上面逻辑实现查找:

注意,加载类的时候要使用自定义类加载器,否则因为目录不在同一个,扫描不到classes:

package cn.tomcat.com;

import java.net.URL;
import java.net.URLClassLoader;


/**
 * 自定义类加载器
 */
public class WebappClassLoader extends URLClassLoader {


    public WebappClassLoader(URL[] urls) {
        super(urls);
    }
}
/**
 * 保存Tomcat有哪些应用
 */
private Map<String, Context> contextMap = new HashMap<>();


/**
 * 遍历当前应用内所有类中是否有继承HttpServlet的,
 * 如果有,就将它添加到应用上下文
 * @param webApps
 * @param appName
 */
private void deployApp(File webApps, String appName) {
    Context context = new Context(appName);
    // 当前应用下面有哪些Servlet
    File appDirectory = new File(webApps, appName); // hello文件夹
    File classesDirectory = new File(appDirectory, "classes"); // classes文件夹
    List<File> allFilePath = getAllFilePath(classesDirectory);
    for (File file : allFilePath) {
        if(file.getName().endsWith(".class")){
            // 是类文件
            // 思路:加载为Class对象,随后用反射判断是否继承了HttpServlet

            // 转换类加载格式
            String name = file.getPath();
            name = name.replace(classesDirectory.getPath() + "\\ ", "/");
            name = name.replace(".class", "");
            name = name.replace("\\", "/");

            // 类加载器加载类
            try {
                // 这样是加载不到的,因为应用不在这个cn.tomcat.com下
//                    Class<?> servletClass = Thread.currentThread().getContextClassLoader().loadClass(name);
                // 使用自定义的类加载器加载类,让它去加载classes目录
                WebappClassLoader webappClassLoader = new WebappClassLoader(new URL[]{classesDirectory.toURI().toURL()});
                Class<?> servletClass = webappClassLoader.loadClass(name);
                // 判断是否继承了HttpServlet
                if(HttpServlet.class.isAssignableFrom(servletClass)){
                    // 是HttpServlet的子类
                    System.out.println("发现Servlet:" + name);
                    // 解析URL对应的匹配规则
                    if(servletClass.isAnnotationPresent(javax.servlet.annotation.WebServlet.class)){
                        // 获取注解value值
                        WebServlet webServlet = servletClass.getAnnotation(WebServlet.class);
                        String[] urlPatterns = webServlet.urlPatterns();
                        // 存储到上下文
                        for (String urlPattern : urlPatterns) {
                            System.out.println("发现URL:" + urlPattern);
                            // 存储到Map中
                            context.addServlet(urlPattern, (Servlet) servletClass.newInstance());
                        }
                    }
                }
            } catch (ClassNotFoundException e) {
                throw new RuntimeException(e);
            } catch (MalformedURLException e) {
                throw new RuntimeException(e);
            } catch (InstantiationException e) {
                throw new RuntimeException(e);
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            }
        }
    }
    // 部署完成,保存应用映射
    contextMap.put(appName, context);
}

最后我们去完善SocketProcesser内处理Socket方法:

西药修改的是我们的匹配Servlet,需要通过刚刚在TomcatElevenApplication保存到tomcat的map来根据url找到对应的Servlet。

/**
 * 处理 socket
 * @param socket
 */
private void processSocket(Socket socket) {
    try (InputStream inputStream = socket.getInputStream()) {
        byte[] bytes = new byte[1024];
        int read = inputStream.read(bytes);

        if (read <= 0) return;

        // 转成字符串
        String requestText = new String(bytes, 0, read);
        System.out.println("原始请求:\n" + requestText);

        // 按行拆分,第一行是请求行
        String[] lines = requestText.split("\r\n");
        if (lines.length > 0) {
            String requestLine = lines[0]; // 例如: GET /test HTTP/1.1
            String[] parts = requestLine.split(" ");
            if (parts.length >= 3) {
                String method = parts[0];    // GET
                String url = parts[1];       // /test
                String protocol = parts[2];  // HTTP/1.1
                // 封装到 Request 对象
                Request request = new Request(method, url, protocol, socket);
                // 封装到 Response 对象
                Response response = new Response(request);

//                    // 匹配Servlet
//                    ElevenServlet servlet = new ElevenServlet();
//                    // 调用Servlet的service方法,帮助我们判断到底要调用doGet还是doPost等方法
//                    servlet.service(request, response);

                // 判断请求是想访问哪些应用
                String requestUrl = request.getRequestURL().toString();
                // 例如: http://localhost:8080/test
                // 我们要获取 /test 这部分
                String contextPath = requestUrl.substring(requestUrl.indexOf("/", 7), requestUrl.indexOf(":", 7));
                // 从应用中获取 Servlet
                Context context = tomcatElevenApplication.getContextMap().get(contextPath);
                Servlet servlet = context.getByUrlPattern(url);
                if (servlet != null) {
                    servlet.service(request, response);
                    // 发送响应数据
                    response.complete();
                } else {
                    new DefaultServlet().service(request, response);
                    // 404
                    response.setStatus(404, "Not Found");
                    response.complete();
                }
            }
        }
    } catch (IOException e) {
        // 也需要构造一个Response去返回异常提示
        throw new RuntimeException(e);
    } catch (ServletException e) {
        throw new RuntimeException(e);
    }
}

这里为了让没找到也有对应的Servlet,我们设置一个默认的Servlet:

package cn.tomcat.com;

import javax.servlet.http.HttpServlet;

public class DefaultServlet extends HttpServlet {

}