阅读 134

Tomcat Connector的BIO与NIO模式的比较及区别

这是我参与8月更文挑战的第16天,活动详情查看:8月更文挑战

详细介绍了Tomcat的Connector组件的BIO和NIO模式以及工作流程。

上一篇文章:Tomcat的核心组件以及server.xml配置全解【一万字】中,我们简单介绍了Connector组件的几种模式,现在我们比较详细的介绍Connector组件的BIO和NIO模式以及工作流程。

1 BIO的connector

BIO的connector实现架构如下:

在这里插入图片描述

在BIO的connector实现中,主要采用org.apache.coyote.http11.Http11Protocol处理请求,Http11Protocol包含了JIoEndpoint对象及Http11ConnectionHandler对象。

JIoEndpoint维护了两个线程池,Acceptor及Worker。Acceptor是接收socket连接,然后从Worker线程池中找出空闲的线程处理socket,如果worker线程池没有空闲线程,则Acceptor将阻塞。Worker线程拿到socket后,就从Http11Processor对象池中获取Http11Processor对象,进一步处理。 Worker线程池是默认线程池,我们前面也介绍了可以自己配置Executor标签并引用为工作线程池!

  1. Http11ConnectionHandler对象维护了一个Http11Processor对象池,Http11Processor对象会解析socket中的http request并封装到Request对象中(这个Request是临时使用的一个类,它的全类名是org.apache.coyote.Request),并创建org.apache.coyote.Request。
  2. 随后调用org.apache.catalina.connector.CoyoteAdapter继续完成解析,构造org.apache.catalina.connector.Request和Response对象,并且通过connector.getService().getContainer().getPipeline().getFirst().invoke(request, response)将请求转发发给对应的Container(Engine,也就是Servlet容器)。
  3. 然后就是另一个流程了,即Engine->Host->Context->Wrapper的处理流程,即Engine找到对应的Web应用并且调用对应的Servlet方法,然后将response通过socket发回client。注意这些操作也都是在Worker线程中完成的。

2 NIO的connector

NIO的connector实现架构如下,比BIO的实现更加复杂:

在这里插入图片描述

在NIO的connector实现中,主要采用org.apache.coyote.http11.Http11NioProtocol处理请求,Http11NioProtocol包含了NioEndpoint对象及Http11ConnectionHandler对象。

NioEndpoint主要包括Acceptor、Poller、Worker组件,如图所示,NioEndpoint的主要流程:

在这里插入图片描述

NioEndpoint中的Acceptor线程用于从Accept队列中接收socket连接,在接收socket方面还是传统的serverSocket.accept()方式,也就是阻塞式的,可以设置多个Accepter线程,这一点和BIO一致。将会获得SocketChannel对象,然后封装在一个tomcat的实现类org.apache.tomcat.util.net.NioChannel对象中。然后将NioChannel对象封装在一个PollerEvent对象中,并将PollerEvent对象压入events queue队列里。

这里是个典型的生产者-消费者模式,Acceptor与Poller线程之间通过events queue队列通信,Acceptor是events queue的生产者,Poller是events queue的消费者。

Poller线程是NIO实现的主要线程,可以有多个Poller线程。Poller线程中维护了一个Selector对象,tomcat的“NIO”就是基于这个Selector来完成逻辑的。Poller从events queue中取出PollerEvent对象,然后将此对象中的channel以OP_READ事件注册到主Selector中,然后Selector执行select操作,遍历出可以读数据的socket(触发OP_READ事件),并从Worker线程池中拿到可用的Worker线程,然后将socket传递给Worker。整个过程是典型的NIO实现。

Worker在获取到从Poller传过来的socket后,将socket封装在SocketProcessor对象中。然后从Http11ConnectionHandler中取出Http11NioProcessor对象,从Http11NioProcessor中调用CoyoteAdapter的逻辑,随后的实现跟BIO的实现一样都是阻塞式的并且是Worker线程来做的。

在架构中,NioEndpoint对象中还维护了一个NioSelectPool对象,这个NioSelectorPool中又维护了一个BlockPoller线程,这个线程就是基于辅Selector进行NIO的逻辑,主要处理阻塞的读写操作,比如用于读取socket请求体的数据和通过response往socket中写数据

当数据(请求体)不可读的时候的时候(客户端数据未发送完毕),会注册封装的 OPEN_READ 事件到 BlockPoller 线程中,然后阻塞当前线程(一般为tomcat io线程),如果OPEN_READ 事件触发,那么将会唤醒阻塞的线程。对于响应数据(响应头和响应体)的写入由 tomcat io 线程进行,当数据不可写的时候(原始 socket 发送缓冲区满),会注册封装的 OPEN_WRITE 事件对象到 BlockPoller 线程中,然后阻塞当前线程,如果OPEN_WRITE事件触发,那么将会唤醒阻塞的线程。这里的当前线程一般就是Worker线程。BlockPoller主要用于减少Poller的压力!

根据上面的内容我们知道,tomcat的NIO connector并非完全是非阻塞的,有的部分,例如Acceptor接收socket,从socket中读、写数据等,还是阻塞模式实现的。只是在将Acceptor获取的socket交给Worker线程的时候由于采用了多路复用模型而是非阻塞的,只有当新的请求到来时才会交给Worker线程,并且请求处理完毕立即释放,而在BIO中,Acceptor每获取一个socket连接就必须马上要一个Worker线程来处理,否则Acceptor线程阻塞!

3 总结

对于BIO的connector,即同步阻塞,tomcat服务器实现模式为一个连接一个Worker线程,即客户端有连接时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情(比如后续的请求并没有到来,或者请求完毕连接还没有释放)会造成不必要的线程开销,非常消耗资源。BIO的connector在最新tomcat8.5及其之后的版本已经移除支持了。

对于NIO的connector,即同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器Selector上,多路复用器轮询到连接有I/O请求事件时才会启动一个Worker线程进行处理,请求处理完毕之后Worker线程马上释放。

为什么NIO可以处理10000个socket连接? 目前大多数HTTP请求使用的是长连接(HTTP/1.1默认keep-alive为true),而长连接意味着,一个TCP的socket在当前请求结束后,如果没有新的请求到来,socket不会立马释放,而是等timeout后再释放。如果使用BIO,“读取socket并交给Worker中的线程”这个过程是阻塞的,也就意味着在socket等待下一个请求或等待释放的过程中,处理这个socket的Worker工作线程会一直被占用,无法释放;因此tomcat可以同时处理的socket数目实际上是不能超过最大线程数的,性能受到了极大限制。而使用NIO,“读取socket并交给Worker中的线程”这个过程是非阻塞的,当socket在等待请求到来或等待释放时,并不会占用工作线程,因此tomcat基于Secoter的IO多路复用机制的可以使得同时处理的socket数目远大于最大线程数,并发性能大大提高。

下面是Tomcat官方对于Connector的描述和对比:

这是BIO和NIO和APR的对比(tomcat.apache.org/tomcat-7.0-…

在这里插入图片描述

这是NIO和NIO2和APR的对比(tomcat.apache.org/tomcat-8.5-…

在这里插入图片描述

可以发现,BIO和NIO在真正的读取和响应数据的时候都是阻塞的(Read Request Body和Write Response Headers and Body),而NIO由于采用多路复用模型,在等待下一次请求的时候(Wait for next Request)是非阻塞的,除此外之外,我们还发现,NIO在读取请求头的时候(Read Request Headers),同样是非阻塞的,为什么呢?

BIO的Read Request Headers是阻塞的,是因为其没有Poller这个I/O多路复用机制,而是直接拿线程池的Worker线程去获取请求头,所以其是阻塞的,因为此时请求数据可能还在进行网卡到操作系统内核缓冲区的传输,并没有真正的到来。

而NIO的Read Request Headers之所以是非阻塞的,是因为通过SocketChannel.read()方法去读取请求头和请求行信息的,读取不到会立即返回,由 poll 线程继续监测下次数据的到来。而NIO对于请求体的读取是阻塞的读取,如果发现请求体数据不可读,那么首先注册封装的 OP_READ 事件到 BlockPoller 对象实例的事件队列里,然后阻塞此Worker线程。

参考资料:

  1. Apache Tomcat 8 Configuration Reference
  2. 深入理解-Tomcat-(二)-从宏观上理解-Tomcat-组件及架构
  3. 深度解读Tomcat中的NIO模型
  4. 详解tomcat的连接数与线程池

如有需要交流,或者文章有误,请直接留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!

文章分类
后端