游乐游手机版
首页/业界动态/文章详情

行代码手写迷你版Tomcat教程

时间:2026-07-20 13:10
Tomcat 是 Java Web 开发绕不开的基础设施。天天打交道,但真正理解其内核的人并不多。要想打破这个“黑盒”,最好的方式不是死读书,而是亲手写一个。我们启动的这个 “TinyTomcat” 项目,目标非常明确:用大约 300 行纯 Java 代码,实现一个能从监听端口、解析 HTTP 请求

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 路径则动态处理、其他路径则尝试查找静态文件。

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 servletMapping = new ConcurrentHashMap<>(); // 静态文件后缀映射 private static final Map 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 启动在 https://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(""); writer.println("MiniTomcat"); writer.println(""); writer.println("

"); writer.println("

? SimpleTomcat 已启动!

"); writer.println("

? 测试链接

"); writer.println("

/hello - 问候Servlet

"); writer.println("

/time - 时间Servlet

"); writer.println("

/index.html - 静态文件

"); 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.Entry 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("404 Not Found"); writer.println("

404 找不到页面

" + 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 headers = new HashMap<>(); private final Map 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 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 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接口 */@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 页面)。

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("问候页面"); writer.println(""); writer.println("

? 你好, " + name + "!

"); writer.println("

" + LocalDateTime.now().format( DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss")) + "

"); writer.println("
"); writer.println(""); writer.println(""); writer.println("
"); 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 处理。
来源:https://www.51cto.com/article/841305.html
上一篇飞利浦27E3U7903专业显示器上市 影像工作者高标准之选 下一篇创维A7H Pro艺术壁纸家装级MiniLED旗舰画质电视
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
台式机加装固态硬盘怎么选?三星9100 PRO深度解析
业界动态 · 2026-09-01

台式机加装固态硬盘怎么选?三星9100 PRO深度解析

台式机升级存储常受限于系统启动慢、游戏加载卡顿与大文件传输延迟。本文基于三星9100 PRO的PCIe 5 0架构、14800MB s读取、13400MB s写入、2200K 2600K IOPS、1TB~8TB容量、第八代V-NAND与5nm主控、镍涂层散热与DTG技术、散热片版适配及魔术师软件,提供选购判断与安装兼容性要点,帮助读者评估是否值得一步到位升级。

宁德时代2026年中期分红61.8亿元,同比增35%,创历史新高
业界动态 · 2026-09-01

宁德时代2026年中期分红61.8亿元,同比增35%,创历史新高

宁德时代发布2026年中期分红方案,总额达61 8亿元,同比增长35%。本文梳理分红具体安排、历史对比、业绩支撑及分红机制,帮助投资者评估公司现金流实力与股东回报策略。

企业硬盘报废销毁合规指南:如何选择专业机构与处理流程
业界动态 · 2026-08-31

企业硬盘报废销毁合规指南:如何选择专业机构与处理流程

企业硬盘报废面临数据复原与合规风险,需选择具备资质且流程透明的专业机构。本文解析行业乱象,介绍以团体标准为核心的合规销毁流程,涵盖上门收运、消磁粉碎、视频溯源及尾料处置,帮助企业规避泄密责任,确保数据安全闭环。

机密文件销毁找什么机构?认准团标参编与资质合规
业界动态 · 2026-08-31

机密文件销毁找什么机构?认准团标参编与资质合规

机密文件销毁找什么机构?核心在于甄别服务商是否具备正规保密资质及是否参与行业标准制定。本文解析《商业秘密及敏感信息载体销毁通用规范》团标要求,提供筛选销毁机构的实操指南,帮助企业规避数据泄露风险,确保销毁流程合规可溯。

影石Insta360 X6全球首销登顶:8K全景画质与AI创作功能解析
业界动态 · 2026-08-31

影石Insta360 X6全球首销登顶:8K全景画质与AI创作功能解析

影石Insta360 X6全球同步发售即登顶国内外主流平台销量榜首。本文解析其搭载的索尼定制方形大底传感器、4nm AI三芯架构及8K50fps画质,详解3D时光舱、AI导演等独家功能,探讨全景相机从专业工具向大众智能创作设备的演进趋势。