Tomcat NIO 模型的实现
Tomcat 对 BIO 和 NIO 两种模型都进行了实现,其中 BIO 的实现理解起来比较简单,而 NIO 的实现就比较复杂了,并且它跟常用的 Reactor 模型也略有不同,具体设计如下:

可以看出多了一个 BlockPoller 的设计,这是因为在 Servlet 规范中 ServletInputStream 和 ServletOutputStream 是阻塞的,所以请求体和响应体的读取和发送需要阻塞处理。请求行读取和 SSL 握手使用非阻塞的 Poller 处理。一次连接基本的处理流程是:
- Acceptor 接收 TCP 连接,并将其注册到 Poller 上
- Poller 发现通道有就绪的 I/O 事件,将事件分配给线程池中的线程处理
- 线程池线程首先在 Poller 上非阻塞完成请求行和 SSL 握手的处理,然后通过容器调用 Servlet,生成响应,最后如果需要读取请求体或者发送响应,那就会将通道注册到 BlockPoller 上模拟阻塞完成
接下来分析核心代码的实现,源码来自 Tomcat 6.0.53 版本,之所以使用这个版本是因为看起来简单直观没有太多的抽象,也不影响来理解核心的处理逻辑。首先看下连接处理的方法调用情况,可右键直接打开图片查看大图:

相关类或接口的功能如下:
- Acceptor: 阻塞监听和接收通道连接
- Poller: 事件多路复用器,通知 I/O 事件的发生并分配合适的处理器
- PollerEvent: 是对通道、SelectionKey 的附加对象和通道关注事件的封装
- SocketProcessor: 线程池调度单元,它处理 SSL 握手,调用 Handler 解析协议
- Handler: 通道处理接口,用于适配多种协议,如 HTTP、AJP
- NioEndpoint: 服务启停初始化的入口
- NioSelectorPool: 提供一个阻塞读写使用的 Selector 池和一个单例 Selector
- NioBlockingSelector: 提供阻塞读和写的方法
- BlockPoller: 与 NioBlockingSelector 配合完成模拟阻塞
- NioChannel: 封装 SocketChannel 和读写使用的 ByteBuffer
- KeyAttachment: Key 的附加对象,它包含通道最后访问时间和用于模拟阻塞使用的读写闭锁
1. Acceptor 注册通道到 Poller 上
Acceptor 和 Poller 分属两个不同的线程,通常情况下 Poller 阻塞在 select() 方法的调用上,此方法会锁住内部的 publicKeys 集合,所以 Acceptor 接收到通道连接不能直接注册到 Poller 上,否则会造成死锁。Tomcat 使用生产者-消费者模式来进行并发协作,缓冲区使用的是 ConcurrentLinkedQueue 无界队列。
Acceptor 接收到连接的 SocketChannel 后,将其配置成非阻塞模式,封装成 NioChannel,最后调用 getPoller0().register(NioChannel) 加入到某个 Poller 的事件队列中。
public void register(final NioChannel socket) {
socket.setPoller(this); // 关联此 Poller
KeyAttachment key = keyCache.poll();
final KeyAttachment ka = key!=null?key:new KeyAttachment();
// 重置或者初始化 KeyAttachment 对象
ka.reset(this,socket,getSocketProperties().getSoTimeout());
PollerEvent r = eventCache.poll();
// 声明此通道关注的事件
ka.interestOps(SelectionKey.OP_READ);//this is what OP_REGISTER turns into.
// 将此通道和 SelectionKey 附件对象封装成 PollerEvent 对象
if ( r==null) r = new PollerEvent(socket,ka,OP_REGISTER);
else r.reset(socket,ka,OP_REGISTER);
// 加入到 Poller 的 events 队列中
addEvent(r);
}
public void addEvent(Runnable event) {
events.offer(event); // 插入队列
if ( wakeupCounter.incrementAndGet() == 0 )
selector.wakeup(); // 唤醒 Selector
}
Poler 有个 events() 方法,用于遍历事件队列进行处理,events() 会在 select 调用超时或者被唤醒且没有通道发生 I/O 事件时被调用,代码如下:
public boolean events() {
boolean result = false;
Runnable r = null;
// 遍历事件队列
while ( (r = events.poll()) != null ) {
result = true;// 有事件待处理
try {
r.run(); // 本质调用的是 PollerEvent.run()
if ( r instanceof PollerEvent ) {
// 重置并缓存 PollerEvent 对象
((PollerEvent)r).reset();
eventCache.offer((PollerEvent)r);
}
} catch ( Throwable x ) {
log.error("",x);
}
}
return result;
}
可以看出这里有个关键对象 PollerEvent,它内部有个 interestOps 属性,表示要处理的事件类型,它有三个可能的值分别是:
- NioEndpoint.OP_REGISTER: 通道注册事件
- SelectionKey.OP_READ: 通道重新声明在 Poller 上关注读事件
- SelectionKey.OP_WRITE: 通道重新声明在 Poller 上关注写事件
OP_REGISTER 的处理就是将通道注册到 Selector 上的最终实现,代码如下:
if ( interestOps == OP_REGISTER ) {
try {
// 将 SocketChannel 注册到 Poller 的 Selector 上并指定关注的事件和附加对象
socket.getIOChannel().register(socket.getPoller().getSelector(), SelectionKey.OP_READ, key);
} catch (Exception x) {
log.error("", x);
}
}
至此已完成了通道注册,接下来看一下 PollerEvent 为什么还要处理 OP_READ 和 OP_WRITE 事件。
2. PollerEvent 对 OP_READ 和 OP_WRITE 的处理
PollerEvent(又或者说 Poller)要处理读写事件,就是因为程序需要一次非阻塞的读或写操作。一开始通道是在 Poller 上声明关注的事件,但是在发生 I/O 事件后,Poller 就会把此通道就绪的事件从它关注的事件中移除(原因见下文),所以如果需要非阻塞的读或写,只能再次在这个 Poller 上重新声明。
解析请求行是非阻塞的,解析过程中,由于 TCP 存在粘包/拆包的问题,可能导致数据读取不完整,需要再次从通道读取,此时就要在关联的 Poller 上重新关注读事件,核心代码:
// 拿到通道在 Poller 上对应的 SelectionKey
final SelectionKey key = socket.getIOChannel().keyFor(socket.getPoller().getSelector());
try {
boolean cancel = false;
if (key != null) {
...
// 将 interestOps 合并到 key 现有关注的事件集合中
int ops = key.interestOps() | interestOps;
// 更新 key 和 附加对象关注的操作
att.interestOps(ops);
key.interestOps(ops);
att.setCometOps(ops);
} else {
cancel = true;
}
}catch (CancelledKeyException ckx) {}
3. Poller 对 I/O 事件的处理
Poller 就是 Reactor,主要功能是将就绪的 SelectionKey 分配给处理器处理,此外它还检查通道是否超时。它在调用 select 方法时会根据条件确定是阻塞还是非阻塞,代码如下:
if ( !close ) {
if (wakeupCounter.getAndSet(-1) > 0) {
// wakeupCounter 大于0,意味着 Acceptor 接收了大量连接,产生大量 PollerEvent 急
// 需 Poller 消费处理,此时进行一次非阻塞调用
keyCount = selector.selectNow();// 非阻塞直接返回
} else {
// wakeupCounter 等于0,阻塞等待 IO 事件发生或被唤醒
keyCount = selector.select(selectorTimeout);
}
wakeupCounter.set(0);
}
当有通道 I/O 事件就绪时,Poller 将会创建一个 SocketProcessor 提交线程池处理,具体代码不再贴出。在这个过程中有一个将当前就绪的事件从 SelectionKey 中移除的操作,这是为了后续能够在 BlockPoller 上阻塞读写时,防止多个线程的干扰,具体代码如下:
protected void unreg(SelectionKey sk, KeyAttachment attachment, int readyOps) {
// 取反再与 - 表示从 sk.interestOps() 中清除 readyOps 所在的位
reg(sk,attachment,sk.interestOps()& (~readyOps));
}
protected void reg(SelectionKey sk, KeyAttachment attachment, int intops) {
sk.interestOps(intops);
attachment.interestOps(intops);
//attachment.setCometOps(intops);
}
检查超时的方法是 Poller.timedout(keyCount, hasEvents),它在 Poller 的每次循环上都被调用,但不是每次都处理超时,因为这会产生过多的负载,而超时可等待几秒钟再超时也没事。Poler 有一个名为 nextExpiration 的成员变量,它表示检查超时的最短时间间隔,在这个时间内,如果只是 select() 调用超时(表示负载不大)会执行处理超时。
4. SocketProcessor 的处理
SocketProcessor 处理 SSL 握手和调用 Handler 进行实际的 I/O 操作。Handler 的子类 Http11ConnectionHandler 会创建 一个 Http11NioProcessor 对象最终处理 Socket,这里不分析具体的协议处理,来看看几种处理结果:
public SocketState process(NioChannel socket) {
Http11NioProcessor processor = null;
try {
processor = connections.remove(socket);
...
SocketState state = processor.process(socket);
if (state == SocketState.LONG) {
// 在处理request和生成response之间,保持socket和此processor的关联
connections.put(socket, processor);
// 通常是收到了不完整的请求行,再次以 OP_READ 注册到 Poller 上
socket.getPoller().add(socket);
} else if (state == SocketState.OPEN) {
// 长连接,Http 保活,回收 processor
release(socket, processor);
// 此时已处理一个完整的请求并响应,再次注册到 Poller 上,等待处理下个请求
socket.getPoller().add(socket);
} else if (state == SocketState.SENDFILE) {
// 处理文件
connections.put(socket, processor);
} else {
// 连接关闭,回收 processor
release(socket, processor);
}
return state;
} catch (...) {...}
release(socket, processor);
return SocketState.CLOSED;
}
5. 模拟阻塞的实现
模拟阻塞是通过 NioBlockingSelector 和 BlockPoller,以及 KeyAttachment 中的两个 CountDownLatch 读写闭锁合作完成。这里分析阻塞读,阻塞写的实现类似。一般的,在读取 POST 请求参数时会使用模拟阻塞完成,来看下 NioBlockingSelector.read() 方法的具体实现:
public int read(ByteBuffer buf, NioChannel socket, long readTimeout) throws IOException {
// 拿到通道在 Poller 上注册的 key
SelectionKey key = socket.getIOChannel().keyFor(socket.getPoller().getSelector());
if ( key == null ) throw new IOException("Key no longer registered");
KeyReference reference = new KeyReference();
// key 的附加对象
KeyAttachment att = (KeyAttachment) key.attachment();
int read = 0; // 读取的字节数
boolean timedout = false; // 是否超时
int keycount = 1; //assume we can write 假设通道可读
long time = System.currentTimeMillis(); //start the timeout timer
try {
while ( (!timedout) && read == 0) {
if (keycount > 0) { //only read if we were registered for a read
// 尝试读取一次,如果通道无数据可读则返回 0,若连接断开则返回 -1
int cnt = socket.read(buf);
if (cnt == -1) throw new EOFException();
read += cnt;
if (cnt > 0) break;
}
try {
// 初始化读闭锁
if ( att.getReadLatch()==null || att.getReadLatch().getCount()==0) att.startReadLatch(1);
// 将此通道注册到 BlockPoller,关注读取事件
poller.add(att,SelectionKey.OP_READ, reference);
// 阻塞等待通道可读
att.awaitReadLatch(readTimeout,TimeUnit.MILLISECONDS);
}catch (InterruptedException ignore) {
Thread.interrupted();
}
if ( att.getReadLatch()!=null && att.getReadLatch().getCount()> 0) {
// 被打断了,但是没有接收到 blockPoller 的提醒
keycount = 0;
// 继续循环等待可读
}else {
//通道可读,重置读闭锁
keycount = 1;
att.resetReadLatch();
}
if (readTimeout > 0 && (keycount == 0)) // 如果超时了,则不再读取,抛异常
timedout = (System.currentTimeMillis() - time) >= readTimeout;
} //while
if (timedout)
throw new SocketTimeoutException();
} finally {
poller.remove(att,SelectionKey.OP_READ); // 移除注册
if (timedout && reference.key!=null) {
poller.cancelKey(reference.key); // 超时取消
}
reference.key = null;
}
return read;
}
BlockPoller 实现逻辑与 Poller 大致相同,不同的地方在于对就绪 key 的处理,核心代码如下:
Iterator<SelectionKey> iterator = keyCount > 0 ? selector.selectedKeys().iterator() : null;
while (run && iterator != null && iterator.hasNext()) {
SelectionKey sk = iterator.next();
KeyAttachment attachment = (KeyAttachment)sk.attachment();
try {
attachment.access();
iterator.remove();
// 移除已就绪的事件
sk.interestOps(sk.interestOps() & (~sk.readyOps()));
// 可读或可写时减少对应闭锁的值,此时阻塞在 NioBlockingSelector.read() 上的线程继续执行读取
if ( sk.isReadable() ) {
countDown(attachment.getReadLatch());
}
if (sk.isWritable()) {
countDown(attachment.getWriteLatch());
}
}catch (CancelledKeyException ckx) {
if (sk!=null) sk.cancel();
countDown(attachment.getReadLatch());
countDown(attachment.getWriteLatch());
}
}//while
6. 小结
至此,本文对连接的接收、分发以及模拟阻塞的核心代码实现进行了分析,为了更好的理解内部流程,尽可能的使用简洁的代码仿写了这部分功能。
源码地址:https://github.com/tonwu/rxtomcat 位于 rxtomcat-net 模块
7. Tomcat 8.5 版本变化
7.1 替换缓存数据结构
Tomcat 对 PollerEvent、NioChannel 和 Processor 对象进行了缓存,目的是减少 GC 提高系统性能,这是一种用空间换时间,被称为对象池的优化手段。从版本 8.* 开始,缓存数据结构从 ConcurrentLinkedQueue 换成了自定义的同步栈 SynchronizedStack。SynchronizedStack 的 javadoc 明确说明:
当需要创建一个无需缩小的可重用对象池时,这是 ConcurrentLinkedQueue 无 GC 的主要替代方案。目的是尽可能快地以最少的垃圾提供最少的所需功能。
在这个特殊的情况下,ConcurrentLinkedQueue 有很多功能是不需要的,所以就实现了一个有重点的类,可以专注完成一件事,来提升性能。但它不是 ConcurrentLinkedQueue 的替代品。
7.2 LimitLatch
Acceptor 在接收连接前添加了一个 LimitLatch(类似信号量)来控制总连接数。分析下如果不加有什么现象,在极端情况下,线程池没有空闲线程并且它内部的队列已满,当有通道发生可读或可写事件时,Poller 会关闭此通道,此时系统负载已达到最高,如果 Acceptor 还在继续接收连接并请求注册,而不加限制,那么就会一直重复 PollerEvent 入队出队和 Poller 单纯关闭通道的操作,浪费系统资源。
Tomcat NIO 模型的实现的更多相关文章
- Tomcat NIO
说起Tomcat的NIO,不得不提的就是Connector这个Tomcat组件.Connector是Tomcat的连接器,其主要任务是负责处理收到的请求,并创建一个Request和Response的对 ...
- 深度解读Tomcat中的NIO模型(转载)
转自https://www.jianshu.com/p/76ff17bc6dea 一.I/O复用模型解读 Tomcat的NIO是基于I/O复用来实现的.对这点一定要清楚,不然我们的讨论就不在一个逻辑线 ...
- Java网络编程与NIO详解10:深度解读Tomcat中的NIO模型
本文转自:http://www.sohu.com/a/203838233_827544 本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 ht ...
- tomcat 线程模型
最近看到了内网ATA上的一篇断网故障时Mtop触发tomcat高并发场景下的BUG排查和修复(已被apache采纳),引起了我的好奇,感觉原作者对应底层十分了解,写的很复杂.原来对于tomcat的线程 ...
- tomcat NIO配置
1.tomcat NIO配置 今天在查看日志时发现tomcat的Socket连接方式为bio,于是我想既然有bio那肯定有nio.果然,一查就发现tomcat在6.0之后就可以配置nio的方式.nio ...
- NIO模型学习笔记
NIO模型学习笔记 简介 Non-blocking I/O 或New I/O 自JDK1.4开始使用 应用场景:高并发网络服务器支持 概念理解 模型:对事物共性的抽象 编程模型:对编程共性的抽象 BI ...
- Tomcat线程模型分析及源码解读
1 四种线程模型 配置方法:在tomcat conf 下找到server.xml,在<Connector port="8080" protocol="HTTP/1 ...
- 浅析tomcat nio 配置
[尊重原创文章摘自:http://blog.csdn.net/yaerfeng/article/details/7679740] tomcat的运行模式有3种.修改他们的运行模式.3种模式的运行是否成 ...
- 关于 tomcat nio connector, servlet 3.0 async, spring mvc async 的关系
tomcat 的 org.apache.coyote.http11.Http11NioProtocol Connector 是一个使用 Java NIO 实现的异步 accept 请求的 connec ...
随机推荐
- 【BZOJ 2713】[Violet 2]愚蠢的副官&&【BZOJ1183】[Croatian2008]Umnozak——【数位DP】
题目链接: 2713传送门 1183传送! 题解: 由于看不懂英文题解(十个单词十一个不认识……),所以只能自己想QAQ. 其实乱搞就好= =. 首先我们发现,各位数字乘积要在1e9以下才可能有用,这 ...
- BZOJ1467_Pku3243 clever Y_EXBSGS
BZOJ1467_Pku3243 clever Y_EXBSGS Description 小Y发现,数学中有一个很有趣的式子: X^Y mod Z = K 给出X.Y.Z,我们都知道如何很快的计算K. ...
- BZOJ_1579_[Usaco2009 Feb]Revamping Trails 道路升级_分层图最短路
BZOJ_1579_[Usaco2009 Feb]Revamping Trails 道路升级_分层图最短路 Description 每天,农夫John需要经过一些道路去检查牛棚N里面的牛. 农场上有M ...
- Python初学者必看(1)
python介绍 python的创始人为吉多·范罗苏姆(Guido van Rossum).1989年的圣诞节期间,吉多·范罗苏姆为了在阿姆斯特丹打发时间,决心开发一个新的脚本解释程序,作为ABC语言 ...
- centos7安装libgdiplus。netcore生成验证码,处理图片
yum install autoconf automake libtool yum install freetype-devel fontconfig libXft-devel yum install ...
- 基于pytorch的电影推荐系统
本文介绍一个基于pytorch的电影推荐系统. 代码移植自https://github.com/chengstone/movie_recommender. 原作者用了tf1.0实现了这个基于movie ...
- 经典卷积神经网络结构——LeNet-5、AlexNet、VGG-16
经典卷积神经网络的结构一般满足如下表达式: 输出层 -> (卷积层+ -> 池化层?)+ -> 全连接层+ 上述公式中,“+”表示一个或者多个,“?”表示一个或者零个,如“卷积层+ ...
- 【重学计算机】操作系统D5章:文件系统
1. 文件系统 文件系统概述 文件的组织: 逻辑结构:流式.记录式 物理结构:顺序.连接.直接.索引 文件的存取:顺序.直接.索引 文件的控制:逻辑控制.物理控制 文件的使用:打开.关闭.读.写.控制 ...
- 为什么设置overflow为hidden可以清除浮动带来的影响
1.问题起源 在平时的业务开发写CSS中,为了满足页面布局,元素的浮动特性我们用的不能再多了.使用浮动的确能够解决一些布局问题,但是也带了一些副作用影响,比如,父元素高度塌陷,我们有好几种可以清除浮动 ...
- PageHelper分页异常(java.base/java.util.ArrayList cannot be cast to com.github.pagehelper.Page)
在SqlMapConfig.xml里面配置分页插件 applicationContext-service.xml里面的配置,我出现问题谁因为,在salSessionFactory里没注入全局配置文件