
1. 为什么需要自己实现Tomcat第一次听说要手写Web服务器时很多Java开发者都会疑惑现成的Tomcat、Jetty不香吗确实成熟的Web服务器经过了长期迭代功能完善性能稳定。但亲手实现一个简易版本对理解HTTP协议本质和Servlet容器工作原理有着不可替代的价值。我在2015年第一次尝试这个实践时发现调试过程暴露出的知识盲区比看任何书籍都直接。比如浏览器发送的原始请求数据究竟长什么样Servlet的init()方法到底在什么线程被调用过滤器链的执行顺序由什么决定这些在标准文档中语焉不详的实现细节只有亲手实现时才会真正理解。下面我们就从最基础的Socket通信开始逐步构建一个能运行Servlet的简易容器。2. 核心架构设计2.1 整体架构图一个最简化的Tomcat核心包含以下模块┌───────────────────────────────────────┐ │ MiniTomcat │ │ ┌───────────┐ ┌───────────────┐ │ │ │ Connector │─────│ Engine │ │ │ └───────────┘ │ ┌───────────┐ │ │ │ │ │ Host │ │ │ │ │ └───────────┘ │ │ │ │ ┌───────────┐ │ │ │ │ │ Context │ │ │ │ │ └───────────┘ │ │ │ └───────────────┘ │ └───────────────────────────────────────┘2.2 关键组件职责Connector处理Socket连接解析HTTP原始请求使用ServerSocket监听端口将字节流转换为Request对象支持多线程处理并发请求Engine核心容器管理多个虚拟主机(Host)路由请求到对应HostHost虚拟主机对应一个域名包含多个Web应用(Context)ContextWeb应用上下文对应一个WEB-INF目录加载Servlet类和过滤器维护Servlet映射关系3. 连接器实现细节3.1 Socket通信基础连接器的核心是一个经典的Java网络编程模式ServerSocket serverSocket new ServerSocket(8080); while (true) { Socket socket serverSocket.accept(); new Thread(() - { InputStream input socket.getInputStream(); // 解析HTTP请求 // 构建Response OutputStream output socket.getOutputStream(); output.write(responseBytes); socket.close(); }).start(); }关键点每个请求必须在新线程中处理否则会阻塞后续请求3.2 HTTP协议解析原始HTTP请求示例GET /test?namevalue HTTP/1.1 Host: localhost:8080 Connection: keep-alive User-Agent: Chrome/91.0 (空行) (请求体)解析过程需要读取第一行获取方法、URI和协议版本逐行读取请求头直到遇到空行根据Content-Length读取请求体BufferedReader reader new BufferedReader( new InputStreamReader(input)); String line reader.readLine(); String[] requestLine line.split( ); String method requestLine[0]; String uri requestLine[1];3.3 请求参数处理URI可能包含查询参数/test?namevalueage20需要拆分处理int questionMark uri.indexOf(?); if (questionMark 0) { String queryString uri.substring(questionMark 1); uri uri.substring(0, questionMark); // 解析queryString... }4. 容器核心实现4.1 Servlet生命周期管理标准Servlet生命周期容器启动时调用init()请求到达时调用service()容器关闭时调用destroy()我们的实现需要扫描WEB-INF/web.xml通过反射实例化Servlet类维护Servlet实例池Class? servletClass Class.forName(className); Servlet servlet (Servlet) servletClass.newInstance(); servlet.init(config);4.2 请求分发机制URL到Servlet的映射规则精确匹配优先/test → TestServlet路径匹配次之/admin/* → AdminServlet扩展名最后*.do → ActionServlet实现示例public Servlet getServlet(String uri) { // 1. 检查精确匹配 if (exactMap.containsKey(uri)) { return exactMap.get(uri); } // 2. 检查路径匹配 for (String path : pathMap.keySet()) { if (uri.startsWith(path)) { return pathMap.get(path); } } // 3. 检查扩展名 int dot uri.lastIndexOf(.); if (dot 0) { String ext uri.substring(dot); return extMap.get(ext); } return null; }5. 高级功能实现5.1 会话管理HTTP是无状态的会话通过Cookie实现首次请求生成唯一JSESSIONID响应头设置Set-Cookie: JSESSIONIDxxx后续请求从Cookie头获取会话ID// 生成会话ID String sessionId UUID.randomUUID().toString(); // 创建会话对象 HttpSession session new StandardSession(sessionId); // 设置响应头 response.addHeader(Set-Cookie, JSESSIONID sessionId ; Path/);5.2 过滤器链实现过滤器执行顺序按web.xml中定义的顺序执行doFilter()最后一个过滤器调用servlet.service()反向执行filterChain.doFilter()后的代码核心实现技巧public class ApplicationFilterChain implements FilterChain { private int pos 0; private Servlet servlet; private Filter[] filters; public void doFilter(Request req, Response res) { if (pos filters.length) { filters[pos].doFilter(req, res, this); } else { servlet.service(req, res); } } }6. 性能优化实践6.1 连接器优化原始实现的问题每个请求新建线程开销大线程数无限制可能导致资源耗尽解决方案线程池ExecutorService pool Executors.newFixedThreadPool(100); while (true) { Socket socket serverSocket.accept(); pool.execute(() - processRequest(socket)); }6.2 对象池技术频繁创建的对象Request/Response对象字节数组缓冲区实现对象池public class RequestPool { private static QueueRequest pool new ConcurrentLinkedQueue(); public static Request getRequest() { Request req pool.poll(); return req ! null ? req : new Request(); } public static void release(Request req) { req.reset(); // 重置状态 pool.offer(req); } }7. 常见问题排查7.1 请求挂起无响应可能原因Servlet.service()方法未返回过滤器链未继续传递请求输出流未正确关闭排查步骤检查线程堆栈jstack pid添加请求日志记录检查过滤器链执行顺序7.2 内存泄漏问题常见泄漏点静态Map缓存用户数据未正确释放Servlet实例线程局部变量未清理检测方法使用VisualVM监控堆内存添加finalize()方法日志定期强制GC观察对象回收8. 测试验证方案8.1 单元测试要点关键测试场景不同HTTP方法请求(GET/POST)带参数的URI解析会话跟踪功能验证过滤器链顺序测试使用Mock对象Test public void testRequestParsing() { String httpRequest GET /test?namevalue HTTP/1.1\r\n Host: localhost\r\n\r\n; ByteArrayInputStream input new ByteArrayInputStream(httpRequest.getBytes()); Request request new Request(input); assertEquals(GET, request.getMethod()); assertEquals(/test, request.getUri()); }8.2 性能压测方法使用Apache Benchmarkab -n 10000 -c 100 http://localhost:8080/test关键指标吞吐量(Requests/sec)平均响应时间错误率优化方向调整线程池大小优化对象创建开销减少同步锁竞争9. 扩展思考方向9.1 支持HTTPS实现步骤生成自签名证书使用SSLServerSocket配置安全协议版本SSLContext sslContext SSLContext.getInstance(TLS); KeyStore ks KeyStore.getInstance(JKS); ks.load(new FileInputStream(keystore.jks), password.toCharArray()); KeyManagerFactory kmf KeyManagerFactory.getInstance(SunX509); kmf.init(ks, password.toCharArray()); sslContext.init(kmf.getKeyManagers(), null, null); SSLServerSocketFactory factory sslContext.getServerSocketFactory(); SSLServerSocket serverSocket (SSLServerSocket) factory.createServerSocket(8443);9.2 嵌入式容器模式现代Spring Boot等框架采用嵌入式容器我们的实现也可以调整为提供编程式API启动支持内存中部署应用与主流框架集成MiniTomcat tomcat new MiniTomcat(); tomcat.setPort(8080); tomcat.addWebapp(/app, /path/to/webapp); tomcat.start();这个实现过程让我深刻理解了Tomcat内部工作原理。最意外的收获是发现标准规范中许多看似简单的约定实际实现时却要考虑各种边界情况。比如处理HTTP请求时必须考虑请求头大小写不敏感URL编码解码规则分块传输编码处理建议每个Java Web开发者都尝试这个实践你会对每天使用的工具有全新的认识。