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

📅 2026/7/7 10:12:04
用300行代码手写一个mini版的Tomcat
析协议、调度响应。所以我们的目标是监听一个端口比如8080接受HTTP请求。解析HTTP请求至少能解析请求的URL和方法GET、POST等。根据请求的URL找到对应的处理逻辑类似于Servlet并返回响应。响应基本的HTTP格式包括状态行、头部和响应体。核心设计思路一个基础的 HTTP 服务器无论规模大小其核心流程都可以抽象为下图所示的步骤是是 Servlet 路径是文件路径都不是客户端请求ServerSocket 接受连接读取并解析 HTTP 请求行/头请求路径是 / ?返回欢迎首页调用对应 Servlet.service查找并发送静态文件返回 404 错误构建 HTTP 响应发送响应给客户端基于这个流程我们设计出五个核心类共同完成了上图的闭环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 路径则动态处理、其他路径则尝试查找静态文件​javaimport 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 MapString, SimpleServlet servletMapping new ConcurrentHashMap(); // 静态文件后缀映射 private static final MapString, String CONTENT_TYPES Map.of( .html, text/html; charsetutf-8, .txt, text/plain; charsetutf-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; charsetutf-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; charsetutf-8); PrintWriter writer res.getWriter(); writer.println(!DOCTYPE html); writer.println(htmlheadtitleMiniTomcat/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/headbody); writer.println(div classcontainer); writer.println(h1 SimpleTomcat 已启动/h1); writer.println(div classcardh3 测试链接/h3); writer.println(pa href/hello/hello - 问候Servlet/a/p); writer.println(pa href/time/time - 时间Servlet/a/p); writer.println(pa href/index.html/index.html - 静态文件/a/p); writer.println(/div); writer.println(div classcardh3 服务器信息/h3); writer.println(pstrong服务器时间/strong LocalDateTime.now().format(DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss)) /p); writer.println(pstrong工作目录/strong new File(webRoot).getAbsolutePath() /p); writer.println(pstrong已注册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.EntryString, 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; charsetutf-8); PrintWriter writer res.getWriter(); writer.println(htmlheadtitle404 Not Found/title/head); writer.println(bodyh1404 找不到页面/h1p message /p); writer.println(pa 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 中?后面的部分如nameBobage25将其拆解成键值对存入params映射这样 Servlet 中就能通过getParameter(name)获取值。解析请求头。通过循环读取输入流直到空行将HeaderName: HeaderValue这样的行解析后存入headers映射。虽然我们的迷你版没有用到所有头部信息但这种设计为后续扩展如处理 Cookie、Session留出了空间。javaimport java.io.*; import java.util.*; /** * 请求对象 */ public class SimpleRequest { private final String method; private final String path; private final MapString, String headers new HashMap(); private final MapString, 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它是分隔头部和身体的关键标记。javaimport 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 MapString, 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.EntryString, 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方法即可javaimport 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 页面。javaimport 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; charsetutf-8); PrintWriter writer res.getWriter(); writer.println(!DOCTYPE html); writer.println(htmlheadtitle问候页面/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/headbody); writer.println(div classgreeting 你好, name !/div); writer.println(div classtime LocalDateTime.now().format( DateTimeFormatter.ofPattern(yyyy年MM月dd日 HH:mm:ss)) /div); writer.println(form methodGET); writer.println(input typetext namename placeholder输入你的名字 value name ); writer.println(button typesubmit重新问候/button); writer.println(/form); writer.println(pa href/ stylecolor: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 创建、跟踪、持久化机制。