Tomcat NIO 工作原理详解:从 Acceptor、Poller 到容器处理链

平时写 Spring Boot 接口时,我们通常只关心 Controller 能不能收到请求,很少去想 Tomcat 在这之前做了什么。没有把请求是如何流转的讲清楚。要理解 Tomcat NIO,抓住 AcceptorPollerWorker 三类线程就够了。

一、一次请求是怎样被 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 接口有四种重要的子容器:EngineHostContextWrapper。它们是一层包含一层的关系。

容器 表示的含义 常见例子
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 接收请求,然后按照下面的思路寻找处理目标:

  1. Engine 接收 Connector 转交的请求;

  2. 根据 www.example.com 找到对应的 Host;

  3. 根据 /shop 找到部署的 Context;

  4. 根据 Servlet 映射找到对应的 Wrapper;

  5. Wrapper 管理并调用具体 Servlet。

在 Spring Boot 项目中,这个 Servlet 通常就是 DispatcherServlet。至于 /order/list 对应哪个 Controller 方法,则由 Spring MVC 在进入 DispatcherServlet 后继续完成匹配。

这种分层并不是为了把结构设计得复杂,而是让每层只负责一种范围:Engine 管整个引擎,Host 管域名,Context 管应用,Wrapper 管 Servlet。

参考资料

相关推荐
渡我白衣2 小时前
并查集:基础认识与模拟实现
android·java·javascript·数据结构·c++·算法·并查集
电商API_180079052472 小时前
电商商品价格监控工具淘宝京东商品价格抓取API项目实操分享
java·大数据·开发语言·前端·数据挖掘
zzzll11112 小时前
深度剖析:HashMap 并发死循环与 ConcurrentHashMap 两代演进全解
java·开发语言
秋名RG2 小时前
2026/5/27 生产事故复盘与整改方案
java
血小板要健康3 小时前
链表 阶段算法总结
java·数据结构·笔记·算法·leetcode·链表
坐吃山猪3 小时前
【多线程】CompletableFuture使用
java·开发语言·多线程
m0_587383003 小时前
点餐预约核销系统的架构脉络
java·架构·系统架构·需求分析
代码方舟4 小时前
O2O二手车交易平台B端车商管理后台:基于天远车信盟出险构建自动化车况评估网关
java·运维·人工智能·自动化
爱学堂IT分享4 小时前
图灵Java互联网架构师六期
java·开发语言