【SpringBoot + Tomcat】【一】请求到达后端服务进程后的处理过程-连接器的创建和执行
1 前言
这节我主要是想看下,Tomcat 如何接收到请求并且是怎么一步步封装并交给 SpringMVC 处理的。这块之前一直没太深入的了解过,所以这节我们来看看。
在看这节之前,你首先要清楚这两个问题,方便你更好的去理解。
(1)SpringBoot启动的过程中,Tomcat 的创建和启动时机是在什么时候呢?不知道的话,可以看我的这篇。
(2)我们都知道 SpringMVC 其实可以理解为就是一个 DispatchServlet ,那么它是如何跟 Tomcat 挂上关系的呢?不知道的话,可以看我的这篇 还有这篇现代和传统的启动方式的区别会说现代 DispatchServlet 是如何放入 Tomcat 中的。
如果你看完还是有点模糊的话,也不要紧,下边我会着重挑一些细节拿出来带大家再看一遍。
2 Tomcat 回顾
首先我们请求能过到达服务,那么我们的服务肯定是要启动着的吧,是不是,后端服务都没启,网络连接都建立不起来,更别说服务了。所以我们要看看服务启动的过程中,Tomcat 的建立。在看之前我们先回忆下 Tomcat 的一个大致组件结构,这里我简单画了个图:
可以看到有这么几个概念哈:
Server:一个 Tomcat 实例就是一个 Server
Service:一个 Server里可以有一个或者多个 Service,每个 Service 里有一个 Container 和多个 Connector。
Connector:负责网络连接,底层就是socket来进行连接的(java中的网络通信是通过socket实现的 socket又分为普通的socket、NioSocket),还有我们的 request、response的创建等,封装完交给 Container 处理,处理完返回给 Connector,然后通过socket将结果返回给客户端。它具体用 protocolhandler 处理请求的,不同的protocolhandler代表不同的连接类型,http11protocol用的是普通socket来连接的,http11nioprotocol用的是niosocket来连接的。而 protocolhandler 有三个重要的组件:endpoint、processor、adapter,endpoint 用于处理底层socket的网络连接,processor 用于将endpoint接收的socket封装成request,adapter用于将封装好的request交给container进行具体处理各司其职。
Container :就是servlet容器,具体处理 Servlet,它内部有四个组件:engine、host、context、wrapper,engine是引擎管理多个站点,host表示一个站点 也叫虚拟主机 appbase站点位置 默认就是我们的webapps目录,context 就是我们webapps下的每个应用程序,wrapper 每个wrapper 封装着一个servlet。
了解完这几个概念后,我们看看 SpringBoot 启动的时候是不是这样的,细节、入口这里就不一点点看了,我就直接挑关键代码给大家展示了哈:
我们可以看到创建了 Tomcat 对象、一个连接器对象 Connector,那么 Service、Host 怎么创建的呢?我们继续看看:
先看 Service 的创建,Tomcat 对象第一次内部的 Server 为空,会先创建 Server 对象,然后再创建一个 Service 对象,并把它加入到了 Server 对象中:
那我们继续看看 Host:
这些创建我们看完捋一下,类之间的关系,大致有一个这么个模样:
插一个小细节:我们知道 onRefresh 的时候是创建 Web 容器,但当你看源码的时候,会发现在实例化TomcatWebServer 对象中就会调用 tomcat的 start 方法,这岂不是就启动了么?
哈哈哈,看启动前的那几行代码,也就是第69行,它会把 connector 连接器先给挪出来,因为连接器一旦启动了,就能接收到请求了,可是这个时候我们的 Spring 容器中有的 Bean 都还没创建完,还不能正常提供服务,所以这里把它挪出来。然后在 finishRefresh 的时候,会把连接器塞,并启动连接器:
另外关于 DispatchServlet 是放到哪个对象里了呢?可以看这个现代和传统的启动方式的区别,这篇最后会一步步说它是怎么注入到 Tomcat 的 ServletContext 中的哈。
3 连接器
3.1 基础认识
我们既然要网络通信,那就涉及到网络协议,Tomcat支持的多种I/O模型和应用层协议,具体包含哪些IO模型和应用层协议,参考如下:
传输层协议:
IO模型 | 描述 |
---|---|
NIO | 非阻塞I/O,采用Java NIO类库实现。 |
NIO2 | 异步I/O,采用JDK 7最新的NIO2类库实现。 |
APR | 采用Apache可移植运行库实现,是C/C++编写的本地库。如果选择该方案,需要单独安装APR库。 |
Tomcat 支持的IO模型:自8.5/9.0 版本起,Tomcat 移除了 对 BIO 的支持.
应用层协议:
应用层协议 | 描述 |
---|---|
HTTP/1.1 | Web应用采用的访问协议 |
AJP | 用于和Web服务器集成(如Apache),以实现对静态资源的优化以及集群部署,当前支持AJP/1.3。 |
HTTP/2 | HTTP 2.0大幅度的提升了Web性能。下一代HTTP协议 , 自8.5以及9.0版本之后支持。 |
在 Tomcat8.0 之前 ,Tomcat 默认采用的I/O方式为 BIO , 之后改为 NIO。 无论 NIO、NIO2还是 APR, 在性能方面均优于以往的BIO。
然后我们细细看一下连接器的组件结构:
(1)Endpoint:
- 通信端点,即通信监听接口
- 是具体的Socket请求的接收和发送处理器,是对传输层抽象,用来实现TCP/IP协议:
- Tomcat中并没有Endpoint接口,而是定义了一个抽象类AbstractEndpoint, 该抽象类定义了两个内部类:
- Acceptor:用于监听Socket连接请求
- SocketProcessor:用于处理接收到的Socket请求;实现Runnable接口,在Run方法中调用了协议处理组件Processor进行处理;为了提高处理能力 ,socketProcessor会被提交到线程池来执行,该线程池是Tomcat扩展原生的 Java线程池,被称作执行器Executor
(2)Processor:
- 协议处理接口,是对应用层协议的抽象
- Processor用来实现HTTP协议
- Processor接收来自Endpoint的Socket数据,读取字节流解析成HttpRequest对象
- 然后将HttpRequest通过Adapter转化成ServletRequest提交给容器处理
(3)ProtocolHandler:
- 协议接口,通过Endpoint和Processor实现针对具体协议的处理能力,默认使用的协议是Http11NioProtocol
(4)Adapter:
- 由于协议的不同,客户端传输的请求信息也会不同 ,Tomcat自定义一个Request类来存放这些请求信息
- ProcotolHandler接口负责解析请求并生成Request类,但是这个Request不是标准的ServletRequest, 所有不能使用Tomcat中自定义的Request作为参数来调用容器进行数据处理
- 通过运用适配器模式,引入CoyoteAdapter来解决这样的问题:连接器调用CoyoteAdapter的Service方法,传入Tomcat Request对象,然后CoyoteAdapter负责将Tomcat Request转化成ServletRequest, 最后再调用容器的Service方法
3.2 连接器的创建
我们再细细看看它的创建:
// Tomcat 工厂创建 public class TomcatServletWebServerFactory extends AbstractServletWebServerFactory implements ConfigurableTomcatWebServerFactory, ResourceLoaderAware { // 现在默认的协议 private String protocol = "org.apache.coyote.http11.Http11NioProtocol"; public WebServer getWebServer(ServletContextInitializer... initializers) { ... Tomcat tomcat = new Tomcat(); File baseDir = this.baseDirectory != null ? this.baseDirectory : this.createTempDir("tomcat"); tomcat.setBaseDir(baseDir.getAbsolutePath()); // 创建连接器 Connector connector = new Connector(this.protocol); ... }
继续看一下他的构造方法:
public Connector(String protocol) { // 是否是 apr 默认是 false boolean apr = AprStatus.isAprAvailable() && AprStatus.getUseAprConnector(); ProtocolHandler p = null; try { // 创建 handler p = ProtocolHandler.create(protocol, apr); } catch (Exception e) { log.error(sm.getString( "coyoteConnector.protocolHandlerInstantiationFailed"), e); } if (p != null) { protocolHandler = p; protocolHandlerClassName = protocolHandler.getClass().getName(); } else { protocolHandler = null; protocolHandlerClassName = protocol; } }
继续看下 Handler 的创建(主要就是根据协议名称、是否开启 apr 来选择创建不同的 Handler),默认情况下创建的是:new org.apache.coyote.http11.Http11NioProtocol()
public static ProtocolHandler create(String protocol, boolean apr) throws ClassNotFoundException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException { if (protocol == null || "HTTP/1.1".equals(protocol) || (!apr && org.apache.coyote.http11.Http11NioProtocol.class.getName().equals(protocol)) || (apr && org.apache.coyote.http11.Http11AprProtocol.class.getName().equals(protocol))) { if (apr) { return new org.apache.coyote.http11.Http11AprProtocol(); } else { return new org.apache.coyote.http11.Http11NioProtocol(); } } else if ("AJP/1.3".equals(protocol) || (!apr && org.apache.coyote.ajp.AjpNioProtocol.class.getName().equals(protocol)) || (apr && org.apache.coyote.ajp.AjpAprProtocol.class.getName().equals(protocol))) { if (apr) { return new org.apache.coyote.ajp.AjpAprProtocol(); } else { return new org.apache.coyote.ajp.AjpNioProtocol(); } } else { // Instantiate protocol handler Class<?> clazz = Class.forName(protocol); return (ProtocolHandler) clazz.getConstructor().newInstance(); } }
protocolHandler 依赖的 EndPoint:
public class Http11NioProtocol extends AbstractHttp11JsseProtocol<NioChannel> { private static final Log log = LogFactory.getLog(Http11NioProtocol.class); public Http11NioProtocol() { super(new NioEndpoint()); } }
3.3 连接器的启动
接下来,我们就看看连接器的启动:
// Connector 连接器 protected void startInternal() throws LifecycleException { // 端口小于0 直接报错 if (getPortWithOffset() < 0) { throw new LifecycleException(sm.getString( "coyoteConnector.invalidPort", Integer.valueOf(getPortWithOffset()))); } // 设置状态启动中 setState(LifecycleState.STARTING); try { // 内部的 protocolHandler 启动 protocolHandler.start(); } catch (Exception e) { throw new LifecycleException( sm.getString("coyoteConnector.protocolHandlerStartFailed"), e); } }
Http11NioProtocol 启动,由于它间接继承的是 AbstractProtocol,这里则调用的是 AbstractProtocol 的启动方法:
@Override public void start() throws Exception { ... // 内部的 endpoint 启动 这里的话也就是 NioEndPoint endpoint.start(); // endpoint 内部的创建固定间隔60秒的线程池,用来启动异步超时检查 monitorFuture = getUtilityExecutor().scheduleWithFixedDelay( new Runnable() { @Override public void run() { if (!isPaused()) { startAsyncTimeout(); } } }, 0, 60, TimeUnit.SECONDS); }
可以看到主要就两步,一个是将内部的 endpoint 启动,并且会启动一个间隔60s的任务,校验超时。那我们继续看看 endpoint 的启动:
// AbstractEndpoint // endpoint 的启动并绑定 public final void start() throws Exception { // 第一次的 endpoint 还未绑定 if (bindState == BindState.UNBOUND) { // 开始绑定 bindWithCleanup(); bindState = BindState.BOUND_ON_START; } startInternal(); }
可以看到主要做了两件事情,一个是如果还未绑定的话会进行绑定(比如监听哪个端口呀)然后调用模板方法启动。那我们先看一下绑定方法都做了哪些事情:
// AbstractEndpoint 绑定 private void bindWithCleanup() throws Exception { try { // 抽象方法 调用子类具体实现 bind(); } catch (Throwable t) { // Ensure open sockets etc. are cleaned up if something goes // wrong during bind ExceptionUtils.handleThrowable(t); unbind(); throw t; } } // NioEndpoint#bind /** * Initialize the endpoint. */ @Override public void bind() throws Exception { // 初始化并绑定通道 initServerSocket(); // 停止的计数器用于关闭 poller 线程 setStopLatch(new CountDownLatch(1)); // Initialize SSL if needed initialiseSsl(); // 初始化并打开共享的 Selector selectorPool.open(getName()); }
哇,这里其实就涉及到 NioSocket 的相关东西了,比如 Buffer、Channel、Selector,这个你要是一点儿都不知道的话,那看起来确实有点困难,可以参考我的这篇会讲 Java里边的两种 Socket哈,有个简单的认识,再继续往下看。
我们接着往里看看初始化通道:
protected void initServerSocket() throws Exception { if (!getUseInheritedChannel()) { serverSock = ServerSocketChannel.open(); socketProperties.setProperties(serverSock.socket()); InetSocketAddress addr = new InetSocketAddress(getAddress(), getPortWithOffset()); serverSock.socket().bind(addr,getAcceptCount()); } else { // Retrieve the channel provided by the OS Channel ic = System.inheritedChannel(); if (ic instanceof ServerSocketChannel) { serverSock = (ServerSocketChannel) ic; } if (serverSock == null) { throw new IllegalArgumentException(sm.getString("endpoint.init.bind.inherited")); } } serverSock.configureBlocking(true); //mimic APR behavior }
- getUseInheritedChannel() 方法默认为 false ,所以会走 if 分支。
- 在上面的 if 分支里, 会利用 java NIO 对象 ServerSocketChannel 创建 server socket ,绑定监听地址和端口,设置 socket 的 backlog 以及其他属性。
- 最后调用 serverSock.configureBlocking(true) 来设置监听的 socket 为阻塞 scoket ,即该 socket 在 accept 的时候,如果没有连接就使当前线程阻塞。
绑定看的差不多了,我们继续回到 endpoint的启动,看看模板的方法 startInternal 都做了哪些事情:
/** * Start the NIO endpoint, creating acceptor, poller threads. */ @Override public void startInternal() throws Exception { if (!running) { running = true; paused = false; if (socketProperties.getProcessorCache() != 0) { processorCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE, socketProperties.getProcessorCache()); } if (socketProperties.getEventCache() != 0) { eventCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE, socketProperties.getEventCache()); } if (socketProperties.getBufferPool() != 0) { nioChannels = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE, socketProperties.getBufferPool()); } // Create worker collection 初始化工作线程池 if (getExecutor() == null) { createExecutor(); } // 初始化控制连接数 initializeConnectionLatch(); // Start poller thread 又来了一个 poller poller = new Poller(); Thread pollerThread = new Thread(poller, getName() + "-ClientPoller"); pollerThread.setPriority(threadPriority); pollerThread.setDaemon(true); pollerThread.start(); // 启动 accept 用来接收请求 startAcceptorThread(); } }
兄弟们我发现这家伙不是一篇两篇就能写清楚的,这东西太多了,越往里看东西越多,还涉及到 Tomcat 的好几种线程,你看我们看到这里我就发现了两种 poller线程、还有这里的 accept 用于处理接收请求的线程,我这里就不往里深究了。分析一个大佬的一套文章,会详细讲解 Tomcat NIO的处理,大家想详细了解的话,可以看看。它是一套文章,可以从第一章开始看哈。
我这里就小小的引用一下:对于 tomcat NIO 来说,是由一系列框架类和数据读写类来组成的,同时这些类运行在不同的线程中,共同维持整个 tomcat NIO 架构。包括原始 socket 监听的acceptor 线程,监测注册在原始 scoket 上的事件是否发生的 poller thread 事件线程,进行数据读写和运行 servlet API 的 tomcat io 线程。当数据需要多次读写的时候,监测注册在原始 scoket 上的读写事件的 block poller 事件线程。这些类和线程共同组成的 tomcat NIO 整体结构如下所示:
上面我们可以发现整体架构运行着4种线程:
- Acceptor 线程
- Poller 线程
- Tomcat IO 线程
- BlockPoller 线程
Acceptor线程
- tomcat NIO 架构中会有一个 acceptor 线程,这个线程主要监听端口。当有请求过来的时候,完成 tcp 三次握手,将 accept 过来的 socket 注册OP_REGISTER 事件,并将该事件提交到 Poller 线程的事件队列 PollerEventQueue中 。
Poller线程
- 在 tomcat NIO 架构中会有 poller 线程,在 tomcat8 及以前的版本之中,可以通过 pollerThreadCount 配置 poller thread 的数目,但是在 tomcat 9.0.21 中 poller thread 数目始终会为 1。
- poller thread 对于每一个 poller 实例都有一个 NIO selector 实例,同时也有一个事件队列SynchronizedQueue<PollerEvent>。
- 对于每一个 poller thread 来说,都会去轮询队列 SynchronizedQueue<PollerEvent>,该队列的事件由 acceptor 线程(监听到新连接时)或者 tomcat io 线程(处理完请求之后保持长连接,添加读事件)放入,然后根据不同的事件对原始socket注册相应的读写事件。
- 每一个poller thread 来说,会调用 java NIO 对象 selector,发起系统调用,来监测原始 scoket 是否有读写事件发生,如果有则将原始 scoket 的封装对象交由 tomcat io 线程处理。
Tomcat IO线程
- tomcat io 线程是一个线程池,线程池大小可由 tomcat 相关参数配置。
- tomcat io 线程会接受 poller thread 传入的 scoket 封装对象后,依次调用 SocketProcessor/ConnectionHanlder(global instance)/Http11Processor/CoyoteAdapter,最后交由 servlet container 完成servlet API 的调用。
- 对于request header 的解析,request body 的获取,servlet API 的调用,response 数据的写入发送,这一系列过程都是在 tomcat io 线程中完成的。
- 对于request header,request body 有时候需要多次读取操作,这个时候 tomcat io 线程会将原始 socket 再次进行读事件的注册,并交由 BlockPoller 线程处理,然后在 readLatch (CountDownLatch) 对象上等待。
- 对于 response data 有时候不可写,比如原始 scoket 的发送缓冲区满了,这个时候 tomcat io 线程同样会将原始 socket 再次注册写事件,并交由 BlockPoller 线程处理,然后在 writeLatch 对象上等待。
BlockPoller线程
- tomcat NIO 架构中会有 block poller 线程,其核心功能由 BlockPoller 类来实现,BlockPoller 实例会有一个 NIO selector 实例,同时也会拥有一个自己的事件队列实例SynchronizedQueue<Runnable>。
- 对于BlockPoller thread来说, 会去轮询队列 SynchronizedQueue<Runnable>,该队列的对象(RunnableAdd类型)由 tomcat io 线程放入,然后根据不同的对象对原始 socket 注册相应的读写事件。
- 对于BlockPoller thread来说, 会调用 java NIO 对象 selector,发起系统调用,来监测原始 scoket 是否有读写事件发生。如果有,则将 readLatch 或者 writeLatch 对象上等待的 tomcat io 线程唤醒,然后 tomcat io 线程继续完成读写操作。
它的启动,我们就暂时看到这里,是不是有点枯燥,我们就真实的 debug 看一下一次请求的处理。
4 请求过程
我们发送请求,首先就是连接的建立。那我们直接在它这个 accpet 线程里边打个断点:
当我发送一个请求后,这就是我们请求的 socket :
endpoint.serverSocketAccept() 方法来获取已经完成 tcp 三次握手的socket 连接,如果没有发生异常,并且 endpoint 正在 running 状态,同时也没有暂停,那么该逻辑就会把针对新进入连接所创建的原始 java socket 对象交由 endpoint.setSocketOptions(socket) 方法去处理。
上面我们也看到socket 初始化过程,server 端的监听 socket 是被设置为阻塞 socket ,所以endpoint.serverSocketAccept() 方法在没有可用连接的时候 acceptor 线程是阻塞的。
- 上述逻辑会在 setSocketOptions() 方法里进行构造 SocketBufferHandler 对象,主要设定读写 buffer 大小,以及是否使用 DirectBuffer (默认使用)。
- 构造 NioChannel 对象,该对象封装了基于原始 socket 去进行构造的 SocketBufferHandler 对象。
- 构造 NioSocketWrapper 对象,该对象封装了上面构造的 NioChannel 对象和当前 NioEndpoint 对象。
- 调用 poller 的 register(channel, socketWrapper) 方法将上面构造的 NioChannel 对象和 NioSocketWrapper 对象注册到 poller 线程的事件队列里。
- 由poller 对象的 register() 方法分析可知,会注册 OP_REGISTER 类型的PollerEvent 到 poller 对象的事件队列里。
5 小结
铁子们,暂时看到这里哈,后面处理别的事情了,有点草草的收尾了,下节我再看看请求的处理。