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.
// 将此