本系列为本人Java编程方法论 响应式解读系列的Webflux
部分,现分享出来,前置知识Rxjava2 ,Reactor的相关解读已经录制分享视频,并发布在b站,地址以下:java
Rxjava源码解读与分享:www.bilibili.com/video/av345…web
Reactor源码解读与分享:www.bilibili.com/video/av353…apache
NIO源码解读相关视频分享: www.bilibili.com/video/av432…编程
NIO源码解读视频相关配套文章:tomcat
BIO到NIO源码的一些事儿之NIO 下 之 Selector BIO到NIO源码的一些事儿之NIO 下 Buffer解读 上 BIO到NIO源码的一些事儿之NIO 下 Buffer解读 下异步
其中,Rxjava与Reactor做为本人书中内容将不对外开放,你们感兴趣能够花点时间来观看视频,本人对着两个库进行了全面完全细致的解读,包括其中的设计理念和相关的方法论,也但愿你们能够留言纠正我其中的错误。socket
在咱们要面临愈来愈高的并发处理,传统的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二三事