从零设计一个“微Tomcat”:彻底搞懂NIO如何解决C10K问题 + 手写Pipeline责任链 + 类加载隔离(附完整源码)
关于作者:技术博主,专注底层原理和实战。更多干货请关注 CSDN:CodeStats-CSDN博客
📑 文章目录
-
一、什么是C10K问题?为什么NIO能解决?
-
二、BIO vs NIO:从代码看本质区别
-
三、Tomcat核心架构:一张图看懂容器层级
-
四、启动入口:Catalina如何组装所有组件?
-
五、NIO连接器:手写Selector管理万级连接
-
六、HTTP协议解析:如何把Socket字节流变成Request对象?
-
七、Pipeline-Valve:如何用责任链解耦处理逻辑?
-
八、Servlet映射:如何从URL找到对应的Servlet?
-
九、类加载隔离:如何让不同应用用不同版本的JAR?
-
十、DispatcherServlet:Spring MVC风格的入口
-
十一、完整请求链路串联
-
十二、运行验证
-
十三、核心代码文件清单
一、什么是C10K问题?为什么NIO能解决?
1.1 C10K问题的由来
C10K = Concurrent 10K connections,即单机同时处理1万个网络连接。
这个问题的提出者 Dan Kegel 在 2003 年指出:当时的 Web 服务器(Apache)使用 BIO(阻塞I/O)模型,每个连接占用一个线程,当并发达到 1 万时:
-
需要 1 万个线程
-
每个线程栈默认 1MB → 10GB 内存
-
CPU 大量时间花在线程上下文切换上
-
服务器直接崩溃或响应极慢
1.2 BIO 的瓶颈本质
java
// BIO 模型伪代码
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
Socket socket = serverSocket.accept(); // ① 阻塞:等待连接
new Thread(() -> {
while (true) {
socket.getInputStream().read(); // ② 阻塞:等待数据
// 处理业务...
}
}).start();
}
阻塞点:
-
accept()阻塞 → 线程闲等新连接 -
read()阻塞 → 线程闲等数据到来
结论:BIO 的线程利用率极低,大部分时间在空等。
1.3 NIO 如何解决问题?
NIO 的核心是 Selector(多路复用器) + 非阻塞 Channel:
java
// NIO 模型核心思想
Selector selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false); // 非阻塞
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select(); // 只阻塞到有事件发生
for (SelectionKey key : selector.selectedKeys()) {
if (key.isAcceptable()) {
// 有新连接 → 注册到 Selector
SocketChannel client = serverChannel.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
// 有数据可读 → 交给线程池处理
threadPool.submit(() -> handle(key));
}
}
}
核心突破:
| 对比项 | BIO | NIO |
|---|---|---|
| 线程模型 | 1连接 → 1线程 | 1线程 → 万级连接 |
| 阻塞点 | accept()、read() | 只有 select() |
| 1万连接内存 | ~10GB | ~16MB |
| CPU消耗 | 大量线程切换 | 少量事件处理 |
二、BIO vs NIO:从代码看本质区别
2.1 BIO 版服务器(CodeStats 对比示例)
java
// BIO 版本 - 每个请求一个线程
public class BioServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8080);
ExecutorService threadPool = Executors.newCachedThreadPool();
while (true) {
Socket socket = serverSocket.accept(); // 阻塞点1
threadPool.submit(() -> {
try {
BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
String line;
while ((line = reader.readLine()) != null && !line.isEmpty()) {
// 阻塞点2:read() 等待数据
}
// 处理请求...
} catch (IOException e) {
e.printStackTrace();
}
});
}
}
}
2.2 NIO 版服务器(CodeStats 自研实现)
java
// CodeStats NIO 连接器核心
public class Connector {
private Selector selector;
private ServerSocketChannel serverChannel;
private ExecutorService threadPool;
public void start() throws IOException {
serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false); // 关键:非阻塞
serverChannel.bind(new InetSocketAddress(8080));
selector = Selector.open();
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select(); // 阻塞,但有事件就返回
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove();
if (key.isAcceptable()) {
handleAccept();
} else if (key.isReadable()) {
threadPool.submit(() -> handleRead(key));
}
}
}
}
}
关键差异:BIO 的每个线程都在 read() 上阻塞,NIO 只有极少数线程在 select() 上阻塞。
三、Tomcat核心架构:一张图看懂容器层级
text
┌─────────────────────────────────────────────────────────────────┐
│ Catalina │
│ (启动入口 + 组装器) │
└─────────────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Server │
│ (顶层容器,管理Service) │
└─────────────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Service │
│ (包含 Connector + Engine) │
└──────────────┬──────────────────────────────┬───────────────────┘
│ │
▼ ▼
┌────────────────┐ ┌─────────────────┐
│ Connector │ │ Engine │
│ (NIO连接器) │◄────────────►│ (引擎容器) │
│ - 接收请求 │ │ - 管理Host │
│ - 解析HTTP │ └────────┬────────┘
└────────────────┘ │
▼
┌─────────────────┐
│ Host │
│ (虚拟主机) │
│ - 管理Context │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Context │
│ (Web应用) │
│ - 类加载隔离 │
│ - Servlet映射 │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Wrapper │
│ (Servlet) │
│ - service() │
└─────────────────┘
一句话总结:每个组件持有子组件,形成树形结构,请求从 Connector 进入,逐层向下传递。
四、启动入口:Catalina如何组装所有组件?
java
// 源码:Catalina.java
public class Catalina {
private Server server;
public void load() {
// 1. 创建 Server
StandardServer server = new StandardServer();
// 2. 创建 Service,添加到 Server
StandardService service = new StandardService();
server.addService(service);
// 3. 创建 Connector(NIO 连接器)
Connector connector = new Connector(28080);
service.setConnector(connector);
// 4. 创建容器层级:Engine → Host → Context → Wrapper
StandardEngine engine = new StandardEngine();
service.setEngine(engine);
StandardHost host = new StandardHost();
host.setAppBase("webapps");
engine.addChild(host);
StandardContext context = new StandardContext();
context.setDocBase("webapps/demo");
host.addChild(context);
// 5. 注册 DispatcherServlet
StandardWrapper wrapper = new StandardWrapper();
wrapper.setServletClass("com.omni.framework.spring.mvc.DispatcherServlet");
context.addChild(wrapper);
context.addServletMapping("/*", wrapper.getName());
this.server = server;
}
public void start() {
server.init();
server.start();
}
}
五、NIO连接器:手写Selector管理万级连接
java
// 源码:Connector.java - 完整NIO实现
public class Connector implements Lifecycle {
private ServerSocketChannel serverChannel;
private Selector selector;
private ExecutorService threadPool;
private volatile boolean running = true;
@Override
public void init() {
threadPool = Executors.newCachedThreadPool();
}
@Override
public void start() {
new Thread(() -> {
try {
// 1. 初始化 NIO 组件
serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.socket().bind(new InetSocketAddress(port));
selector = Selector.open();
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
log.info("Connector listening on {}", port);
// 2. NIO 事件循环
while (running) {
selector.select(); // 阻塞到有事件发生
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isAcceptable()) {
// 接受新连接
SocketChannel client = serverChannel.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
log.debug("New connection from {}", client.getRemoteAddress());
} else if (key.isReadable()) {
// 有数据可读,交给线程池处理
SocketChannel client = (SocketChannel) key.channel();
threadPool.submit(() -> processRequest(client));
}
}
}
} catch (IOException e) {
log.error("Connector error", e);
}
}).start();
}
private void processRequest(SocketChannel client) {
try (InputStream in = Channels.newInputStream(client);
OutputStream out = Channels.newOutputStream(client)) {
// 解析 HTTP 请求
Request request = new Request(in);
request.parse();
// 处理响应
Response response = new Response(out);
// 调用 Pipeline 处理
service.getEngine().getPipeline().invoke(request, response);
response.send();
} catch (Exception e) {
log.error("Request processing failed", e);
}
}
}
六、HTTP协议解析:如何把Socket字节流变成Request对象?
java
// 源码:Request.java
public class Request implements HttpServletRequest {
private String method;
private String uri;
private String queryString;
private Map<String, String> headers = new HashMap<>();
private Map<String, String> parameters = new HashMap<>();
private String body;
public void parse() throws Exception {
BufferedReader reader = new BufferedReader(new InputStreamReader(input));
// 1. 解析请求行:GET /demo/user/name?name=Tom HTTP/1.1
String requestLine = reader.readLine();
if (requestLine == null) return;
String[] parts = requestLine.split(" ");
method = parts[0];
// 2. 解析 URI 和 QueryString
String fullUri = parts[1];
int qIdx = fullUri.indexOf('?');
if (qIdx >= 0) {
queryString = fullUri.substring(qIdx + 1);
fullUri = fullUri.substring(0, qIdx);
parseQueryString(queryString, parameters);
}
uri = fullUri;
// 3. 解析 Headers
String line;
while ((line = reader.readLine()) != null && !line.isEmpty()) {
int colon = line.indexOf(':');
if (colon > 0) {
String name = line.substring(0, colon).trim().toLowerCase();
String value = line.substring(colon + 1).trim();
headers.put(name, value);
// 特殊处理 Content-Type
if ("content-type".equals(name) && value.startsWith("multipart/form-data")) {
isMultipart = true;
}
}
}
// 4. 解析 Body(POST 请求)
if ("POST".equalsIgnoreCase(method) && !isMultipart) {
StringBuilder bodyBuilder = new StringBuilder();
while (reader.ready()) {
bodyBuilder.append((char) reader.read());
}
body = bodyBuilder.toString();
// 表单格式也解析到 parameters
String contentType = headers.get("content-type");
if (contentType != null && contentType.startsWith("application/x-www-form-urlencoded")) {
parseQueryString(body, parameters);
}
}
}
private void parseQueryString(String query, Map<String, String> target) {
if (query == null) return;
for (String pair : query.split("&")) {
int eq = pair.indexOf("=");
if (eq > 0) {
try {
String key = URLDecoder.decode(pair.substring(0, eq), "UTF-8");
String val = URLDecoder.decode(pair.substring(eq + 1), "UTF-8");
target.put(key, val);
} catch (Exception ignored) {}
}
}
}
}
七、Pipeline-Valve:如何用责任链解耦处理逻辑?
java
// 源码:SimplePipeline.java
public class SimplePipeline implements Pipeline {
private List<Valve> valves = new ArrayList<>();
private Valve basic;
private Container container;
public SimplePipeline(Container container) {
this.container = container;
}
@Override
public void invoke(Request request, Response response) throws Exception {
new ValveChainImpl().invokeNext(request, response);
}
// 责任链核心:递归调用
private class ValveChainImpl implements Valve.ValveChain {
private int index = 0;
@Override
public void invokeNext(Request request, Response response) throws Exception {
if (index < valves.size()) {
// 执行当前 Valve,并传入链对象
valves.get(index++).invoke(request, response, this);
} else if (basic != null) {
basic.invoke(request, response, this);
} else {
// 没有更多 Valve,交给容器处理
dispatchToContainer(request, response);
}
}
}
// 容器处理逻辑:Engine → Host → Context → Wrapper
private void dispatchToContainer(Request request, Response response) throws Exception {
if (container instanceof Engine) {
// Engine → 找到默认 Host
String hostName = ((Engine) container).getDefaultHost();
Container host = container.findChild(hostName);
host.getPipeline().invoke(request, response);
} else if (container instanceof Host) {
// Host → 根据 Context Path 找到 Context
String contextPath = request.getContextPath();
Container context = container.findChild(contextPath);
if (context != null) {
context.getPipeline().invoke(request, response);
} else {
response.setStatus(404, "Context not found");
}
} else if (container instanceof Context) {
// Context → 根据 URI 找到 Servlet
String servletName = ((Context) container).findServletMapping(request.getUri());
Container wrapper = container.findChild(servletName);
if (wrapper != null) {
wrapper.getPipeline().invoke(request, response);
} else {
response.setStatus(404, "Servlet not found");
}
} else if (container instanceof Wrapper) {
// Wrapper → 调用 Servlet
((Wrapper) container).invokeServlet(request, response);
}
}
}
// 自定义 Valve 示例:日志 Valve
public class AccessLogValve implements Valve {
@Override
public void invoke(Request request, Response response, ValveChain chain) throws Exception {
long start = System.currentTimeMillis();
chain.invokeNext(request, response);
long elapsed = System.currentTimeMillis() - start;
System.out.printf("%s %s %dms%n", request.getMethod(), request.getUri(), elapsed);
}
}
八、Servlet映射:如何从URL找到对应的Servlet?
java
// 源码:StandardContext.java
public class StandardContext extends ContainerBase implements Context {
private Map<String, String> servletMappings = new HashMap<>();
private String defaultServletName;
@Override
public void addServletMapping(String pattern, String servletName) {
servletMappings.put(pattern, servletName);
if ("/*".equals(pattern)) {
this.defaultServletName = servletName;
}
}
@Override
public String findServletMapping(String uri) {
// 1. 精确匹配:/user/info
String servletName = servletMappings.get(uri);
if (servletName != null) return servletName;
// 2. 路径前缀匹配:/user/* → 匹配 /user/info、/user/list
for (Map.Entry<String, String> entry : servletMappings.entrySet()) {
String pattern = entry.getKey();
if (pattern.endsWith("/*")) {
String prefix = pattern.substring(0, pattern.length() - 2);
if (uri.startsWith(prefix)) {
return entry.getValue();
}
}
}
// 3. 后缀匹配:*.do(扩展实现)
for (Map.Entry<String, String> entry : servletMappings.entrySet()) {
String pattern = entry.getKey();
if (pattern.startsWith("*.")) {
String suffix = pattern.substring(1);
if (uri.endsWith(suffix)) {
return entry.getValue();
}
}
}
// 4. 默认 Servlet(/*)
return defaultServletName;
}
}
九、类加载隔离:如何让不同应用用不同版本的JAR?
java
// 源码:WebappClassLoader.java
public class WebappClassLoader extends URLClassLoader {
public WebappClassLoader(ClassLoader parent, URL[] urls) {
super(urls, parent);
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 1. 已加载过的类直接返回
Class<?> clazz = findLoadedClass(name);
if (clazz != null) return clazz;
// 2. 系统类(Java 核心库)委托给父加载器
if (name.startsWith("java.") || name.startsWith("javax.") || name.startsWith("sun.")) {
return super.loadClass(name, resolve);
}
// 3. 优先从当前 Web 应用加载(打破双亲委派!)
try {
clazz = findClass(name);
if (clazz != null) {
if (resolve) resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// 当前应用没有,继续委托父加载器
}
// 4. 委托给父加载器
return super.loadClass(name, resolve);
}
}
为什么打破双亲委派:
-
双亲委派:子 → 父 → 祖父(向上委托)
-
Tomcat 需要:Web 应用优先加载自己的类(WEB-INF/classes、WEB-INF/lib)
-
这样应用 A 可以用 Guava 23.0,应用 B 可以用 Guava 31.0,互不冲突
十、DispatcherServlet:Spring MVC风格的入口
java
// 源码:DispatcherServlet.java
public class DispatcherServlet extends HttpServlet {
private List<HandlerMapping> handlerMappings = new ArrayList<>();
private ObjectMapper objectMapper = new ObjectMapper();
@Override
public void init() throws Exception {
// 从 IoC 容器获取所有 Controller
AbstractApplicationContext ctx = SpringContextHolder.getApplicationContext();
ConfigurableListableBeanFactory beanFactory = ctx.getBeanFactory();
Map<String, Object> controllers = beanFactory.getBeansOfType(Object.class);
for (Object controller : controllers.values()) {
Class<?> clazz = controller.getClass();
// 获取类上的 @RequestMapping 路径
String basePath = "";
if (clazz.isAnnotationPresent(RequestMapping.class)) {
basePath = clazz.getAnnotation(RequestMapping.class).value();
if (!basePath.startsWith("/")) basePath = "/" + basePath;
}
// 遍历方法,注册 @RequestMapping
for (Method method : clazz.getDeclaredMethods()) {
if (method.isAnnotationPresent(RequestMapping.class)) {
String methodPath = method.getAnnotation(RequestMapping.class).value();
String fullPath = (basePath + methodPath).replaceAll("//", "/");
boolean isResponseBody = method.isAnnotationPresent(ResponseBody.class);
handlerMappings.add(new HandlerMapping(fullPath, controller, method, isResponseBody));
log.debug("Mapped {} -> {}", fullPath, method);
}
}
}
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res) throws Exception {
processRequest(req, res);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse res) throws Exception {
processRequest(req, res);
}
private void processRequest(HttpServletRequest req, HttpServletResponse res) throws Exception {
String uri = req.getUri();
for (HandlerMapping hm : handlerMappings) {
if (hm.path.equals(uri)) {
// 1. 解析方法参数(@RequestParam、@PathVariable、@RequestBody)
Object[] args = ParamBinder.resolveParameters(hm.method, req, res, new HashMap<>());
// 2. 反射调用 Controller 方法
Object result = hm.method.invoke(hm.controller, args);
// 3. 处理返回值
if (hm.isResponseBody) {
res.setContentType("application/json; charset=utf-8");
String json = result instanceof String ? (String) result : toJson(result);
res.write(json);
} else {
if (result != null) res.write(result.toString());
}
res.send();
return;
}
}
// 404
res.setStatus(404, "Not Found");
res.write("<h1>404 - Not Found</h1>");
res.send();
}
private String toJson(Object obj) {
try {
return objectMapper.writeValueAsString(obj);
} catch (Exception e) {
return JsonUtil.toJson(obj); // 降级方案
}
}
}
十一、完整请求链路串联
text
┌─────────────────────────────────────────────────────────────────────┐
│ 1. 浏览器发送请求 │
│ GET /omni/demo/user/name HTTP/1.1 │
└─────────────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 2. Connector(NIO)接收 Socket 请求 │
│ - Selector 监听 OP_ACCEPT → 接受新连接 │
│ - Selector 监听 OP_READ → 线程池处理读取 │
└─────────────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 3. Request.parse() 解析 HTTP 协议 │
│ - 解析请求行 → method=GET, uri=/omni/demo/user/name │
│ - 解析 Headers → Content-Type, Host... │
│ - 解析 Body(POST时) │
└─────────────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 4. Engine.Pipeline 处理 │
│ - AccessLogValve:记录开始时间 │
│ - 根据 Host 头找到对应 Host │
└─────────────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 5. Host.Pipeline 处理 │
│ - 根据 Context Path(/omni)找到 Context │
└─────────────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 6. Context.Pipeline 处理 │
│ - WebappClassLoader 加载 Web 应用类 │
│ - 根据 URI 匹配 Servlet 映射 → /* → DispatcherServlet │
└─────────────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 7. Wrapper.Pipeline 处理 │
│ - 调用 servlet.service(request, response) │
└─────────────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 8. DispatcherServlet 分发到 Controller │
│ - 匹配 @RequestMapping("/user/name") │
│ - 反射调用 userController.getName() │
│ - @ResponseBody → JSON 序列化 │
└─────────────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 9. Response 构建 HTTP 响应 │
│ - 设置状态码 200 │
│ - 设置 Content-Type: application/json │
│ - 写入响应体 "Jerry" │
│ - Socket 返回 │
└─────────────────────────────────────────────────────────────────────┘
十二、运行验证
12.1 下载与启动
bash
# 克隆项目 git clone https://gitee.com/zhouzuoli/code-stats.git # 进入目录 cd code-stats # Maven 编译 mvn clean package # 启动服务 java -jar target/CodeStats.jar
12.2 验证 NIO 连接器
bash
# 测试 GET 请求
curl http://localhost:28080/omni/demo/user/name
# 返回: Jerry ✅
# 测试带参数的 GET
curl "http://localhost:28080/omni/demo/hello/text?name=Tom"
# 返回: Hello, Tom! ✅
# 测试 POST 请求
curl -X POST http://localhost:28080/omni/demo/user/manage/create \
-H "Content-Type: application/json" \
-d '{"name":"张三","age":25}'
# 返回: {"success":true} ✅
12.3 观察启动日志
text
[INFO] Connector listening on 28080 [INFO] DispatcherServlet 初始化完成,共映射 15 个处理器 [INFO] Tomcat started in 234 ms
十三、核心代码文件清单
| 文件路径 | 作用 | 核心代码行数 |
|---|---|---|
tomcat/Catalina.java |
组装所有组件 | ~80 |
tomcat/connector/Connector.java |
NIO 连接器 | ~120 |
tomcat/connector/Request.java |
HTTP 协议解析 | ~100 |
tomcat/container/SimplePipeline.java |
责任链实现 | ~80 |
tomcat/container/StandardContext.java |
Servlet 映射 | ~100 |
tomcat/loader/WebappClassLoader.java |
类加载隔离 | ~50 |
spring/mvc/DispatcherServlet.java |
Spring MVC 入口 | ~150 |
开源项目地址
Gitee:https://gitee.com/zhouzuoli/code-stats.git
如果本文让你真正搞懂了 Tomcat 和 NIO 解决 C10K 的原理,请 Star 支持一下!
欢迎在评论区讨论:你遇到过的最大并发量是多少?用什么方案解决的?
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)