用300行代码手写一个mini版的Tomcat
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 (处理接口):这是业务合同,定义了所有动态处理器(Servlet)必须遵守的规范。
- HelloServlet (业务实现):这是我们的一个具体业务逻辑例子。
构建服务器引擎 (SimpleTomcat.java)
这个类是程序的起点,也是调度中心。其核心逻辑在 start()和 handleClient方法中。
- 多线程处理。我们使用
ExecutorService线程池来处理每一个客户端连接 (Socket),这是服务器能同时服务多个请求的基础,避免了单线程阻塞。 - 路由分发。在
handleClient方法中,我们读取请求的第一行(如GET /hello HTTP/1.1),解析出请求路径,然后根据一个预设的“路由表” (servletMapping) 来决定将这个请求派发给谁处理。这模仿了 Tomcat 中web.xml或注解配置的 Servlet 映射机制。 - 区分动态与静态。我们的路由逻辑区分了三种情况:访问根路径返回欢迎页、访问注册的 Servlet 路径则动态处理、其他路径则尝试查找静态文件
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)留出了空间。
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,它是分隔头部和身体的关键标记。
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方法即可
import java.io.IOException;
/**
* 极简Servlet接口
*/
@FunctionalInterface
public interface SimpleServlet {
void service(SimpleRequest request, SimpleResponse response) throws IOException;
}实现业务逻辑 (HelloServlet.java)
HelloServlet是我们契约的一个具体实现。实现的步骤是:
- 从 SimpleRequest对象中获取用户参数(req.getParameter("name"))。
- 通过 SimpleResponse对象设置内容类型。
- 通过 res.getWriter()获得输出流,生成动态的 HTML 内容。
这个 Servlet 就像一个简单的控制器(Controller),它处理业务(组合问候语和当前时间),并渲染视图(生成 HTML 页面)。
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 的核心设计,可以持续关注

