1. 为什么需要手写Tomcat?
作为Java开发者,我们每天都在使用Tomcat,但很少有人真正理解它的内部工作原理。手写一个简化版的Tomcat(我称之为myTomcat)是深入理解Web服务器本质的最佳实践方式。通过这个项目,你将掌握HTTP协议处理、多线程编程、Servlet容器等核心概念,这些知识对于排查线上问题、性能调优都至关重要。
我在2015年第一次尝试实现简易Tomcat时,发现这比单纯使用Tomcat能更深刻地理解:
- 一个HTTP请求是如何从Socket字节流变成Java对象的
- 线程池如何管理请求处理线程
- Servlet规范在容器层面是如何落地的
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. myTomcat核心架构设计
2.1 整体架构图
code复制[浏览器] -> [Socket连接] -> [myTomcat]
-> [Connector]
-> [ProtocolHandler]
-> [Processor]
-> [Adapter]
-> [Container]
-> [Pipeline]
-> [Valve]
-> [Wrapper]
-> [Servlet]
2.2 关键组件实现
2.2.1 连接器(Connector)实现
连接器负责处理网络I/O,我采用Java NIO实现非阻塞IO模型:
java复制public class NioConnector implements Runnable {
private ServerSocketChannel serverChannel;
private Selector selector;
@Override
public void run() {
while(!Thread.interrupted()) {
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
// 处理IO事件...
}
}
}
关键点:每个Connector需要配置独立的线程池,建议使用自定义的NamedThreadFactory便于问题排查
2.2.2 容器(Container)层次结构
我参考Tomcat设计了四级容器结构:
- Engine:整个Servlet引擎
- Host:虚拟主机
- Context:Web应用上下文
- Wrapper:单个Servlet包装器
java复制public interface Container {
void addChild(Container child);
Container findChild(String name);
void invoke(Request request, Response response);
}
3. HTTP协议处理全流程
3.1 请求解析实现
HTTP请求报文示例:
code复制GET /index.html HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0
解析关键代码:
java复制public class HttpParser {
public Request parse(InputStream input) {
// 第一行解析
String requestLine = readLine(input);
String[] parts = requestLine.split(" ");
String method = parts[0];
String uri = parts[1];
// 头部解析
Map<String,String> headers = new HashMap<>();
String line;
while(!(line = readLine(input)).isEmpty()) {
String[] kv = line.split(":");
headers.put(kv[0].trim(), kv[1].trim());
}
return new Request(method, uri, headers);
}
}
3.2 响应生成实现
响应需要遵循HTTP协议格式:
java复制public class HttpResponse {
public void write(OutputStream output) {
String response = "HTTP/1.1 200 OK\r\n" +
"Content-Type: text/html\r\n" +
"\r\n" +
"<html><body>Hello</body></html>";
output.write(response.getBytes());
}
}
4. Servlet容器实现细节
4.1 Servlet生命周期管理
java复制public class StandardWrapper {
private Servlet instance;
public void load() throws ServletException {
if(instance == null) {
Class<?> clazz = Class.forName(className);
instance = (Servlet) clazz.newInstance();
instance.init(new StandardConfig());
}
}
public void invoke(Request req, Response res) {
load();
instance.service(req, res);
}
}
4.2 请求映射策略
我实现了两种URL匹配方式:
- 精确匹配:完全匹配URL路径
- 通配符匹配:支持
*.do这种模式
java复制public class Mapper {
public Container map(Request request) {
String uri = request.getRequestURI();
// 先尝试精确匹配
Container exact = findExactMatch(uri);
if(exact != null) return exact;
// 尝试通配符匹配
return findWildcardMatch(uri);
}
}
5. 性能优化关键点
5.1 线程池调优
java复制public class Executor {
private ThreadPoolExecutor executor;
public Executor() {
executor = new ThreadPoolExecutor(
10, // 核心线程数
200, // 最大线程数
60, // 空闲时间
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000), // 任务队列
new NamedThreadFactory("myTomcat-worker-")
);
}
}
经验值:IO密集型任务建议线程数 = CPU核心数 * (1 + 平均等待时间/平均计算时间)
5.2 对象池技术
频繁创建Request/Response对象会导致GC压力,我实现了简单对象池:
java复制public class ObjectPool<T> {
private Queue<T> pool = new ConcurrentLinkedQueue<>();
public T borrow() {
T obj = pool.poll();
return obj != null ? obj : createNew();
}
public void release(T obj) {
reset(obj); // 重置对象状态
pool.offer(obj);
}
}
6. 常见问题排查指南
6.1 请求超时问题
现象:客户端收到ReadTimeout异常
排查步骤:
- 检查线程池状态:是否所有线程都被占用
- 检查Servlet实现:是否有同步阻塞操作
- 网络抓包:确认TCP连接是否正常建立
6.2 内存泄漏排查
使用以下JVM参数启动:
code复制-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/path/to/dump.hprof
分析工具推荐:
- Eclipse MAT
- VisualVM
7. 扩展功能实现
7.1 Session管理
java复制public class StandardSession implements Session {
private String id;
private Map<String,Object> attributes;
private long lastAccessedTime;
public Object getAttribute(String name) {
lastAccessedTime = System.currentTimeMillis();
return attributes.get(name);
}
}
7.2 静态资源处理
java复制public class StaticResourceProcessor {
public void process(Request request, Response response) {
String path = request.getRequestURI();
File file = new File(webRoot, path);
if(file.exists()) {
Files.copy(file.toPath(), response.getOutputStream());
}
}
}
8. 测试方案设计
8.1 单元测试重点
- 协议解析测试:模拟各种畸形HTTP请求
- 线程安全测试:使用JMeter模拟并发请求
- 性能测试:使用wrk工具进行压测
8.2 集成测试用例
java复制@Test
public void testGetRequest() throws Exception {
Socket socket = new Socket("localhost", 8080);
OutputStream out = socket.getOutputStream();
out.write("GET /test HTTP/1.1\r\nHost: localhost\r\n\r\n".getBytes());
InputStream in = socket.getInputStream();
byte[] buffer = new byte[1024];
int len = in.read(buffer);
String response = new String(buffer, 0, len);
assertTrue(response.contains("200 OK"));
}
9. 生产环境注意事项
- 线程池监控:需要暴露线程池状态指标
- 优雅停机:实现ShutdownHook处理剩余请求
- 请求限流:防止突发流量打垮服务
实现优雅停机示例:
java复制Runtime.getRuntime().addShutdownHook(new Thread(() -> {
connector.pause();
executor.shutdown();
executor.awaitTermination(30, TimeUnit.SECONDS);
}));
10. 进阶优化方向
- 支持HTTP/2协议
- 实现AIO模型提升吞吐量
- 添加JMX监控接口
- 支持热部署功能
实现热部署的关键:
java复制public class HotDeployer {
public void deploy(File warFile) {
// 1. 创建新的ClassLoader
// 2. 加载新应用
// 3. 替换旧的Context
}
}
通过这个项目,我深刻理解了Web容器的核心原理。建议每个Java开发者都尝试实现自己的简易Tomcat,这比阅读源码更能建立系统性认知。在实现过程中,我最大的收获是理解了IO模型与线程模型的配合方式,这对后续排查生产环境的高并发问题有很大帮助。
