老三古 發表於 2026-4-21 09:00:00

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

<p>Tomcat 是 Java Web 开发的基石。我们天天使用它,但你是否思考过它内部是如何工作的?为了打破这个“黑盒”,最好的方式就是动手实现一个极度精简的核心。本项目 “TinyTomcat” 的目标,就是<strong>用大约 300 行纯 Java 代码,实现一个能够解析 HTTP 请求、路由到对应处理逻辑并返回响应的微型服务器</strong>。通过这个过程,你将透彻理解 Tomcat 处理请求的<strong>本质</strong>:监听端口、解析协议、调度响应。</p>
<p>所以,我们的目标是:</p>
<ol>
<li>监听一个端口(比如8080),接受HTTP请求。</li>
<li>解析HTTP请求,至少能解析请求的URL和方法(GET、POST等)。</li>
<li>根据请求的URL,找到对应的处理逻辑(类似于Servlet),并返回响应。</li>
<li>响应基本的HTTP格式,包括状态行、头部和响应体。</li>
</ol>
<h2 id="核心设计思路">核心设计思路</h2>
<p>一个基础的 HTTP 服务器,无论规模大小,其核心流程都可以抽象为下图所示的步骤:</p>
<div class="mermaid">graph TD
    A[客户端请求] --&gt; B(ServerSocket 接受连接)
    B --&gt; C[读取并解析 HTTP 请求行/头]
    C --&gt; D{请求路径是 '/' ?}
    D --&gt;|是| E[返回欢迎首页]
    D --&gt;|是 Servlet 路径| F[调用对应 Servlet.service]
    D --&gt;|是文件路径| G[查找并发送静态文件]
    D --&gt;|都不是| H[返回 404 错误]
    E --&gt; I[构建 HTTP 响应]
    F --&gt; I
    G --&gt; I
    H --&gt; I
    I --&gt; J[发送响应给客户端]
</div><p>基于这个流程,我们设计出五个核心类,共同完成了上图的闭环:</p>
<ol>
<li><strong>SimpleTomcat (服务器引擎)</strong>:这是大脑,负责启动、监听端口,并协调所有工作。</li>
<li><strong>SimpleRequest (请求解析器)</strong>:这是翻译官,将原始的、文本格式的 HTTP 请求解析成程序容易理解的 Java 对象。</li>
<li><strong>SimpleResponse (响应构建器)</strong>:这是包装工,负责将我们的处理结果,包装成符合 HTTP 协议格式的字节流。</li>
<li><strong>SimpleServlet (处理接口)</strong>:这是业务合同,定义了所有动态处理器(Servlet)必须遵守的规范。</li>
<li><strong>HelloServlet (业务实现)</strong>:这是我们的一个具体业务逻辑例子。</li>
</ol>
<h2 id="构建服务器引擎-simpletomcatjava">构建服务器引擎 (SimpleTomcat.java)</h2>
<p>这个类是程序的起点,也是调度中心。其核心逻辑在 <code>start()</code>和 <code>handleClient</code>方法中。</p>
<ul>
<li><strong>多线程处理</strong>。我们使用 <code>ExecutorService</code>线程池来处理每一个客户端连接 (<code>Socket</code>),这是服务器能同时服务多个请求的基础,避免了单线程阻塞。</li>
<li><strong>路由分发</strong>。在 <code>handleClient</code>方法中,我们读取请求的第一行(如 <code>GET /hello HTTP/1.1</code>),解析出请求路径,然后根据一个预设的“路由表” (<code>servletMapping</code>) 来决定将这个请求派发给谁处理。这模仿了 Tomcat 中 <code>web.xml</code>或注解配置的 Servlet 映射机制。</li>
<li><strong>区分动态与静态</strong>。我们的路由逻辑区分了三种情况:访问根路径返回欢迎页、访问注册的 Servlet 路径则动态处理、其他路径则尝试查找静态文件<br>
​</li>
</ul>
<pre><code class="language-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映射表:路径 -&gt; Servlet实例
    private Map&lt;String, SimpleServlet&gt; servletMapping = new ConcurrentHashMap&lt;&gt;();
    // 静态文件后缀映射
    private static final Map&lt;String, String&gt; 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(() -&gt; 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 -&gt; %s\n", path, servlet.getClass().getSimpleName());
    }
   
    private void registerDefaultServlets() {
      addServlet("/hello", new HelloServlet());
      addServlet("/time", (req, res) -&gt; {
            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 &lt; 3) return;
            
            String method = parts;
            String path = parts;
            
            // 创建请求/响应对象
            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("&lt;!DOCTYPE html&gt;");
      writer.println("&lt;html&gt;&lt;head&gt;&lt;title&gt;MiniTomcat&lt;/title&gt;");
      writer.println("&lt;style&gt;");
      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("&lt;/style&gt;&lt;/head&gt;&lt;body&gt;");
      writer.println("&lt;div class='container'&gt;");
      writer.println("&lt;h1&gt;🚀 SimpleTomcat 已启动!&lt;/h1&gt;");
      writer.println("&lt;div class='card'&gt;&lt;h3&gt;📡 测试链接&lt;/h3&gt;");
      writer.println("&lt;p&gt;&lt;a href='/hello'&gt;/hello - 问候Servlet&lt;/a&gt;&lt;/p&gt;");
      writer.println("&lt;p&gt;&lt;a href='/time'&gt;/time - 时间Servlet&lt;/a&gt;&lt;/p&gt;");
      writer.println("&lt;p&gt;&lt;a href='/index.html'&gt;/index.html - 静态文件&lt;/a&gt;&lt;/p&gt;");
      writer.println("&lt;/div&gt;");
      writer.println("&lt;div class='card'&gt;&lt;h3&gt;📁 服务器信息&lt;/h3&gt;");
      writer.println("&lt;p&gt;&lt;strong&gt;服务器时间:&lt;/strong&gt;" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + "&lt;/p&gt;");
      writer.println("&lt;p&gt;&lt;strong&gt;工作目录:&lt;/strong&gt;" + new File(webRoot).getAbsolutePath() + "&lt;/p&gt;");
      writer.println("&lt;p&gt;&lt;strong&gt;已注册Servlet:&lt;/strong&gt;" + servletMapping.size() + "个&lt;/p&gt;");
      writer.println("&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;");
    }
   
    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&lt;String, String&gt; 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("&lt;html&gt;&lt;head&gt;&lt;title&gt;404 Not Found&lt;/title&gt;&lt;/head&gt;");
      writer.println("&lt;body&gt;&lt;h1&gt;404 找不到页面&lt;/h1&gt;&lt;p&gt;" + message + "&lt;/p&gt;");
      writer.println("&lt;p&gt;&lt;a href='/'&gt;返回首页&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;");
    }
   
    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 &gt; 0 ? Integer.parseInt(args) : 8080;
      String webRoot = args.length &gt; 1 ? args : ".";
      
      SimpleTomcat tomcat = new SimpleTomcat(port, webRoot);
      Runtime.getRuntime().addShutdownHook(new Thread(tomcat::stop));
      tomcat.start();
    }
}
</code></pre>
<h2 id="解析-http-请求-simplerequestjava">解析 HTTP 请求 (SimpleRequest.java)</h2>
<p>HTTP 请求本质上是按特定格式组织的文本。<code>SimpleRequest</code>类的任务就是解析它。</p>
<ul>
<li><strong>解析请求行</strong>。构造函数中,通过 <code>requestLine.split(" ")</code>可以得到方法、路径和协议版本</li>
<li><strong>解析查询参数</strong>。在 <code>parseQueryString</code>方法中,我们处理 URL 中 <code>?</code>后面的部分(如 <code>name=Bob&amp;age=25</code>),将其拆解成键值对,存入 <code>params</code>映射,这样 Servlet 中就能通过 <code>getParameter("name")</code>获取值。</li>
<li><strong>解析请求头</strong>。通过循环读取输入流直到空行,将 <code>HeaderName: HeaderValue</code>这样的行解析后存入 <code>headers</code>映射。虽然我们的迷你版没有用到所有头部信息,但这种设计为后续扩展(如处理 Cookie、Session)留出了空间。</li>
</ul>
<pre><code class="language-java">import java.io.*;
import java.util.*;

/**
* 请求对象
*/
public class SimpleRequest {
    private final String method;
    private final String path;
    private final Map&lt;String, String&gt; headers = new HashMap&lt;&gt;();
    private final Map&lt;String, String&gt; params = new HashMap&lt;&gt;();
   
    public SimpleRequest(String method, String path, BufferedReader in) throws IOException {
      this.method = method;
      this.path = path;
      
      // 解析查询参数
      int qIndex = path.indexOf('?');
      if (qIndex &gt; 0) {
            parseQueryString(path.substring(qIndex + 1));
      }
      
      // 解析请求头
      String line;
      while ((line = in.readLine()) != null &amp;&amp; !line.isEmpty()) {
            int colon = line.indexOf(':');
            if (colon &gt; 0) {
                headers.put(
                  line.substring(0, colon).trim().toLowerCase(),
                  line.substring(colon + 1).trim()
                );
            }
      }
    }
   
    private void parseQueryString(String query) {
      for (String pair : query.split("&amp;")) {
            String[] kv = pair.split("=", 2);
            if (kv.length == 2) {
                params.put(kv, kv);
            }
      }
    }
   
    public String getMethod() { return method; }
    public String getPath() {
      int qIndex = path.indexOf('?');
      return qIndex &gt; 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;
    }
}
</code></pre>
<h2 id="构建-http-响应-simpleresponsejava">构建 HTTP 响应 (SimpleResponse.java)</h2>
<p>与解析请求相对,我们需要构建一个格式正确的 HTTP 响应。HTTP 响应由状态行、响应头和响应体三部分组成。</p>
<ul>
<li>
<p><strong>延迟发送头</strong>。我们设置了 <code>headersSent</code>标志位。这是因为在业务代码(Servlet)中,可能会先设置状态、内容类型等头部信息,再输出响应体。<code>getWriter()</code>或 <code>getOutputStream()</code>方法会<strong>在第一次被调用时</strong>,自动将所有已设置的头部信息发送出去(<code>sendHeaders</code>方法),这是一个巧妙的设计,确保了头部先于身体发送。</p>
</li>
<li>
<p><strong>头部格式</strong>。在 <code>sendHeaders</code>方法中,我们严格按照 <code>HTTP/1.1 200 OK\r\nHeader: Value\r\n\r\n</code>的格式拼接字符串。注意最后的空行 <code>\r\n\r\n</code>,它是分隔头部和身体的关键标记。</p>
</li>
</ul>
<pre><code class="language-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&lt;String, String&gt; headers = new HashMap&lt;&gt;();
    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&lt;String, String&gt; 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"));
    }
}
</code></pre>
<h2 id="定义处理契约-simpleservletjava">定义处理契约 (SimpleServlet.java)</h2>
<p>为了支持灵活的动态处理,我们定义了极简的 <code>SimpleServlet</code>接口。它只有一个 <code>service</code>方法,接受请求和响应对象。这模仿了标准 Servlet 的 <code>service</code>方法,是设计模式中<strong>策略模式</strong>​ 的体现。我们可以为不同路径(如 <code>/hello</code>, <code>/time</code>)注册不同的实现类,服务器引擎无需关心具体逻辑,只需调用其 <code>service</code>方法即可</p>
<pre><code class="language-java">import java.io.IOException;

/**
* 极简Servlet接口
*/
@FunctionalInterface
public interface SimpleServlet {
    void service(SimpleRequest request, SimpleResponse response) throws IOException;
}
</code></pre>
<h2 id="实现业务逻辑-helloservletjava">实现业务逻辑 (HelloServlet.java)</h2>
<p>HelloServlet是我们契约的一个具体实现。实现的步骤是:</p>
<ol>
<li>从 SimpleRequest对象中获取用户参数(req.getParameter("name"))。</li>
<li>通过 SimpleResponse对象设置内容类型。</li>
<li>通过 res.getWriter()获得输出流,生成动态的 HTML 内容。</li>
</ol>
<p>这个 Servlet 就像一个简单的控制器(Controller),它处理业务(组合问候语和当前时间),并渲染视图(生成 HTML 页面)。</p>
<pre><code class="language-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("&lt;!DOCTYPE html&gt;");
      writer.println("&lt;html&gt;&lt;head&gt;&lt;title&gt;问候页面&lt;/title&gt;");
      writer.println("&lt;style&gt;");
      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("&lt;/style&gt;&lt;/head&gt;&lt;body&gt;");
      writer.println("&lt;div class='greeting'&gt;👋 你好, " + name + "!&lt;/div&gt;");
      writer.println("&lt;div class='time'&gt;" + LocalDateTime.now().format(
            DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss")) + "&lt;/div&gt;");
      writer.println("&lt;form method='GET'&gt;");
      writer.println("&lt;input type='text' name='name' placeholder='输入你的名字' value='" + name + "'&gt;");
      writer.println("&lt;button type='submit'&gt;重新问候&lt;/button&gt;");
      writer.println("&lt;/form&gt;");
      writer.println("&lt;p&gt;&lt;a href='/' style='color:white;'&gt;🏠 返回首页&lt;/a&gt;&lt;/p&gt;");
      writer.println("&lt;/body&gt;&lt;/html&gt;");
    }
}
</code></pre>
<h2 id="总结">总结</h2>
<p>我们这个 TinyTomcat 虽然简单,但基本已经有了Tomcat的的核心骨架。真正的 Tomcat 正是在此基础上,在各个维度进行了史诗级的增强:</p>
<ul>
<li><strong>性能与并发</strong>:使用 NIO/AIO 连接器、更精细的线程池、缓存机制。</li>
<li><strong>配置与可扩展性</strong>:通过 <code>server.xml</code>, <code>web.xml</code>, 注解等方式进行复杂配置,支持 Valve、Filter 等扩展链。</li>
<li><strong>安全</strong>:实现安全管理器、 Realm 域认证。</li>
<li><strong>生命周期与容器</strong>:实现完整的 <code>Lifecycle</code>接口,管理 Server、Service、Engine、Host、Context、Wrapper 等层次化容器。</li>
<li><strong>协议支持</strong>:支持 HTTP/1.1、HTTP/2,甚至 AJP 协议。</li>
<li><strong>会话管理</strong>:实现复杂而强大的 Session 创建、跟踪、持久化机制。</li>
<li><strong>异步处理</strong>:支持 Servlet 3.0+ 的异步 I/O 处理。</li>
</ul>
<p>接下来,我将会继续从源码角度介绍 Tomcat 的核心设计,可以持续关注</p>


</div>
<div id="MySignature" role="contentinfo">
    <p>本文来自在线网站:seven的菜鸟成长之路,作者:seven,转载请注明原文链接:www.seven97.top</p><br><br>
来源:https://www.cnblogs.com/sevencoding/p/19873923
頁: [1]
查看完整版本: 用300行代码手写一个mini版的Tomcat