手写一个简易版的tomcat

Tomcat 是一个广泛使用的开源 Servlet 容器,用于运行 Java Web 应用程序。深入理解 Tomcat 的工作原理对于 Java 开发者来说是非常有价值的。本文将带领大家手动实现一个简易版的 Tomcat,通过这个过程,我们可以更清晰地了解 Tomcat 是如何处理 HTTP 请求和响应的。

tomcat涉及到的知识点较多,主要有注解、抽象类、反射、IO流等,对基础掌握度要求较高,掌握这些基础后,我们开始手写tomcat

一、创建包

一个基本的 Tomcat 主要完成以下几个核心功能:

  1. 监听端口:等待客户端的 HTTP 请求。
  2. 解析请求:从客户端请求中提取关键信息,如请求方法、请求路径等。
  3. 处理请求:根据请求信息调用相应的处理逻辑。
  4. 返回响应:将处理结果封装成 HTTP 响应返回给客户端。

根据以上功能,我们创建如下图所示的包

--tomcat类的作用是启动整个tomcat容器

--webapp包下存放你自己创建的servlet动态资源

--httpServletRe包下有两个类HttpServletRequest和HttpservletResponse,这两个类可以说是与前端请求直接关联。

在前端的请求信息中,请求方式和访问路径都要放在HttpServletRequest类中,可以说是相当的重要,我们可以通过socket将前端信息装到该类中,具体写法之后细讲,也就是说我们知道该类有请求方式和访问路径,通过和项目本身的资源对比从而定位到某个实际的静态资源或动态资源

而HttpServletResponse则是向前端发送我们自己写的消息,依赖outputStream来完成,该消息需要遵循响应信息的特定格式,之后的工具类中会给出

--servlet包下则是仿照Java类库中的servlet继承关系,创建了servlet接口,Gservlet抽象类和Httpservlet抽象类

--util包下存放两个工具类,一个类的作用是找到webapp下所有servlet类的类路径,用来进行反射;

一个类的作用是创建响应消息的返回格式,包展开如下:

二、写tomcat逻辑

要想得到前端发送的请求,我们首先需要创建一个ServerSocket对象实例来接收:

java 复制代码
ServerSocket serverSocket = new ServerSocket(9090);
while(true){
    Socket socket = serverSocket.accept();
}

写一个while语句循环接收前端请求,一旦接收到请求,我们就可以创建输入流对象,将前端发送的消息存下来,在这里我们先只获取请求行的信息也就是第一行的信息

java 复制代码
BufferedReader bufferedReader = new BufferedReader(new                             
         InputStreamReader(socket.getInputStream()));
String s = bufferedReader.readLine();

可以看到,浏览器中第一行有请求方式和访问路径,我们是以字符串形式接收的第一行数据,所以可以通过split()方法将请求方式和访问路径分开

java 复制代码
String[] s1 = s.split(" ");

tomcat代码展示:(端口的话可以自己选,我写的是9090,tomcat默认端口是8080)

三、写HttpServletRequest的逻辑

得到请求方式和访问路径之后,我们需要将请求方式和访问路径封装到HttpServletRequest类中,在HttpServletRequest类中定义字符串类型的变量url和method,创建getter和setter方法:

java 复制代码
public class HttpServletRequest {
    private String url;
    private String method;
    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getMethod() {
        return method;
    }

    public void setMethod(String method) {
        this.method = method;
    }
}

四、补充tomcat逻辑

我们已经定义好HttpServletRequest类,接下来我们将tomcat类中拿到的请求方式和访问路径赋给该类中的变量method和url,首先在tomcat创建HttpServletRequest类的实例对象request,调用request的set方法,入参是inputStream获取到的请求方式和访问路径

代码如下:

写到该阶段我们可以先测试一下,在HttpServletRequest中重写toString方法,在tomcat中调用

到浏览器上输入http://localhost:9090,发现控制台输出:

请求方法和访问路径成功封装到了request中,我们继续下面的逻辑。

五、仿写servlet的继承关系

servlet包下是仿照Java类库中的servlet继承关系,创建了servlet接口,Gservlet抽象类和Httpservlet抽象类,我们现在在这三个类中添加逻辑

Servlet接口

该接口中定义以下方法:init方法、service方法和destroy方法,service方法用来判断是get请求还是post请求,在本接口中只定义,在HttpServlet抽象类中实现。其中有两个入参,HttpServletRequest类型的request和HttpservletResponse的response,之前request对象封装过请求方式,该方法会通过拿到入参的请求方式判断是get请求还是post请求

java 复制代码
package com.qcby.servlet;

import com.qcby.httpServletRe.HttpServletRequest;
import com.qcby.httpServletRe.HttpServletResponse;

public interface Servlet {
    void init();
    void service(HttpServletRequest request, HttpServletResponse response) throws Exception;
    void destroy();
}

Gservlet抽象类

该抽象类继承Servlet接口并实现init方法和destroy方法,这两种方法我们不写实际功能,了解java类库中这两种方法的作用即可

java 复制代码
package com.qcby.servlet;

public abstract class Gservlet implements Servlet{
    @Override
    public void init() {
        System.out.println("初始化");
    }

    @Override
    public void destroy() {
        System.out.println("销毁");
    }
}

HttpServlet抽象类

该类继承Gservlet抽象类,实现了Servlet接口中的service方法,此外HttpServlet类中还增加了两种方法,doGet方法和doPost方法,相信学过servlet的小伙伴们并不陌生,doGet方法和doPost方法作为抽象方法不具体实现,子类也就是我们自己创建的servlet类继承该类时就必须实现该方法,至于service方法是用来区分get方法和post方法的,所以之前的request入参就是为了提供method做if判断

java 复制代码
package com.qcby.servlet;

import com.qcby.httpServletRe.HttpServletRequest;
import com.qcby.httpServletRe.HttpServletResponse;

import java.io.IOException;

public abstract class HttpServlet extends Gservlet{
    @Override
    public void service(HttpServletRequest request, HttpServletResponse response) throws Exception{
        if(request.getMethod().equals("GET")){
            doGet(request,response);
        }else{
            doPost(request,response);
        }
    }
    public abstract void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException;
    public abstract void doPost(HttpServletRequest request, HttpServletResponse response);
}

六、创建注解类WebSocket

在工具包下创建WebSocket注解类,该注解类的作用是定义servlet类的url路径

java 复制代码
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(value = ElementType.TYPE)
@Retention(value = RetentionPolicy.RUNTIME)
public @interface WebSocket {
    public String url() default "";
}

七、创建servlet容器

tomcat启动时,Servlet 容器需要将客户端请求的 URL 路径映射到具体的 Servlet 上,以便正确处理请求。为了实现这一功能,容器使用 Map 来存储 URL 路径和 Servlet 名称或实例之间的映射关系。

要想获取URL路径和Servlet实例对象,我们首先要通过反射获取到类的类信息,而获取类信息需要获取类的全路径,所以写一个工具类获取webapp包下所有类的全路径,然后初始化map容器

java 复制代码
import com.qcby.servlet.HttpServlet;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;

public class FindServletAllPath {
    public static HashMap<String, HttpServlet> map = new HashMap<>();
    public static List<String> getClassPaths(String packageName) throws IOException, ClassNotFoundException {
        List<String> classPaths = new ArrayList<>();
        // 将包名转换为文件系统路径
        String path = packageName.replace('.', '/');
        // 获取类加载器
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        // 获取指定路径下的所有资源
        Enumeration<URL> resources = classLoader.getResources(path);
        while (resources.hasMoreElements()) {
            URL resource = resources.nextElement();
            // 获取资源的文件路径
            String filePath = resource.getFile();
            // 递归扫描目录
            scanDirectory(new File(filePath), packageName, classPaths);
        }
        return classPaths;
    }

    /**
     * 递归扫描目录,查找所有的 .class 文件
     * @param directory 要扫描的目录
     * @param packageName 当前包名
     * @param classPaths 存储类路径的列表
     */
    private static void scanDirectory(File directory, String packageName, List<String> classPaths) {
        if (!directory.exists()) {
            return;
        }
        // 获取目录下的所有文件和文件夹
        File[] files = directory.listFiles();
        if (files != null) {
            for (File file : files) {
                if (file.isDirectory()) {
                    // 递归扫描子目录
                    scanDirectory(file, packageName + "." + file.getName(), classPaths);
                } else if (file.getName().endsWith(".class")) {
                    // 获取类名
                    String className = packageName + '.' + file.getName().substring(0, file.getName().length() - 6);
                    classPaths.add(className);
                }
            }
        }
    }

    static{
        try {
            // 指定要扫描的包名
            String packageName = "com.qcby.webapp";
            // 获取类路径列表
            List<String> classPaths = getClassPaths(packageName);
            // 打印类路径
            for (String classPath : classPaths) {
                Class<?> aClass = Class.forName(classPath);
                WebSocket annotation = aClass.getAnnotation(WebSocket.class);
                HttpServlet o = (HttpServlet) aClass.newInstance();
                map.put(annotation.url(),o);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

我们把这个过程装入static块中,这样在类加载时就能完成容器的映射

八、创建servlet动态资源

在webapp包下创建一个servlet类,继承HttpServlet抽象类,重写doGet和doPost方法,各自添加一个输出语句,添加WebSocket注解,确定该类的url路径

java 复制代码
import com.qcby.httpServletRe.HttpServletRequest;
import com.qcby.httpServletRe.HttpServletResponse;
import com.qcby.servlet.HttpServlet;
import com.qcby.util.WebSocket;

import java.io.IOException;

@WebSocket(url = "/first")
public class FirstServlet extends HttpServlet {
    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        System.out.println("first的doGet方法被调用");

    }

    @Override
    public void doPost(HttpServletRequest request, HttpServletResponse response) {
        System.out.println("first的doPost方法被调用");
    }
}

九、补充tomcat逻辑

前端发送的请求方式和url路径已经封装到request对象当中,map中k值存放着请求路径,通过map映射可以找到对应的servlet实例对象,此时servlet的引用类型是父类类型,我们可以直接调用父类的方法service,有两个参数request和response,创建一个response对象放入对应参数位置

java 复制代码
if(FindServletAllPath.map.containsKey(request.getUrl())){
   HttpServlet servlet = FindServletAllPath.map.get(request.getUrl());
   servlet.service(request,response);
}

进入service方法后,会通过获取request的请求方式判断是执行doget方法还是doPost方法,因为方法被子类重写,所以最终会调用到实际servlet的doget方法或者doPost方法

十、写HttpServletResponse逻辑

我们已经完成了请求的接受和处理,接下来需要将信息返回给前端

创建BufferOutputStream包装流将socket.outputStream包装后进行发送,需要注意的是,发送回前端的数据需要遵循特定的响应格式,我们再util包下创建一个工具类将数据封装到固定格式中

java 复制代码
package com.qcby;

public class ResponseUtil {
    public  static  final String responseHeader200 = "HTTP/1.1 200 \r\n"+
            "Content-Type:text/html; charset=utf-8 \r\n"+"\r\n";

    public static String getResponseHeader404(){
        return "HTTP/1.1 404 \r\n"+
                "Content-Type:text/html; charset=utf-8 \r\n"+"\r\n" + "404";
    }

    public static String getResponseHeader200(String context){
        return "HTTP/1.1 200 \r\n"+
                "Content-Type:text/html; charset=utf-8 \r\n"+"\r\n" + context;
    }
}

之后通过bufferoutputStream的write方法将数据返回给前端,注意发送后需要进行刷新

java 复制代码
import com.qcby.ResponseUtil;
import java.io.BufferedOutputStream;
import java.io.IOException;

import java.net.Socket;
import java.nio.charset.StandardCharsets;

public class HttpServletResponse {
    Socket socket;
    public HttpServletResponse(Socket socket){
        this.socket = socket;
    }
    public void write(String s) throws IOException {
        BufferedOutputStream outputStream = new BufferedOutputStream(socket.getOutputStream());
        s = ResponseUtil.getResponseHeader200(s);
        outputStream.write(s.getBytes(StandardCharsets.UTF_8));
        outputStream.flush();
        outputStream.close();
    }
}

tomcat的整体流程如上,现在测试一下接收前端数据以及向前端返回数据信息:

相关推荐
校长20084 分钟前
mac安装java环境
java
简 洁 冬冬35 分钟前
java中过滤器
java
V+zmm101341 小时前
电器维修系统小程序+论文源码调试讲解
java·数据库·微信小程序·小程序·毕业设计
PawSQL1 小时前
推理模型对SQL理解能力的评测:DeepSeek r1、GPT-4o、Kimi k1.5和Claude 3.7 Sonnet
java·数据库·人工智能·sql·sql优化·pawsql·deepseek
BeanInJ1 小时前
JAVA字符串与正则表达式
java·正则表达式
珹洺1 小时前
数据库系统概论(三)数据库系统的三级模式结构
java·运维·服务器·数据库·oracle
算法与编程之美2 小时前
冒泡排序
java·开发语言·数据结构·算法·排序算法
Aphelios3802 小时前
Java 学习记录:基础到进阶之路(一)
java·开发语言·学习·idea
程序员麻辣烫2 小时前
晋升系列4:学习方法
java·数据库·程序人生·学习方法
爱学习的小王!2 小时前
有关MyBatis的动态SQL
java·笔记·sql·学习·mybatis