用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[客户端请求] --> 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[发送响应给客户端]
</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映射表:路径 -> 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;
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("<!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) : 8080;
String webRoot = args.length > 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&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<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, kv);
}
}
}
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;
}
}
</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<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"));
}
}
</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("<!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>");
}
}
</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]