用300行代码手写一个mini版的Tomcat

用300行代码实现一个简易版的HTTP服务器

Java Web开发离不开Tomcat这样的应用服务器,但其内部工作原理往往不为开发者所知。本文通过构建一个简易版本的HTTP服务器(名为TinyTomcat),以约300行纯Java代码的方式,帮助读者深入理解Tomcat处理请求的本质:监听端口、解析协议、调度响应。通过这个过程,你将更好地掌握HTTP服务器的核心流程和技术细节。

核心设计思路

构建一个简易的HTTP服务器需要关注以下几个关键步骤:

  1. 监听端口与初始化 :启动服务并设置监听端口。
  2. 请求解析和路由处理 :读取客户端发送的HTTP请求,解析出方法(如GET、POST)和URL路径,并根据预设规则将请求分发到相应的处理器。
  3. 响应生成 :根据业务逻辑构建标准格式的HTTP响应并返回给客户端。

基于上述流程,设计了五个核心类:

  1. SimpleTomcat (服务器引擎) :启动服务和监听端口,协调所有工作流程。
  2. SimpleRequest (请求解析器) :将原始文本形式的HTTP请求转换为内部使用的Java对象。
  3. SimpleResponse (响应构建器) :根据业务逻辑生成符合HTTP协议格式的响应。
  4. SimpleServlet (处理接口) :定义动态处理器必须遵循的标准规范。
  5. HelloServlet (具体实现) :展示一个具体的业务逻辑示例。

构建服务器引擎 (SimpleTomcat.java)

服务启动

import java.io.*;
import java.net.*;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.*;
import java.time.*;
import java.time.format.*;

/**
 * 简易HTTP服务器 - 核心类
 */
public class SimpleTomcat {
    private int port = 8080;
    private String webRoot = ".";
    private ServerSocket serverSocket;
    private ExecutorService threadPool;
    private boolean running = false;

    // 路由映射表:路径 -> Servlet实例
    private Map<String, SimpleServlet> servletMapping = new ConcurrentHashMap<>();

    public SimpleTomcat(int port, String webRoot) {
        this.port = port;
        this.webRoot = webRoot;
        this.threadPool = Executors.newFixedThreadPool(20);
    }

    public void start() throws IOException {
        serverSocket = new ServerSocket(port);
        running = true;
        System.out.printf("🚀 SimpleTomcat 启动在 http://localhost:%d\n", port);
        System.out.printf("📁 静态文件目录: %s\n", new File(webRoot).getAbsolutePath());

        // 注册默认处理器
        registerDefaultServlets();

        while (running) {
            Socket client = serverSocket.accept();
            threadPool.submit(() -> handleClient(client));
        }
    }

    public void stop() {
        running = false;
        try {
            if (serverSocket != null) serverSocket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        threadPool.shutdown();
    }

    // 注册Servlet
    public void addServlet(String path, SimpleServlet servlet) {
        servletMapping.put(path, servlet);
        System.out.printf("📋 注册Servlet: %s -> %s\n", path, servlet.getClass().getSimpleName());
    }

    private void registerDefaultServlets() {
        addServlet("/hello", new HelloServlet());
        addServlet("/time", (req, res) -> {
            res.setContentType("text/plain; charset=utf-8");
            res.getWriter().write("当前时间: " + Instant.now().toString());
        });
    }

路由处理

private void handleClient(Socket client) {
    try (client;
         BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
         OutputStream out = client.getOutputStream()) {

        // 读取请求行
        String requestLine = in.readLine();
        if (requestLine == null) return;

        String[] parts = requestLine.split(" ");
        if (parts.length < 3) return;

        String method = parts[0];
        String path = parts[1];

        // 创建请求/响应对象
        SimpleRequest request = new SimpleRequest(method, path, in);
        SimpleResponse response = new SimpleResponse(out);

        // 记录访问日志
        logRequest(client.getInetAddress().getHostAddress(), method, path);

        // 路由处理逻辑
        if (path.equals("/")) {
            serveWelcomePage(response);
        } else if (servletMapping.containsKey(path)) {
            // 动态Servlet处理
            servletMapping.get(path).service(request, response);
        } else if (path.equals("/favicon.ico")) {
            serveFavicon(response);
        } else {
            // 静态文件服务
            serveStaticFile(path, response);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

总结

本文通过构建一个简易版的HTTP服务器(TinyTomcat),展示了如何使用约300行Java代码实现基本的服务启动、请求解析和响应生成功能。通过这种方式,读者可以深入理解HTTP协议的工作机制以及应用服务器在处理Web请求时的核心流程和技术细节。这种方法不仅有助于学习,还能激发创新思维,在实际项目中灵活运用这些原理设计更加复杂的系统架构。

注意,这里只展示了部分代码逻辑,完整实现包括更多的静态文件服务、错误处理等细节,请参考完整的源码进行深入研究和实践。通过这种方式,不仅可以更深入地了解HTTP服务器的工作机制,还可以帮助构建更多实用的应用程序和服务。

实现错误页面 (NotFoundServlet.java)

当用户请求一个不存在的资源时,serve404方法会调用 NotFoundServlet来生成并返回一个标准的 404 错误页面。这种方法不仅便于统一管理所有 404 页面,还允许将来在不同上下文中定制错误信息。

import java.io.PrintWriter;

/**
 * 示例 404 错误 Servlet
 */
public class NotFoundServlet implements SimpleServlet {
    @Override
    public void service(SimpleRequest request, SimpleResponse response) throws IOException {
        response.setStatus(404, "Not Found");
        response.setContentType("text/html; charset=utf-8");

        PrintWriter writer = response.getWriter();
        writer.println("
<html><head><title>404 Not Found</title></head>");
        writer.println("<body style='background-color: #f1f1f1; font-family: Arial, sans-serif;'>");
        writer.println("<div class='container' style='margin-top: 150px; text-align: center;'>");
        writer.println("
<h1>页面找不到</h1>");
        writer.println("
<p>请求的资源不存在或已被删除。</p>");
        writer.println("<a href='/'>返回首页</a>");
        writer.println("</div></body></html>");
    }
}

实现favicon.ico处理 (FaviconServlet.java)

虽然favicon.ico文件大小通常很小,但它对于网站的品牌标志性和用户体验至关重要。我们的 serveFavicon 方法通过调用一个简单的 FaviconServlet类来发送这个图标文件。

import java.io.IOException;
import java.nio.file.Files;

/**
 * 处理请求 favicon.ico 的 Servlet
 */
public class FaviconServlet implements SimpleServlet {
    @Override
    public void service(SimpleRequest request, SimpleResponse response) throws IOException {
        String path = "resources/favicon.ico";
        byte[] iconContent = Files.readAllBytes(new File(path).toPath());

        if (Files.isRegularFile(Paths.get(path))) {
            response.setStatus(200, "OK");
            response.setContentType("image/x-icon");
            response.setContentLength(iconContent.length);

            OutputStream out = response.getOutputStream();
            out.write(iconContent);
            out.flush();
        } else {
            response.setStatus(404, "Not Found");
        }
    }
}

日志记录 (SimpleTomcat.java)

日志记录在调试和维护服务器时起着至关重要的作用。logRequest方法在每次接收到请求后被调用,它将客户端IP地址、请求的方法以及路径打印出来。这样的信息可以帮助我们快速定位问题或分析用户行为。

private void logRequest(String ip, String method, String path) {
    String time = LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"));
    System.out.printf("[%s] %s %s %s\n", time, ip, method, path);
}

启动与停止服务器 (SimpleTomcat.java)

启动和停止是任何服务最基础的操作。在 main方法中,我们通过命令行参数指定端口和根目录,并创建一个 SimpleTomcat实例来监听这些设置,并添加了一个关闭钩子以确保正常退出时能够调用sstop方法。

public static void main(String[] args) throws IOException {
    int port = 8080;
    String webRoot = ".";

    if (args.length > 0) {
        try {
            port = Integer.parseInt(args[0]);
        } catch (NumberFormatException e) {
            System.err.println("端口设置错误,使用默认值:" + port);
        }
    }

    if (args.length > 1) {
        webRoot = args[1];
    }

    SimpleTomcat tomcat = new SimpleTomcat(port, webRoot);
    Runtime.getRuntime().addShutdownHook(new Thread(tomcat::stop));
    tomcat.start();
}

总结

通过这些核心组件的实现,我们构建了一个简单的HTTP服务器框架。每个部分都为更复杂的功能提供了基础结构和可扩展性。尽管这个TinyTomcat无法与完整的Apache Tomcat相媲美,但它演示了基本概念,并为进一步开发打下了坚实的基础。

真正的Apache Tomcat在性能、配置灵活性及协议支持上有着显著的增强。它通过使用现代技术和设计模式(如NIO/AIO连接器和异步处理),以及强大的生命周期管理和安全性特性,提供了高度可定制且高效的应用服务器环境。如果你对深入理解或开发类似的Web容器感兴趣,可以研究Tomcat内部的工作原理和技术细节。


> 🔗 相关阅读Spring Boot