Featured image of post Java工程师 框架 手写Tomcat

Java工程师 框架 手写Tomcat

🌏Java工程师 手写Tomcat 🎯 这篇文章用于记录 模拟实现Tomcat的主要功能 实现一个简易版的Tomcat 从而更加深入地理解服务器原理

🎄概述

我将模拟实现一个Tomcat,它的名字叫YellowBabyDuck(小黄鸭服务器),与Tomcat显眼的区别在于,它俩的logo很不一样

🎄开始

🍭创建一个Maven项目

引入Servlet的依赖

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>3.1.0</version>
</dependency>

🍭异步BIO方式获取并处理Socket连接

🌴版本1.1设计 只能处理一个请求

分析 下面第一个版本的设计❌只能接收一个请求 ❌接收之后服务器程序便终止了

package com.bigbigmeng;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

/**
@author Liu Xianmeng
@createTime 2023/10/4 18:46
@instruction 小黄鸭服务器类
*/

@SuppressWarnings({"all"})
public class YellowBabyDuctServer {
    private void start() {
        try {
            ServerSocket serverSocket = new ServerSocket(9999);
            // 只能接收一个请求
            Socket accept = serverSocket.accept();
            processSocket(accept);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private void processSocket(Socket accept) {
        // ...
    }

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

🌴版本1.2设计 只能同步处理请求

修改start()使得服务器可以不间断地接收请求 但还是存在一个重要的问题 那就是整个服务器❌只能处理同步请求 ❌只有上一个程序请求处理完 才能接收处理下一个请求

private void start() {
    try {
        // 开启一个服务器端口
        ServerSocket serverSocket = new ServerSocket(9999);
        // 只能接收一个请求
        Socket accept = serverSocket.accept();
        processSocket(accept);
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

🌴版本1.3设计 处理异步请求

新建一个类SocketProcessor 专门用来处理Socket连接

package com.bigbigmeng;

import java.net.Socket;

/**
@author Liu Xianmeng
@createTime 2023/10/4 19:05
@instruction SocketProcessor类 专门用来处理一个Socket连接

    [1] SocketProcessor类本身要是一个可执行的Task 所以这里让它去🟪实现Runnable接口重写run()方法
    [2] 既然它用来处理一个Socket连接 那么它就应该持有一个⚡Socket连接

*/

@SuppressWarnings({"all"})
public class SocketProcessor implements Runnable {

    // ⚡持有一个Socket连接
    private Socket socket;

    public SocketProcessor(Socket socket) {
        this.socket = socket;
    }

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

    private void processSocket(Socket socket) {
    }
}

修改YellowBabyDuctServer

package com.bigbigmeng;

/**
@author Liu Xianmeng
@createTime 2023/10/4 18:46
@instruction 小黄鸭服务器类
*/
public class YellowBabyDuctServer {
    private void start() {
        try {
            // 开启一个服务器端口
            ServerSocket serverSocket = new ServerSocket(9999);
            // 创建一个线程池执行器来执行请求 线程池大小为20
            ExecutorService executorService = Executors.newFixedThreadPool(20);
            while(true) {
                // 接收一个socket请求
                Socket socket = serverSocket.accept();
                // 扔给线程池进行处理
                executorService.execute(new SocketProcessor(socket));
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    public static void main(String[] args) {
        YellowBabyDuctServer yellowBabyDuctServer = new YellowBabyDuctServer();
        yellowBabyDuctServer.start();
    }
}

❓服务器具体如何处理请求?

看下面SocketProcessor 类的processSocket()方法

/**
 * 处理传入的Socket
 * @param socket
 */
private void processSocket(Socket socket) {
    try {
        InputStream is = socket.getInputStream();
        // 每次读1024B
        byte[] bytes = new byte[1024];
        is.read(bytes);
        // 读完以后打印出来
        for(byte b : bytes) {
            System.out.print((char)b);
        }
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

测试一下 查看后端日志

这样的打印结果还不够清晰 因此我们可以使用BufferedReader来按行进行读取 并打印

private void processSocket(Socket socket) {
    try {
        InputStream is = socket.getInputStream();
        // 修改读取方式 按行读取并打印
        BufferedReader br = new BufferedReader(new InputStreamReader(is));
        String strLine = null;
        while((strLine = br.readLine()) != null) {
            System.out.println(strLine);
        }
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

执行测试 看到http的请求头被打印出来了 可以看到请求头中是有很多字段可以被服务器进行解析的 例如第一行的 请求方式GET 请求路径/ 请求协议HTTP/1.1

GET / HTTP/1.1
User-Agent: PostmanRuntime/7.33.0
Accept: */*
Cache-Control: no-cache
Postman-Token: 4c57ed99-34b9-46c1-aeb7-966225a8ffee
Host: localhost:9999
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Cookie: ticket=ee85af76cc8a40bca2f86c8173a3e787

再用POST的方式携带数据请求 看看会打印怎样的结果

当对以上的所有字段进行解析之后 就可以得到一个Request对象 在传统的JavaWeb的开发里 这个对象就是常用的HttpServletRequest 紧接着 我们来创建这个Request对象

🌴创建Request对象 封装请求

这个Request对象就是原本Tomcat应该封装的HttpServletRequest对象

🎯可以看到下面的JavaWeb的开发实例场景 这个代码就用到了HttpServletRequest对象 Tomcat会自动将请求中的一系列字段封装都这个对象 并共后端进行使用和操作

/**
@author Liu Xianmeng
@createTime 2022/8/4 10:36
@instruction
*/

@SuppressWarnings({"all"})
@WebServlet("/cart") // 专门用来处理关于购物车的请求的
public class CartCtrller extends BasicCtrller {

    private CartServ cs = new CartServImpl();
    private CartDao cd = new CartDao(); // 专门用来处理数据库表中的数据的

    /**
     * 这个函数就实现刚才向所登录的用户的购物车中添加一个产品(所点击添加的那个产品)
     */
    public void addOne(🎯HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("C CartCtrller M addOne()...");
        // 要获取从请求传递过来的参数 参数有哪些?
        String furnName = 🎯req.getParameter("name");
        String memberId = 🎯req.getParameter("memberId");
        String furnId = 🎯req.getParameter("furnId");
        ...
    }
}

接下来我们来创建小黄鸭服务器的Request对象 这个对象用来封装上面测试控制台打印出来的HTTP报文的所有内容

package com.bigbigmeng;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.concurrent.ConcurrentHashMap;

/**
@author Liu Xianmeng
@createTime 2023/10/4 20:12
@instruction
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Request {
    private String method; // 请求方法
    private String path; // 请求路径
    private String protocol; // 协议
    /**
     * 每一个socket都应该持有一个Request对象 所以不需要考虑线程安全的问题
     * 直接使用Properties来存储其他的属性
     */
    private Properties properties = new Properties();
    // 使用这个方法来返回属性
    public String getParamater(String key) {
        return properties.getProperty(key);
    }
}

上面代码使用了lombok注解 因为我引入了lombok

<!-- 20231004 引入lombok -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.24</version>
    <scope>provided</scope>
</dependency>

创建好Request对象后 就可以将请求的参数封装起来了 下面是封装过程

/**
 * 处理传入的Socket
 *
 * @param socket
 */
private void processSocket(Socket socket) {
    try {
        InputStream is = socket.getInputStream();
        // 使用BufferedReader包装流 它能提供更强的读取功能
        BufferedReader br = new BufferedReader(new InputStreamReader(is));
        // 创建一个Request对象 用来封装所有请求参数
        Request request = new Request();
        // 封装请求参数
        // 定义一个变量来接收br读取到的行字符串
        String curLine = null;
        try {
            // 先读取第一行来获取 请求方式|路径|协议
            curLine = br.readLine();
            if(curLine == null) return;
            // 先封装 请求方式|路径|协议
            String[] firstLineThreeStr = curLine.split(" ");
            request.setMethod(firstLineThreeStr[0]);
            request.setPath(firstLineThreeStr[1]);
            request.setProtocol(firstLineThreeStr[2]);
            // 继续封装剩下的参数
            while((curLine = br.readLine()) != null) {
                String[] twoPartStr = curLine.split("\\: ");
                if(twoPartStr[0].equals("Cookie")) {
                    // 关于Cookie的封装可以放到后面再进行
                } else {
                    // 封装其他的请求头
                    if(twoPartStr.length > 1) {
                        request.getProperties().setProperty(twoPartStr[0], twoPartStr[1]);
                    }
                }
            }
            // 判断请求是否携带数据
            if(request.getMethod().equals("POST")) {
                // 封装数据 后面再加处理逻辑
            }
            System.out.println("C SocketProcessor M processSocket() -> request = " + request);
            BigBigMengServlet bigBigMengServlet = new BigBigMengServlet();
            // 调用service方法 -> 根据请求参数再决定调用GET还是POST方法
            // 传入刚刚写的Request和Response对象
            bigBigMengServlet.doGet(request, response);
            // 执行完Servlet之后就可以发送响应了
            response.complete();
            socket.close(); // 关闭socket连接
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            /*
            try {
                br.close();
                socket.close();
                System.out.println("C SocketProcessor M processSocket() -> 连接关闭");
            } catch (IOException e) {
                throw new RuntimeException(e);
            }*/
        }
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

请求测试结果如下 Request对象将请求的参数封装起来了

🍭按照Servlet规范实现Request和Response

实现Request

先写一个抽象类AbstractHttpServletRequest实现HttpServletRequest接口

package com.bigbigmeng;
/**
@author Liu Xianmeng
@createTime 2023/10/4 21:46
@instruction
*/
public class AbstractHttpServletRequest implements HttpServletRequest {
    @Override
    public String getAuthType() {
        return null;
    }
    @Override
    public Cookie[] getCookies() {
        return new Cookie[0];
    }
    ...
}

然后让刚刚写的Request类继承AbstractHttpServletRequest

package com.bigbigmeng;
/**
@author Liu Xianmeng
@createTime 2023/10/4 20:12
@instruction
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Request extends AbstractHttpServletRequest {
    private String method; // 请求方法
    private String requestUrl; // 请求路径
    private String protocol; // 协议
    /**
     * 每一个socket都应该持有一个Request对象 所以不需要考虑线程安全的问题
     * 直接使用Properties来存储其他的属性
     */
    private Properties properties = new Properties();
    // 使用这个方法来返回属性
    @Override
    public String getParamater(String key) {
        return properties.getProperty(key);
    }
    @Override
    public String getMethod() {
        return method;
    }
    @Override
    public String getProtocol() {
        return method;
    }
    @Override
    public String getRequestURI() {
        return requestUrl;
    }
    ...
}

实现Response

先写一个抽象类AbstractHttpServletResponse实现HttpServletResponse接口

package com.bigbigmeng;
/**
@author Liu Xianmeng
@createTime 2023/10/5 9:18
@instruction
*/
public class AbstractHttpServletResponse implements HttpServletResponse {
    @Override
    public void addCookie(Cookie cookie) {
    }
    @Override
    public boolean containsHeader(String name) {
        return false;
    }
    ...
}

然后再写一个Response类继承AbstractHttpServletResponse

package com.bigbigmeng;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.*;
import java.util.HashMap;
import java.util.Map;

/**
@author Liu Xianmeng
@createTime 2023/10/5 9:18
@instruction
*/

@SuppressWarnings({"all"})
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Response extends AbstractHttpServletResponse {
    private int status = 200; // 状态码
    private String message = "OK"; // 返回消息
    private Map<String, String> headers = new HashMap<>();
    private Request request; // 一个Response对应一个Request对象
    private OutputStream socketOutputStream; // Response要用outputStream返回内容
    // 响应体的outputStream 暂存响应数据
    private ResponseServletOutputStream responseServletOutputStream = new ResponseServletOutputStream();
    private byte SP = ' ';
    private byte CR = '\r'; // 换行
    private byte LF = '\b';

    /**
     * 完成最后的响应
     */
    public void complete() throws IOException {
        sendResponseLine();
        sendResponseHeader();
        sendResponseBody();
    }
    
    // 写响应行 响应的第一行
    private void sendResponseLine() throws IOException {
        String nextLine = "HTTP/1.1 " + status + " " + message;
        socketOutputStream.write(nextLine.getBytes());
        socketOutputStream.write('\r');
        socketOutputStream.write('\n');
    }

    // 写响应头
    private void sendResponseHeader() throws IOException {
        for(String key : headers.keySet()) {
            socketOutputStream.write((key + ": " + headers.get(key)).getBytes());
            // “\r\n”是一个换行
            socketOutputStream.write('\r');
            socketOutputStream.write('\n');
        }
        socketOutputStream.write('\r');
        socketOutputStream.write('\n');
    }
    
    // 写返回的数据
    private void sendResponseBody() throws IOException {
        // 获取数据
        int count = responseServletOutputStream.getPos();
        byte[] tempData = responseServletOutputStream.getBytes();
        byte[] data = new byte[count];
        for(int i = 0; i < count; ++i) {
            data[i] = tempData[i];
        }
        // 写出数据
        socketOutputStream.write(data);
    }

    @Override
    public void addHeader(String name, String value) {
        this.headers.put(name, value);
    }

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

编写一个测试Servlet -> BigBigMengServlet

package com.bigbigmeng;
/**
@author Liu Xianmeng
@createTime 2023/10/5 9:36
@instruction 
*/
public class BigBigMengServlet extends HttpServlet {
    /********** 重写三个处理请求的方法 ********/
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("C BigBigMengServlet M doGet() -> method = " + req.getMethod());
        // 🎯🎯🎯写回返回的数据
        resp.getOutputStream().write("C BigBigMengServlet M doGet()".getBytes());
        resp.addHeader("Content-Length", "C BigBigMengServlet M doGet()".length() + "");
        resp.addHeader("Content-Type", "text/plain;charset=utf-8");
        System.out.println("断定调试...");
    }
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doPost(req, resp);
    }
    @Override
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        super.service(req, res);
    }
}

修改SocketProcessor类的processSocket方法

...
// 判断请求是否携带数据
if(request.getMethod().equals("POST")) {
    // 封装数据 后面再加处理逻辑
}
logger.info("request = " + request);
BigBigMengServlet bigBigMengServlet = new BigBigMengServlet();
// 调用service方法 -> 根据请求参数再决定调用GET还是POST方法
// 传入刚刚写的Request和Response对象
bigBigMengServlet.service(request, response);
...

执行测试 后面的报错空指针是因为自己写的Response类没有重写getOutputStream()方法

🍭暂存响应体

新建一个ResponseServletOutputStream类

package com.bigbigmeng;

/**
@author Liu Xianmeng
@createTime 2023/10/5 10:25
@instruction
*/
@Data
public class ResponseServletOutputStream extends ServletOutputStream {
    private byte[] bytes = new byte[2048]; // 暂存响应数据
    private int pos = 0;
    @Override
    public void write(int b) throws IOException {
        bytes[pos++] = (byte) b;
    }
}

Response类添加@Override方法

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

🍭按照HTTP协议发送数据

修改Request类 添加socket属性

public class Request extends AbstractHttpServletRequest {
    ...
    private Socket socket; // 一个请求持有一个socket
}

修改SocketProcessor类的processSocket方法 创建Request对象的时候setSocket

// 创建一个Request对象 用来封装所有请求参数
Request request = new Request();
request.setSocket(socket);

修改SocketProcessor类的processSocket方法 创建Response对象的时候setRequest

...
Response response = new Response();
response.setRequest(request);
response.setOutputStream(request.getSocket().getOutputStream());
...

⚡阶段测试

服务端持续监听

Postman的请求结果

🍭Tomcat部署应用实现

🌴补全小黄鸭服务器的目录

🌴部署Servlet 阶段1 获取Servlet的Class对象

添加@WebServlet(urlPatterns = {"/test"})注解 并重新编译

package com.bigbigmeng;

/**
@author Liu Xianmeng
@createTime 2023/10/5 9:36
@instruction
*/
@WebServlet(urlPatterns = {"/test"})
public class BigBigMengServlet extends HttpServlet {
    ...
}

编译后将其放到webapps/app/classes/com/bigbigmeng目录下

修改项目访问Servlet的方式 注销SocketProcessor类中对BigBigMengServlet的调用

主类YellowBabyDuckServer编写🎯deployApps()方法 用于部署项目

package com.bigbigmeng;

/**
@author Liu Xianmeng
@createTime 2023/10/4 18:46
@instruction 小黄鸭服务器类
*/
public class YellowBabyDuctServer {
    ...
    public static void main(String[] args) {
        YellowBabyDuctServer yellowBabyDuctServer = new YellowBabyDuctServer();
        deployApps();
        yellowBabyDuctServer.start();
    }
    // 🎯部署项目
    private static void deployApps() {
        File file = new File(System.getProperty("user.dir"), "webapps");
        for (String app : file.list()) {
            System.out.println(app);
        }
    }
}

打印查看

新建方法deployApp()

/**
 * 在这个方法中 服务器需要获取到所有的Servlet并登记其路径
 * 以便在调用Servlet的时候直接根据请求路径进行get
 * @param webapps
 * @param appName
 */
private static void deployApp(File webapps, String appName) {
    // 指定父文件夹和子文件名可以获取子文件夹
    File appDirectory = new File(webapps, appName);
    File classDirectory = new File(appDirectory, "classes");
    List<File> allFiles = new ArrayList<>();
    getAllFiles(allFiles, classDirectory);
    // 判断哪些文件是.class文件
    for (File file : allFiles) {
        String path = file.getPath();
        // 把父目录的路径删除
        path = path.replace(classDirectory.getPath() + "\\", "");
        // 删除文件后缀名
        path = path.replace(".class", "");
        // 把路径的/替换为.
        path = path.replace("\\", ".");
        // 把全类名打印出来
        System.out.println("C YellowBabyDuctServer M deployApp() -> 当前全类名 = " + path);
        // 使用反射判断当前的class对象是不是Servlet
        try {
            // 创建classLoader对象
            ServerClassLoader classLoader = new ServerClassLoader(new URL[]{classDirectory.toURL()});
            // 加载当前全类名对应的java对象
            Class<?> aClass = classLoader.loadClass(path);
            // 判断是不是一个Servlet
            if(HttpServlet.class.isAssignableFrom(aClass)) {
                // 如果是HttpServlet的子类 则它是一个Servlet
                System.out.println("C YellowBabyDuctServer M deployApp() -> class对象 = " + aClass);
                System.out.println("...");
            }
        } catch (MalformedURLException e) {
            throw new RuntimeException(e);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }
}

// 获取某个文件夹下的所有文件
private static void getAllFiles(List<File> list, File classDirectory) {
    for (File file : classDirectory.listFiles()) {
        if(!file.isFile()) {
            getAllFiles(list, file);
        }
        else list.add(file);
    }
}

⚡阶段测试

🌴部署Servlet 阶段2 完成url和Servlet的映射

新建Context类

package com.bigbigmeng;

/**
@author Liu Xianmeng
@createTime 2023/10/5 17:24
@instruction 上下文类 服务器部署App 一个App对应一个Context
*/
@Data
public class Context {
    // app的名字
    private String appName;
    // app的url和Servlet之间的额映射关系
    private Map<String, Servlet> urlMapping = new HashMap<>();
    public void addServletUrlMapping(String url, Servlet servlet) {
        urlMapping.put(url, servlet);
    }
}

给小黄鸭服务器添加一个🎯新的属性

/**
@author Liu Xianmeng
@createTime 2023/10/4 18:46
@instruction 小黄鸭服务器类
*/
public class YellowBabyDuctServer {
    // app和上下文的映射map
    private static HashMap<String, Context> 🎯contextMap = new HashMap<>();
    public static HashMap<String, Context> getContextMap() {
        return contextMap;
    }
    ...
}

完成url和Servlet的映射

/**
 * 在这个方法中 服务器需要获取到所有的Servlet并登记其路径
 * 以便在调用Servlet的时候直接根据请求路径进行get
 * @param webapps
 * @param appName
 */
private static void deployApp(File webapps, String appName) {
    // 创建一个上下文对象 一个应用对应一个上下文对象
    Context context = new Context();
    context.setAppName(appName);
    ...
    // 判断哪些文件是.class文件
    for (File file : allFiles) {
        ...
        try {
            ...
            if(HttpServlet.class.isAssignableFrom(servletClazz)) {
                ...
                // 如果这个class对象被@WebService注解修饰 则获取映射路径
                if(servletClazz.isAnnotationPresent(WebServlet.class)) {
                    WebServlet annotation = servletClazz.getAnnotation(WebServlet.class);
                    String[] urlPatterns = annotation.urlPatterns();
                    // 创建一个Servlet对象
                    Servlet servlet = (Servlet) servletClazz.newInstance();
                    // 添加映射关系
                    for (String urlPattern : urlPatterns) {
                        context.addServletUrlMapping(urlPattern, servlet);
                    }
                }
            }
        } catch (Exception e) {
            ...
        } 
    }
    // 处理完之后给服务器添加context的映射
    contextMap.put(appName, context);
}

修改SocketProcessor类 🎯完成根据请求路径映射Servlet

这个地方我将之前的processSocket()函数弃用 直接把Runnable的任务写在run()方法里面

package com.bigbigmeng;
/**
@author Liu Xianmeng
@createTime 2023/10/4 19:05
@instruction SocketProcessor类 专门用来处理一个Socket连接
*/
public class SocketProcessor implements Runnable {
	...
    @Override
    public void run() { // 🟪
        try {
            ...
                // 判断请求是否携带数据
                if(request.getMethod().equals("POST")) {
                    // 封装数据 后面再加处理逻辑
                }
                System.out.println("C SocketProcessor M processSocket() -> request = " + request);
                /**
                 * 🎯调用service方法 -> 根据请求参数再决定调用GET还是POST方法
                 * 传入刚刚写的Request和Response对象
                 */
                //❌BigBigMengServlet bigBigMengServlet = new BigBigMengServlet();
                //❌bigBigMengServlet.doGet(request, response);
            	
            	/*********** 20231005 重新处理url和Servlet调用的映射 开始 **********/
                // 获取请求路径 /app/test /app映射到应用 后面的/text映射到应用下的servelet
                String originUrl = request.getRequestUrl();
                // 使用StringBuilder对象进行处理
                StringBuilder sb = new StringBuilder(originUrl);
                // 删除第一个/
                sb.delete(0,1);
                // 获取app的名字
                String appName = sb.substring(0, sb.indexOf("/"));
                // 删除app的名字 -> /app
                sb.delete(0, sb.indexOf("/"));
                // 获取请求路径 -> 对应Servlet
                Context context = YellowBabyDuctServer.getContextMap().get(appName);
                // 根据originUrl请求路径获取 -> 对应Servlet
                Servlet servlet = context.getUrlMapping().get(sb.toString());
                // 执行目标Servlet
                servlet.service(request, response);
            	/*********** 20231005 重新处理url和Servlet调用的映射 结束 **********/

                // 执行完Servlet之后就可以发送响应了
                response.complete();
                socket.close(); // 关闭socket连接
            } catch (IOException e) {
                throw new RuntimeException(e);
            } finally {
                ...
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 处理传入的Socket
     * @param socket
     */
    private void processSocket(Socket socket) {
        // 清空弃用这个函数
    }
}

⚡阶段测试

成功响应

到此 一个简易功能的YellowBabyDuck服务器(模拟实现Tomcat)✨就实现了~ 如果想让其支持更加复杂的功能 后续可以进行补充