本系列为本人Java编程方法论 响应式解读系列的 Webflux 部分,现分享出来,前置知识Rxjava2 ,Reactor的相关解读已经录制分享视频,并发布在b站,地址如下:
Rxjava源码解读与分享: www.bilibili.com/video/av345…
Reactor源码解读与分享: www.bilibili.com/video/av353…
NIO源码解读相关视频分享: www.bilibili.com/video/av432…
NIO源码解读视频相关配套文章:
BIO到NIO源码的一些事儿之BIO
BIO到NIO源码的一些事儿之NIO 上
BIO到NIO源码的一些事儿之NIO 中
BIO到NIO源码的一些事儿之NIO 下 之 Selector BIO到NIO源码的一些事儿之NIO 下 Buffer解读 上 BIO到NIO源码的一些事儿之NIO 下 Buffer解读 下
其中,Rxjava与Reactor作为本人书中内容将不对外开放,大家感兴趣可以花点时间来观看视频,本人对着两个库进行了全面彻底细致的解读,包括其中的设计理念和相关的方法论,也希望大家可以留言纠正我其中的错误。
在我们要面临越来越高的并发处理,传统的 Spring Web MVC 已经无法满足我们的需求,即我们需要一个无阻塞的且通过很少的硬件资源(体现就是通过很少数量的线程)的 web 框架来处理并发任务。 Servlet 3.1 确实为非阻塞I/O提供了相应API。但是,使用它时,Servlet 其余部分API的在执行时就是同步(比如 Filter )或阻塞( getParameter , getPart )。 我们知道, Tomcat 这类服务器其有一个 Servlet Worker 线程池,而使用 Spring Web MVC 的话,对于请求的处理过程将会在 DispatcherServlet 中进行,而其内部默认并不会进行异步处理,所以,当有 I/O 或者耗时操作的时候,很可能会阻塞当前 Servlet 所在线程。(参考网上关于 SpringMVC 异步操作的相关博文),关于其异步改造,本人也在 RxJava2 的相关分享视频的项目实例中进行有改造,大家可回顾。而我们的目的就是将当前 Servlet 所在线程给让出来,这样可以接收更多的请求。 那两者的区别到底在什么地方, Spring WebFlux 到底有何意义可供我们迁移学习。相信大家在接触过 Tomcat 之后,都会去学习一下 Tomcat 的配置文件 server.xml ,从中我们也知道 Connector ,其主要功能是:接收连接请求,创建 Request 和 Response 对象用于和请求端交换数据;然后分配线程让 Servlet 容器来处理这个请求,并把产生的 Request 和 Response 对象传给 Servlet 。当 Servlet 处理完请求后,也会通过 Connector 将响应返回给客户端。 所以我们从 Connector 入手,讨论一些与 Connector 有关问题,包括 NIO/BIO 模式、线程池、连接数等。 根据协议的不同, Connector 可以分为 HTTP Connector 、 AJP Connector 等,此处只讨论 HTTP Connector 。
Connector 在处理HTTP请求时,会使用不同的 protocol 。不同的 Tomcat版本 支持的 protocol 不同,其中典型的 protocol 包括 BIO、NIO和APR ( Tomcat7 中支持这 3 种, Tomcat8 增加了对 NIO2 的支持,而在 Tomcat8.5 和 Tomcat9.0 ,则去掉了对 BIO 的支持)。
Connector 使用哪种 protocol ,可以通过 Tomcat 配置文件 server.xml 中的 <connector> 元素中的 protocol 属性进行指定,也可以使用默认值。如果没有指定 protocol ,则使用默认值 HTTP/1.1 ,其含义如下:在 Tomcat7 中,自动选取使用 BIO 或 APR (如果找到 APR 需要的本地库,则使用 APR ,否则使用 BIO );在 Tomcat8 中,自动选取使用 NIO 或 APR (如果找到 APR 需要的本地库,则使用 APR ,否则使用 NIO )。
无论是 BIO ,还是 NIO , Connector 处理请求的大致流程是一样的: 在accept队列中接收连接(当客户端向服务器发送请求时,如果客户端与服务端完成三次握手建立了连接,则服务端将该连接放入accept队列);在连接中获取请求的数据,生成request;调用Servlet容器处理请求;返回response。
为了便于大家的理解,这里先明确一下连接与请求的关系:
TCP 层面的(传输层),对应 socket 。 HTTP 层面的(应用层),必须依赖于 TCP 的连接实现。 TCP 连接中可能传输多个 HTTP 请求。 BIO是Blocking IO,顾名思义是阻塞的IO;NIO是Non-blocking IO,则是非阻塞的IO。而APR是Apache Portable Runtime,是Apache可移植运行库,利用本地库可以实现高可扩展性、高性能;Apr是在Tomcat上运行高并发应用的首选模式,但是需要安装apr、apr-utils、tomcat-native等包。
在 BIO 实现的 Connector 中,请求主要是由 JioEndpoint 对象来处理。 JioEndpoint 维护了 Acceptor 和 Worker ,通过 Acceptor 接收 socket ,然后从 Worker线程池 中找出空闲的线程处理 socket ,如果 worker线程池 没有空闲线程,则 Acceptor 将阻塞。其中 Worker 是 Tomcat 自带的线程池,如果通过 <Executor> 配置了其他线程池,原理与 Worker 类似。
在 NIO 实现的 Connector 中,处理请求的主要实体是 NIOEndpoint 对象。 NIOEndpoint 中除了包含 Acceptor 和 Worker 外,还是用了 Poller ,处理流程如下图所示:
图中 Acceptor 及 Worker 分别是以线程池形式存在, Poller 是一个单线程(此处是其与Netty最大的区别)。注意,与 BIO 的实现一样,这里,需要提及的是,在 server.xml 中没有配置 <Executor> ,则以 Worker线程池 运行,如果配置了 <Executor> ,则以基于 java.util.concurrent.ThreadPoolExecutor 线程池运行。
由图可知, Acceptor 接收 socket 后(这里虽然是基于 NIO 的 connector ,但是在接收 socket 方面还是传统的 serverSocket.accept() 方式,获得 SocketChannel 对象,然后封装在一个 tomcat 的 org.apache.tomcat.util.net.NIOChannel 实现类对象,并将之包装为一个 PollerEvent对象 ),并不是直接使用 Worker 中的线程处理请求,而是先将 PollerEvent对象 发送给了 Poller ,而 Poller 是实现 NIO 的关键。 Acceptor 向 Poller 发送 包装后的请求 通过添加队列的操作实现,这里使用了典型的生产者-消费者模式。同时,在 Poller 中,维护了一个 Selector 对象;当 Poller 从队列中取出 socket 后,注册到该 Selector 中;然后通过遍历 Selector ,找出其中可读的 socket ,并使用 Worker 中的线程处理相应请求。与 BIO 类似, Worker 也可以被自定义的线程池代替。
通过上述过程可以看出,在 NIOEndpoint 处理请求的过程中,无论是 Acceptor 接收 socket ,还是线程处理请求(添加到 Poller 队列是同步的),使用的仍然是阻塞方式;但在 读取socket并交给Worker中的线程 的这个过程中,使用非阻塞的 NIO 实现,这是 NIO 模式与 BIO 模式的最主要区别(其他区别对性能影响较小)。而也是由于这个区别,在并发量较大的情形下可以给Tomcat效率带来显著提升。
目前大多数 HTTP 请求使用的是长连接( HTTP/1.1 默认 keep-alive 为 true ),而长连接意味着,一个 TCP 的 socket 在当前请求结束后,如果没有新的请求到来, socket 不会立马释放,而是等 timeout 后再释放。如果使用 BIO , 读取socket并交给Worker中的线程 这个过程是阻塞的,也就意味着在 socket 等待下一个请求或等待释放的过程中,处理这个 socket 的工作线程会一直被占用,无法释放;因此Tomcat可以同时处理的socket数目不能超过最大线程数,性能受到了极大限制。而使用NIO, 读取socket并交给Worker中的线程 这个过程是非阻塞的(是由 Poller 所在线程维护的),并不会占用工作线程,因此Tomcat可以同时处理的 socket 数目不受最大线程数约束,并发性能也就大大提高,但 Poller 同时也是其性能瓶颈。
因此,随着 NIO 所实现 Connector 的引入,客户端到服务器的通信是非阻塞的,但是服务器到 servlet 的连接仍然是阻塞的,也就意味着每个请求都会阻塞一个线程,也就导致我们会看到一个线程处理一个请求的模型。 因此,随着 Servlet 容器的发展, Servlet API 也就需要非阻塞支持,也就是 Servlet 3.1+ 。
关于 Tomcat 下 Connector 的更多深入解读,有感兴趣的可以参考本人的另一篇博文 tomcat从启动到接轨Servlet二三事