用300行代码手写一个mini版的Tomcat
Tomcat 是 Java Web 开发的基石。我们天天使用它,但你是否思考过它内部是如何工作的?为了打破这个“黑盒”,最好的方式就是动手实现一个极度精简的核心。本项目 “TinyTomcat” 的目标,就是用大约 300 行纯 Java 代码,实现一个能够解析 HTTP 请求、路由到对应处理逻辑并返回响应的微型服务器。通过这个过程,你将透彻理解 Tomcat 处理请求的本质:监听端口、解析协议、调度响应。 所以,我们的目标是: 一个基础的 HTTP 服务器,无论规模大小,其核心流程都可以抽象为下图所示的步骤: 基于这个流程,我们设计出五个核心类,共同完成了上图的闭环: 这个类是程序的起点,也是调度中心。其核心逻辑在 HTTP 请求本质上是按特定格式组织的文本。 与解析请求相对,我们需要构建一个格式正确的 HTTP 响应。HTTP 响应由状态行、响应头和响应体三部分组成。 为了支持灵活的动态处理,我们定义了极简的 HelloServlet是我们契约的一个具体实现。实现的步骤是: 这个 Servlet 就像一个简单的控制器(Controller),它处理业务(组合问候语和当前时间),并渲染视图(生成 HTML 页面)。 我们这个 TinyTomcat 虽然简单,但基本已经有了Tomcat的的核心骨架。真正的 Tomcat 正是在此基础上,在各个维度进行了史诗级的增强: 接下来,我将会继续从源码角度介绍 Tomcat 的核心设计,可以持续关注核心设计思路
构建服务器引擎 (SimpleTomcat.java)
start()和 handleClient方法中。ExecutorService线程池来处理每一个客户端连接 (Socket),这是服务器能同时服务多个请求的基础,避免了单线程阻塞。handleClient方法中,我们读取请求的第一行(如 GET /hello HTTP/1.1),解析出请求路径,然后根据一个预设的“路由表” (servletMapping) 来决定将这个请求派发给谁处理。这模仿了 Tomcat 中 web.xml或注解配置的 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)
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)
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)
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>");
}
}总结
server.xml, web.xml, 注解等方式进行复杂配置,支持 Valve、Filter 等扩展链。Lifecycle接口,管理 Server、Service、Engine、Host、Context、Wrapper 等层次化容器。