3.1 Connector 如何接住第一个字节 本节摘要:连接器是 Tomcat 的城门与海关。本节解剖它的内部构造——Acceptor 接受连接、Poller 轮询读写事件、工作线程池执行任务、协议处理器解析报文、适配器完成翻译交接,并用一次线程名观察实验把这套分工"看见"。学完你能解释一个请求从 TCP 握手到进入容器之间的每一步,以及每一步对应哪个可调参数。 旅程正式上路。1.1 的旅程图里"进城"一段,在这里放大成完整的内部构造;本节认识的所有部件名,3.2 与 3.3 会反复使用,也是第 7 章性能排障时的"嫌疑人名单"。 城门与海关:连接器的两副面孔 老手嘴里常说的"城门口那点事",指的是连接器的两副面孔。对外它是城门:监听端口、接受连接、收发字节,纯粹的網絡活。
本节摘要:连接器是 Tomcat 的城门与海关。本节解剖它的内部构造——Acceptor 接受连接、Poller 轮询读写事件、工作线程池执行任务、协议处理器解析报文、适配器完成翻译交接,并用一次线程名观察实验把这套分工"看见"。学完你能解释一个请求从 TCP 握手到进入容器之间的每一步,以及每一步对应哪个可调参数。
旅程正式上路。1.1 的旅程图里"进城"一段,在这里放大成完整的内部构造;本节认识的所有部件名,3.2 与 3.3 会反复使用,也是第 7 章性能排障时的"嫌疑人名单"。
老手嘴里常说的"城门口那点事",指的是连接器的两副面孔。对外它是城门:监听端口、接受连接、收发字节,纯粹的網絡活。对内它是海关:把 HTTP 报文这种"外文"翻译成容器能懂的内部队列,把网络世界的套接字包装成 Java 世界请求对象。两副面孔对应两套技能,Tomcat 把它们分别交给了不同的内部角色。
先看全貌,再逐个上岗。

Acceptor 是唯一直接碰监听套接字的岗位。它在端口上调用 accept,每接到一条 TCP 连接,就把连接包装登记给 Poller,自己立刻回去等下一位。操作系统在它身后维护着等待队列,队列长度就是 acceptCount——城门再宽,排队的地方也有限,队伍塞满后新来的客户端会收到连接拒绝,这是"服务没挂但用户连不上"的第一嫌疑人。
Poller 是 NIO 时代的主角。它拿着一份连接清单反复问内核"谁的数据到了",只有就绪的连接才会被派给工作线程。有了它,线程不再干"守着连接等数据"的傻事——这正是 3.2 要展开的 IO 模型分水岭,此处记住分工即可:Acceptor 管进门,Poller 管叫号。
工作线程池承接叫到号的连接,把后续所有活干完:协议处理器解析报文、适配器翻译交接、容器处理、响应写回。maxThreads 是这个池的天花板,池满时任务在队列里等,表现为请求时延上升而非报错——瓶颈定位时,"变慢"与"被拒"是两种完全不同的线索,前者查线程池,后者查等待队列。
协议处理器与适配器是翻译岗。Http11Processor 按语法把字节切成请求行、头部、体;CoyoteAdapter 把内部对象适配成 Servlet 规范的请求与响应,并顺手把主机名、路径等映射信息补齐,然后把请求"点燃"给 Engine——从这一刻起,请求离开连接器,进入第 4 章的指路系统。
这套分工不是纸上谈兵,线程名字就是证据。启动一个默认配置的 Tomcat,对进程做一次线程转储。
# 找到 Tomcat 进程号 并打印线程概览 pid=$(ps -ef | grep catalina | grep -v grep | awk '{print $2}') jstack $pid | grep -E 'catalina-exec|Poller|Acceptor' | head -n 12
"catalina-exec-1" #14 daemon prio=5 ... waiting on condition "catalina-exec-2" #15 daemon prio=5 ... waiting on condition "http-nio-8080-Poller" #18 daemon prio=5 ... RUNNABLE "http-nio-8080-Acceptor" #19 daemon prio=5 ... RUNNABLE "http-nio-8080-ClientPoller" #20 daemon prio=5 RUNNABLE
三个细节值得驻足。第一,线程名自带结构信息:http-nio-8080 前缀同时暴露了协议处理器的 IO 模型与端口——看到 bio 字样就知道这套配置活在旧世界。第二,exec 是工作线程的编号,数量上限就是 maxThreads;压测时反复抓几次转储,若池里全部线程都 RUNNABLE 且持续增长到 200(默认值),瓶颈大概率在应用处理而非连接器。第三,Poller 与 Acceptor 常年只有一两个线程,它们是"叫号员"不是"办事员",不参与业务处理,不要按工作线程的思路去调它们。
对应到配置,一份带注释的连接器定义把岗位与参数对上号。
<!-- server.xml:岗位与参数的对应关系 --> <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" <!-- 空闲连接等待数据的上限 毫秒 --> maxThreads="200" <!-- 工作线程池天花板 --> minSpareThreads="10" <!-- 池里常备的空闲线程 --> acceptCount="100" <!-- 城门外的等待队列长度 --> maxConnections="8192" <!-- 同一时刻接纳的连接总数 --> redirectPort="8443"/> <!-- 收到需加密请求时转投的端口 -->
⚠️ 常见坑:把 maxThreads 调到几千去"提升并发"。工作线程是真线程,每个都要占栈内存,几千个线程先压垮 JVM 再谈服务。NIO 模式下连接容量由 maxConnections 扛,maxThreads 只需匹配业务处理时长的倒数——这在 3.2 用一道算术讲清。
岗位认齐了,下一节回答那个分水岭问题:BIO 与 NIO 到底差在哪,三道闸门参数怎么配才不自相矛盾。