平时写 Spring Boot 接口时,我们通常只关心 Controller 能不能收到请求,很少去想 Tomcat 在这之前做了什么。没有把请求是如何流转的讲清楚。要理解 Tomcat NIO,抓住 Acceptor、Poller 和 Worker 三类线程就够了。
一、一次请求是怎样被 Tomcat 处理的
先来看一条简化后的处理链路:
客户端连接
↓
Acceptor 接收连接
↓
Poller 监听读写事件
↓
Worker 解析并处理请求
↓
Engine → Host → Context → Wrapper
↓
Servlet / Controller
这三类线程并不是重复做同一件事,而是把"接收连接""等待数据"和"执行业务"拆开处理。
1. Acceptor:负责接收连接
Acceptor 可以理解为 Tomcat 的接待人员。它会阻塞在服务端端口上,等待客户端建立 TCP 连接。连接建立后,Acceptor 对 Socket 做一些基础配置,然后把它交给 Poller 管理。
Acceptor 只负责接收和移交连接,并不会一直跟着某个客户端,更不会亲自执行 Controller 中的业务代码。正因为它的工作比较轻,一般不需要为每个连接创建一个 Acceptor 线程。
2. Poller:负责监听事件
连接交给 Poller 后,会注册到 Java NIO 的 Selector 上。Poller 可以同时观察很多连接,判断哪些连接已经准备好读取数据,或者具备写回响应的条件。
如果客户端只是保持 Keep-Alive,却暂时没有发送新请求,Poller 只需要继续监听它,不必让一个业务线程阻塞在那里等待。这正是 NIO 能管理大量连接的关键。
当 Selector 发现某个连接已经有数据可读时,Poller 会为它创建处理任务,并提交给线程池。
3. Worker:负责真正处理请求
线程池中的工作线程通常被称为 Worker。它会读取并解析 HTTP 报文,生成 Request 和 Response 对象,再通过 Tomcat 的适配与容器处理链,把请求交给对应的 Servlet。
如果项目使用 Spring MVC,请求最终还会进入 DispatcherServlet,之后才匹配到我们编写的 Controller。
因此,Tomcat NIO 并不是完全不使用线程,而是让线程用在真正需要计算和处理业务的阶段。连接处于空闲等待状态时,由少量 Poller 线程统一管理。
二、为什么 Tomcat 8.5 以后不再使用 BIO
传统 BIO 的思路比较直接:接收到一个连接后,通常由一个线程负责处理。当客户端迟迟不发送数据,或者使用 Keep-Alive 保持连接时,这个线程可能一直处于阻塞状态。
如果同时只有几十个连接,BIO 的问题并不明显。但连接数量上升到几千甚至更多时,大量线程会带来明显成本:
-
每个线程都需要占用栈内存;
-
线程数量过多会增加上下文切换;
-
很多线程并没有执行业务,只是在等待网络数据;
-
突发流量下容易迅速耗尽线程资源。
NIO 将"等待网络事件"和"处理业务请求"分开。一个 Poller 可以监听大量连接,只有连接真正就绪时才提交给 Worker。这样可以用相对有限的工作线程支撑更多并发连接。
需要注意的是,NIO 不一定让单个请求执行得更快。如果一条 SQL 本身需要两秒,换成 NIO 也不会让它自动变成两百毫秒。NIO 的主要价值,是减少连接等待期间的线程浪费,提高服务器管理并发连接的能力和整体吞吐量。
三、Tomcat 支持哪些 I/O 模型
不同版本的 Tomcat 支持情况并不完全相同,可以按下面的方式理解:
| I/O 模型 | 特点 | 目前的情况 |
|---|---|---|
| BIO | 阻塞式 I/O,连接等待时容易占用线程 | Tomcat 8.5 起已移除 |
| NIO | 基于 Selector 进行事件轮询 |
当前最常用,也是默认选择 |
| NIO2 | 基于 Java 7 的异步通道 API | 可以使用,但实际项目中不如 NIO 常见 |
| APR/native | 借助本地库实现网络和 TLS 能力 | 属于历史上常见的 Connector;新版本主要保留 Native 对 OpenSSL 的支持 |
常见的 NIO Connector 配置如下:
<Connector
port="8080"
protocol="org.apache.coyote.http11.Http11NioProtocol"
maxThreads="200"
maxConnections="8192"
acceptCount="100" />
如果使用 NIO2,可以把协议类改为:
org.apache.coyote.http11.Http11Nio2Protocol
日常项目没有明确需求时,使用默认 NIO 即可。不要仅仅因为 NIO2 名字里多了一个"2",就认为它一定比 NIO 快。最终效果仍然要结合操作系统、JDK、业务类型和压测结果判断。
四、Tomcat Engine 的作用
一个 Service 可以关联多个 Connector,但只关联一个 Engine。比如 Tomcat 同时提供 HTTP 和 AJP Connector,它们接收到的请求最终都可以交给同一个 Engine 处理。
Engine 的主要作用是:
-
接收当前 Service 下各个 Connector 转交的请求;
-
根据请求中的主机信息选择对应的 Host;
-
找不到匹配主机时使用
defaultHost; -
通过自己的 Pipeline 和 Valve 执行访问日志、认证等公共处理;
-
把响应沿原来的链路交还给 Connector。
Engine 不负责监听 8080 端口,也不直接管理 TCP 连接。端口监听属于 Connector 的职责,Engine 关注的是请求进入 Servlet 容器之后应该交给哪个应用处理。
五、Container 体系中的四种子容器
Tomcat 的 Container 接口有四种重要的子容器:Engine、Host、Context 和 Wrapper。它们是一层包含一层的关系。
| 容器 | 表示的含义 | 常见例子 |
|---|---|---|
| Engine | 整个 Catalina Servlet 引擎 | 一个 Service 对应一个 Engine |
| Host | 一个虚拟主机 | www.example.com |
| Context | 一个 Web 应用 | /shop |
| Wrapper | 一个具体的 Servlet | DispatcherServlet |
假设用户访问:
http://www.example.com:8080/shop/order/list
Tomcat 会先由 Connector 接收请求,然后按照下面的思路寻找处理目标:
-
Engine 接收 Connector 转交的请求;
-
根据
www.example.com找到对应的 Host; -
根据
/shop找到部署的 Context; -
根据 Servlet 映射找到对应的 Wrapper;
-
Wrapper 管理并调用具体 Servlet。
在 Spring Boot 项目中,这个 Servlet 通常就是 DispatcherServlet。至于 /order/list 对应哪个 Controller 方法,则由 Spring MVC 在进入 DispatcherServlet 后继续完成匹配。
这种分层并不是为了把结构设计得复杂,而是让每层只负责一种范围:Engine 管整个引擎,Host 管域名,Context 管应用,Wrapper 管 Servlet。