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

行代码手写迷你版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.xmlweb.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旗舰画质电视
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

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

同类最新

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

更多
京东数码影音暑假装备投影仪游戏掌机AI眼镜一站购齐
业界动态 · 2026-07-20

京东数码影音暑假装备投影仪游戏掌机AI眼镜一站购齐

京东“数码放暑价”好物榜汇聚投影仪、游戏掌机、智能眼镜等多款全品类数码好物,提供国家补贴、以旧换新最低五折优惠,覆盖宅家观影、游戏对战、出行记录等暑期场景,全方位满足多场景趣玩需求。

年7月出差笔记本横评:五款轻薄本选购指南
业界动态 · 2026-07-20

年7月出差笔记本横评:五款轻薄本选购指南

二零二六年出差轻薄本选购指南,从便携、续航、性能等八维度评测五款机型。入门级推荐华硕无畏14SE性价比高,荣耀X14均衡;中端惠普战X均衡全面,适合商务;高端联想小新Pro16GT性能最强,适合重度应用;ThinkPadX1Carbon极致轻便,适合频繁出差。

日本人体冰箱5分钟快速降温售价约150万日元
业界动态 · 2026-07-20

日本人体冰箱5分钟快速降温售价约150万日元

日本推出“人体冰箱”DoHiemonBox,外形类似自动售货机,站立式结构,底部带滚轮。内部维持15℃环境,5℃冷风吹向头颈肩背,约5分钟快速降温,10分钟缓解不适。三种制冷模式,20分钟自动关闭,耗电仅为普通立式空调一半,主要面向高温作业人员及公共场所。

小米米家智能茶吧机Pro立式众筹价1099元
业界动态 · 2026-07-20

小米米家智能茶吧机Pro立式众筹价1099元

小米米家智能茶吧机Pro立式版众筹价1099元,集即热、煮茶、消毒于一体。配备2 8英寸触控屏及小爱同学语音控制,5 5升水箱双区双出水,15秒烧开、3秒即热。10升消毒存储空间支持UVC紫外线杀菌和PTC热风烘干,附赠茶壶与分享壶。

年7月设计本选购 五款高色准创作本深度解析
业界动态 · 2026-07-20

年7月设计本选购 五款高色准创作本深度解析

2026年设计专业笔记本选购指南推荐五款:入门华硕无畏16SE、中端惠普战X、高端YOGAAir14与ThinkPadT14p、旗舰联想小新Pro16GT。核心要点为屏幕色准、PantherLake平台AI算力及扩展性,覆盖5000至20000元以上预算。