Tomcat 是 Java Web 开发绕不开的基础设施。天天打交道,但真正理解其内核的人并不多。要想打破这个“黑盒”,最好的方式不是死读书,而是亲手写一个。我们启动的这个 “TinyTomcat” 项目,目标非常明确:用大约 300 行纯 Java 代码,实现一个能从监听端口、解析 HTTP 请求、路由分发,再到返回响应的微型服务器。
通过这个过程,你将彻底搞懂 Tomcat 处理请求的本质:监听端口、解析协议、调度响应。下面这张图首先展示了核心流程:

因此,我们的具体目标包括:
监听一个端口(例如 8080),接收 HTTP 请求。解析 HTTP 请求,至少能解析出请求的 URL 和方法(GET、POST 等)。根据请求的 URL,找到对应的处理逻辑(类似 Servlet),并返回响应。响应符合基本的 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[发送响应给客户端]
基于这个流程,我们设计了五个核心类,它们共同完成了上图的闭环:
SimpleTomcat (服务器引擎):大脑,负责启动、监听端口,并协调所有工作。SimpleRequest (请求解析器):翻译官,将原始的、文本格式的 HTTP 请求解析成 Java 对象。
SimpleResponse (响应构建器):包装工,负责将处理结果包装成符合 HTTP 协议格式的字节流。
SimpleServlet (处理接口):业务合同,定义了所有动态处理器必须遵守的规范。
HelloServlet (业务实现):一个具体的业务逻辑例子,用于演示动态处理。
构建服务器引擎 (SimpleTomcat.java)
这个类是整个引擎的起点,也是调度中心。它的核心逻辑在 start() 和 handleClient 方法中体现。
多线程处理。 我们用 ExecutorService 线程池来处理每一个客户端连接(Socket),这是服务器能同时服务多个请求的基础,避免单线程阻塞。
路由分发。 在 handleClient 方法中,读取请求的第一行(如 GET /hello HTTP/1.1),解析出请求路径,然后根据预设的“路由表”(servletMapping)决定将这个请求派发给谁处理。这模仿了 Tomcat 中 web.xml 或注解配置的 Servlet 映射机制。
区分动态与静态。 路由逻辑区分了三种情况:访问根路径返回欢迎页、访问注册的 Servlet 路径则动态处理、其他路径则尝试查找静态文件。
"); writer.println("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? SimpleTomcat 已启动!
"); writer.println("? 测试链接
"); writer.println(""); writer.println(""); writer.println(""); writer.println("
? 服务器信息
"); writer.println("服务器时间:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + "
"); writer.println("工作目录:" + new File(webRoot).getAbsolutePath() + "
"); writer.println("已注册Servlet:" + servletMapping.size() + "个
"); writer.println(""); } 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.Entry404 找不到页面
" + message + "
"); writer.println(""); } 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)留出了空间。
import java.io.*;import java.util.*;/** * 请求对象 */public class SimpleRequest { private final String method; private final String path; private final Map
构建 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,它是分隔头部和身体的关键标记。
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
定义处理契约 (SimpleServlet.java)
为了支持灵活的动态处理,我们定义了极简的 SimpleServlet 接口。它只有一个 service 方法,接受请求和响应对象。这模仿了标准 Servlet 的 service 方法,是设计模式中策略模式的体现。我们可以为不同路径(如 /hello、/time)注册不同的实现类,服务器引擎无需关心具体逻辑,只需调用其 service 方法即可。
import java.io.IOException;/** * 极简Servlet接口 */@FunctionalInterfacepublic interface SimpleServlet { void service(SimpleRequest request, SimpleResponse response) throws IOException;}
实现业务逻辑 (HelloServlet.java)
HelloServlet 是契约的一个具体实现。实现的步骤是:
SimpleRequest 对象中获取用户参数(req.getParameter("name"))。通过 SimpleResponse 对象设置内容类型。通过 res.getWriter() 获得输出流,生成动态的 HTML 内容。
这个 Servlet 就像一个简单的控制器(Controller),它处理业务(组合问候语和当前时间),并渲染视图(生成 HTML 页面)。
? 你好, " + name + "! " + LocalDateTime.now().format( DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss")) + "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(""); writer.println("
总结
这个 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 处理。
