【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  小结

铁子们,暂时看到这里哈,后面处理别的事情了,有点草草的收尾了,下节我再看看请求的处理。

posted @ 2024-04-09 08:07  酷酷-  阅读(641)  评论(0编辑  收藏  举报