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

Tomcat 是 Java Web 开发的基石。我们天天使用它,但你是否思考过它内部是如何工作的?为了打破这个"黑盒",最好的方式就是动手实现一个极度精简的核心。本项目 "TinyTomcat" 的目标,就是用大约 300 行纯 Java 代码,实现一个能够解析 HTTP 请求、路由到对应处理逻辑并返回响应的微型服务器 。通过这个过程,你将透彻理解 Tomcat 处理请求的本质:监听端口、解析协议、调度响应。

所以,我们的目标是:

  1. 监听一个端口(比如8080),接受HTTP请求。
  2. 解析HTTP请求,至少能解析请求的URL和方法(GET、POST等)。
  3. 根据请求的URL,找到对应的处理逻辑(类似于Servlet),并返回响应。
  4. 响应基本的HTTP格式,包括状态行、头部和响应体。

核心设计思路

一个基础的 HTTP 服务器,无论规模大小,其核心流程都可以抽象为下图所示的步骤:

graph TD A[客户端请求] --> B(ServerSocket 接受连接) B --> C[读取并解析 HTTP 请求行/头] C --> D{请求路径是 '/' ?} D -->|是| E[返回欢迎首页] D -->|是 Servlet 路径| F[调用对应 Servlet.service] D -->|是文件路径| G[查找并发送静态文件] D -->|都不是| H[返回 404 错误] E --> I[构建 HTTP 响应] F --> I G --> I H --> I I --> J[发送响应给客户端]

基于这个流程,我们设计出五个核心类,共同完成了上图的闭环:

  1. SimpleTomcat (服务器引擎):这是大脑,负责启动、监听端口,并协调所有工作。
  2. SimpleRequest (请求解析器):这是翻译官,将原始的、文本格式的 HTTP 请求解析成程序容易理解的 Java 对象。
  3. SimpleResponse (响应构建器):这是包装工,负责将我们的处理结果,包装成符合 HTTP 协议格式的字节流。
  4. SimpleServlet (处理接口):这是业务合同,定义了所有动态处理器(Servlet)必须遵守的规范。
  5. HelloServlet (业务实现):这是我们的一个具体业务逻辑例子。

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

这个类是程序的起点,也是调度中心。其核心逻辑在 start()handleClient方法中。

  • 多线程处理 。我们使用 ExecutorService线程池来处理每一个客户端连接 (Socket),这是服务器能同时服务多个请求的基础,避免了单线程阻塞。
  • 路由分发 。在 handleClient方法中,我们读取请求的第一行(如 GET /hello HTTP/1.1),解析出请求路径,然后根据一个预设的"路由表" (servletMapping) 来决定将这个请求派发给谁处理。这模仿了 Tomcat 中 web.xml或注解配置的 Servlet 映射机制。
  • 区分动态与静态。我们的路由逻辑区分了三种情况:访问根路径返回欢迎页、访问注册的 Servlet 路径则动态处理、其他路径则尝试查找静态文件
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.*;

/**
 * Mini版 Tomcat - 核心服务器
 * 功能:监听端口、解析HTTP、路由请求
 */
public class SimpleTomcat {
    private int port = 8080;
    private String webRoot = ".";
    private ServerSocket serverSocket;
    private ExecutorService threadPool;
    private boolean running = false;
    
    // Servlet映射表:路径 -> Servlet实例
    private Map<String, SimpleServlet> servletMapping = new ConcurrentHashMap<>();
    // 静态文件后缀映射
    private static final Map<String, String> CONTENT_TYPES = Map.of(
        ".html", "text/html; charset=utf-8",
        ".txt", "text/plain; charset=utf-8",
        ".js", "application/javascript",
        ".css", "text/css",
        ".json", "application/json",
        ".png", "image/png",
        ".jpg", "image/jpeg",
        ".jpeg", "image/jpeg",
        ".gif", "image/gif"
    );
    
    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();
        }
    }
    
    private void serveWelcomePage(SimpleResponse res) throws IOException {
        res.setContentType("text/html; charset=utf-8");
        PrintWriter writer = res.getWriter();
        writer.println("<!DOCTYPE html>");
        writer.println("<html><head><title>MiniTomcat</title>");
        writer.println("<style>");
        writer.println("body { font-family: Arial; margin: 40px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; }");
        writer.println(".container { max-width: 800px; margin: 0 auto; padding: 20px; background: rgba(255,255,255,0.1); border-radius: 10px; }");
        writer.println("h1 { text-align: center; font-size: 2.5em; margin-bottom: 30px; }");
        writer.println(".card { background: rgba(255,255,255,0.2); padding: 20px; border-radius: 8px; margin: 15px 0; }");
        writer.println("a { color: #ffd700; text-decoration: none; padding: 8px 15px; background: rgba(0,0,0,0.3); border-radius: 5px; }");
        writer.println("a:hover { background: rgba(0,0,0,0.5); }");
        writer.println("</style></head><body>");
        writer.println("<div class='container'>");
        writer.println("<h1>🚀 SimpleTomcat 已启动!</h1>");
        writer.println("<div class='card'><h3>📡 测试链接</h3>");
        writer.println("<p><a href='/hello'>/hello - 问候Servlet</a></p>");
        writer.println("<p><a href='/time'>/time - 时间Servlet</a></p>");
        writer.println("<p><a href='/index.html'>/index.html - 静态文件</a></p>");
        writer.println("</div>");
        writer.println("<div class='card'><h3>📁 服务器信息</h3>");
        writer.println("<p><strong>服务器时间:</strong>" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + "</p>");
        writer.println("<p><strong>工作目录:</strong>" + new File(webRoot).getAbsolutePath() + "</p>");
        writer.println("<p><strong>已注册Servlet:</strong>" + servletMapping.size() + "个</p>");
        writer.println("</div></div></body></html>");
    }
    
    private void serveStaticFile(String path, SimpleResponse res) throws IOException {
        File file = new File(webRoot + path);
        if (!file.exists() || file.isDirectory()) {
            serve404(res, "文件未找到: " + path);
            return;
        }
        
        // 设置Content-Type
        String contentType = "application/octet-stream";
        for (Map.Entry<String, String> entry : CONTENT_TYPES.entrySet()) {
            if (path.endsWith(entry.getKey())) {
                contentType = entry.getValue();
                break;
            }
        }
        res.setContentType(contentType);
        res.setContentLength(file.length());
        
        // 发送文件
        Files.copy(file.toPath(), res.getOutputStream());
    }
    
    private void serve404(SimpleResponse res, String message) throws IOException {
        res.setStatus(404, "Not Found");
        res.setContentType("text/html; charset=utf-8");
        PrintWriter writer = res.getWriter();
        writer.println("<html><head><title>404 Not Found</title></head>");
        writer.println("<body><h1>404 找不到页面</h1><p>" + message + "</p>");
        writer.println("<p><a href='/'>返回首页</a></p></body></html>");
    }
    
    private void serveFavicon(SimpleResponse res) throws IOException {
        res.setStatus(204, "No Content"); // 不返回favicon
    }
    
    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);
    }
    
    public static void main(String[] args) throws IOException {
        int port = args.length > 0 ? Integer.parseInt(args[0]) : 8080;
        String webRoot = args.length > 1 ? args[1] : ".";
        
        SimpleTomcat tomcat = new SimpleTomcat(port, webRoot);
        Runtime.getRuntime().addShutdownHook(new Thread(tomcat::stop));
        tomcat.start();
    }
}

解析 HTTP 请求 (SimpleRequest.java)

HTTP 请求本质上是按特定格式组织的文本。SimpleRequest类的任务就是解析它。

  • 解析请求行 。构造函数中,通过 requestLine.split(" ")可以得到方法、路径和协议版本
  • 解析查询参数 。在 parseQueryString方法中,我们处理 URL 中 ?后面的部分(如 name=Bob&age=25),将其拆解成键值对,存入 params映射,这样 Servlet 中就能通过 getParameter("name")获取值。
  • 解析请求头 。通过循环读取输入流直到空行,将 HeaderName: HeaderValue这样的行解析后存入 headers映射。虽然我们的迷你版没有用到所有头部信息,但这种设计为后续扩展(如处理 Cookie、Session)留出了空间。
java 复制代码
import java.io.*;
import java.util.*;

/**
 * 请求对象
 */
public class SimpleRequest {
    private final String method;
    private final String path;
    private final Map<String, String> headers = new HashMap<>();
    private final Map<String, String> params = new HashMap<>();
    
    public SimpleRequest(String method, String path, BufferedReader in) throws IOException {
        this.method = method;
        this.path = path;
        
        // 解析查询参数
        int qIndex = path.indexOf('?');
        if (qIndex > 0) {
            parseQueryString(path.substring(qIndex + 1));
        }
        
        // 解析请求头
        String line;
        while ((line = in.readLine()) != null && !line.isEmpty()) {
            int colon = line.indexOf(':');
            if (colon > 0) {
                headers.put(
                    line.substring(0, colon).trim().toLowerCase(),
                    line.substring(colon + 1).trim()
                );
            }
        }
    }
    
    private void parseQueryString(String query) {
        for (String pair : query.split("&")) {
            String[] kv = pair.split("=", 2);
            if (kv.length == 2) {
                params.put(kv[0], kv[1]);
            }
        }
    }
    
    public String getMethod() { return method; }
    public String getPath() { 
        int qIndex = path.indexOf('?');
        return qIndex > 0 ? path.substring(0, qIndex) : path;
    }
    public String getParameter(String name) { return params.get(name); }
    public String getHeader(String name) { return headers.get(name.toLowerCase()); }
    
    public String toString() {
        return method + " " + path;
    }
}

构建 HTTP 响应 (SimpleResponse.java)

与解析请求相对,我们需要构建一个格式正确的 HTTP 响应。HTTP 响应由状态行、响应头和响应体三部分组成。

  • 延迟发送头 。我们设置了 headersSent标志位。这是因为在业务代码(Servlet)中,可能会先设置状态、内容类型等头部信息,再输出响应体。getWriter()getOutputStream()方法会在第一次被调用时 ,自动将所有已设置的头部信息发送出去(sendHeaders方法),这是一个巧妙的设计,确保了头部先于身体发送。

  • 头部格式 。在 sendHeaders方法中,我们严格按照 HTTP/1.1 200 OK\r\nHeader: Value\r\n\r\n的格式拼接字符串。注意最后的空行 \r\n\r\n,它是分隔头部和身体的关键标记。

java 复制代码
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * 响应对象
 */
public class SimpleResponse {
    private final OutputStream output;
    private PrintWriter writer;
    private int status = 200;
    private String statusText = "OK";
    private final Map<String, String> headers = new HashMap<>();
    private boolean headersSent = false;
    
    public SimpleResponse(OutputStream output) {
        this.output = output;
        headers.put("Server", "SimpleTomcat/1.0");
        headers.put("Date", new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US)
            .format(new Date()));
    }
    
    public void setStatus(int status, String text) {
        this.status = status;
        this.statusText = text;
    }
    
    public void setContentType(String type) {
        headers.put("Content-Type", type);
    }
    
    public void setContentLength(long length) {
        headers.put("Content-Length", String.valueOf(length));
    }
    
    public PrintWriter getWriter() throws IOException {
        sendHeaders();
        if (writer == null) {
            writer = new PrintWriter(new OutputStreamWriter(output, "UTF-8"), true);
        }
        return writer;
    }
    
    public OutputStream getOutputStream() throws IOException {
        sendHeaders();
        return output;
    }
    
    private void sendHeaders() throws IOException {
        if (headersSent) return;
        headersSent = true;
        
        StringBuilder sb = new StringBuilder();
        sb.append("HTTP/1.1 ").append(status).append(" ").append(statusText).append("\r\n");
        for (Map.Entry<String, String> entry : headers.entrySet()) {
            sb.append(entry.getKey()).append(": ").append(entry.getValue()).append("\r\n");
        }
        sb.append("\r\n");
        output.write(sb.toString().getBytes("ISO-8859-1"));
    }
}

定义处理契约 (SimpleServlet.java)

为了支持灵活的动态处理,我们定义了极简的 SimpleServlet接口。它只有一个 service方法,接受请求和响应对象。这模仿了标准 Servlet 的 service方法,是设计模式中策略模式 ​ 的体现。我们可以为不同路径(如 /hello, /time)注册不同的实现类,服务器引擎无需关心具体逻辑,只需调用其 service方法即可

java 复制代码
import java.io.IOException;

/**
 * 极简Servlet接口
 */
@FunctionalInterface
public interface SimpleServlet {
    void service(SimpleRequest request, SimpleResponse response) throws IOException;
}

实现业务逻辑 (HelloServlet.java)

HelloServlet是我们契约的一个具体实现。实现的步骤是:

  1. 从 SimpleRequest对象中获取用户参数(req.getParameter("name"))。
  2. 通过 SimpleResponse对象设置内容类型。
  3. 通过 res.getWriter()获得输出流,生成动态的 HTML 内容。

这个 Servlet 就像一个简单的控制器(Controller),它处理业务(组合问候语和当前时间),并渲染视图(生成 HTML 页面)。

java 复制代码
import java.io.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * 示例Servlet
 */
public class HelloServlet implements SimpleServlet {
    @Override
    public void service(SimpleRequest req, SimpleResponse res) throws IOException {
        String name = req.getParameter("name");
        if (name == null || name.trim().isEmpty()) {
            name = "朋友";
        }
        
        res.setContentType("text/html; charset=utf-8");
        PrintWriter writer = res.getWriter();
        
        writer.println("<!DOCTYPE html>");
        writer.println("<html><head><title>问候页面</title>");
        writer.println("<style>");
        writer.println("body { font-family: Arial, sans-serif; text-align: center; margin: 100px; background: linear-gradient(45deg, #f093fb 0%, #f5576c 100%); color: white; }");
        writer.println(".greeting { font-size: 3em; margin: 20px; text-shadow: 2px 2px 4px rgba(0,0,0,0.3); }");
        writer.println(".time { font-size: 1.2em; opacity: 0.9; }");
        writer.println("input, button { padding: 10px; font-size: 16px; margin: 10px; border: none; border-radius: 5px; }");
        writer.println("</style></head><body>");
        writer.println("<div class='greeting'>👋 你好, " + name + "!</div>");
        writer.println("<div class='time'>" + LocalDateTime.now().format(
            DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss")) + "</div>");
        writer.println("<form method='GET'>");
        writer.println("<input type='text' name='name' placeholder='输入你的名字' value='" + name + "'>");
        writer.println("<button type='submit'>重新问候</button>");
        writer.println("</form>");
        writer.println("<p><a href='/' style='color:white;'>🏠 返回首页</a></p>");
        writer.println("</body></html>");
    }
}

总结

我们这个 TinyTomcat 虽然简单,但基本已经有了Tomcat的的核心骨架。真正的 Tomcat 正是在此基础上,在各个维度进行了史诗级的增强:

  • 性能与并发:使用 NIO/AIO 连接器、更精细的线程池、缓存机制。
  • 配置与可扩展性 :通过 server.xml, web.xml, 注解等方式进行复杂配置,支持 Valve、Filter 等扩展链。
  • 安全:实现安全管理器、 Realm 域认证。
  • 生命周期与容器 :实现完整的 Lifecycle接口,管理 Server、Service、Engine、Host、Context、Wrapper 等层次化容器。
  • 协议支持:支持 HTTP/1.1、HTTP/2,甚至 AJP 协议。
  • 会话管理:实现复杂而强大的 Session 创建、跟踪、持久化机制。
  • 异步处理:支持 Servlet 3.0+ 的异步 I/O 处理。

接下来,我将会继续从源码角度介绍 Tomcat 的核心设计,可以持续关注

相关推荐
隐退山林2 小时前
JavaEE进阶:SpirngMVC入门(2)
java·java-ee
小江的记录本4 小时前
【分布式】分布式核心组件——分布式锁:Redis/ZooKeeper/etcd 实现方案(附全方位对比表)、优缺点、Redlock、时钟回拨问题
java·网络·redis·分布式·后端·zookeeper·架构
好家伙VCC4 小时前
**发散创新:用Rust实现基于RAFT共识算法的轻量级分布式日志系统**在分布式系统中,**一致性协议**是保障数据可靠
java·分布式·python·rust·共识算法
晔子yy5 小时前
【JAVA探索之路】从头开始讲透、实现单例模式
java·开发语言·单例模式
chools10 小时前
【AI超级智能体】快速搞懂工具调用Tool Calling 和 MCP协议
java·人工智能·学习·ai
李白你好11 小时前
TongWeb EJB 反序列化生成工具(Java-Chain 插件)
java·安全
U盘失踪了12 小时前
Java 的 JAR 是什么?
java·jar
今天又在写代码12 小时前
java-v2
java·开发语言
competes13 小时前
慈善基金投资底层逻辑应用 顶层代码低代码配置平台开发结构方式数据存储模块
java·开发语言·数据库·windows·sql