How to start



In the SpringBoot boot process, the following code is invoked

private void createWebServer(a) {
    WebServer webServer = this.webServer;
    ServletContext servletContext = getServletContext();
    if (webServer == null && servletContext == null) {
        / / get to ServletWebServerFactory implementation class: TomcatServletWebServerFactory
        / / ServletWebServerFactoryConfiguration by automation configuration class ServletWebServerFactoryConfiguration load
        ServletWebServerFactory factory = getWebServerFactory();
        // the resulting webServer is TomcatWebServer. See below for details
        this.webServer = factory.getWebServer(getSelfInitializer());
        
        SmartLifecycle is essentially a SpringBoot hook that performs callbacks to start and stop methods when the IOC container is refreshed and closed
        getBeanFactory().registerSingleton("webServerGracefulShutdown".new WebServerGracefulShutdownLifecycle(this.webServer));

        getBeanFactory().registerSingleton("webServerStartStop".new WebServerStartStopLifecycle(this.this.webServer));
    }
    else if(servletContext ! =null) {
        try {
            getSelfInitializer().onStartup(servletContext);
        }
        catch (ServletException ex) {
            throw new ApplicationContextException("Cannot initialize servlet context", ex);
        }
    }
    initPropertySources();
}
Copy the code

Create TomcatWebServer through TomcatServletWebServerFactory factory

@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
    if (this.disableMBeanRegistry) {
        Registry.disableRegistry();
    }
    Tomcat tomcat = new Tomcat();
    // Create a temporary directory
    File baseDir = (this.baseDirectory ! =null)?this.baseDirectory : createTempDir("tomcat");
    tomcat.setBaseDir(baseDir.getAbsolutePath());
    
    // Create Connector, a core component that receives network requests and reads and writes data streams through the combination of the ProtocolHandler interface implementation class. By default, Http11NioProtocol is used
    Connector connector = new Connector(this.protocol);
    connector.setThrowOnFailure(true);
    // Add Connector to the Service component. This call will trigger StandardServer instantiation, along with the instantiation of StandardService.
    tomcat.getService().addConnector(connector);
    // Customize Connector, such as Max, min, etc
    customizeConnector(connector);
    tomcat.setConnector(connector);
    tomcat.getHost().setAutoDeploy(false);
    configureEngine(tomcat.getEngine());
    for (Connector additionalConnector : this.additionalTomcatConnectors) {
        tomcat.getService().addConnector(additionalConnector);
    }
    prepareContext(tomcat.getHost(), initializers);
    // Create a TomcatWebServer instance and initialize and start its contained child components. See below for details on relationships between subcomponents.
    return getTomcatWebServer(tomcat);
}
Copy the code

In the above code, the tomcat. start method is called to initialize and start Tomcat and its subcomponents as follows:





WebServerGracefulShutdownLifecycle as SmartLifecycle interface implementation class in the SpringBoot application startup process will be called back its start method, The purpose is to start the Connector for port monitoring and the thread for accessing the connection and processing network IO data.





Through the above two processes:

  • Example Initialize Tomcat and TomcatWebServer
  • The callback WebServerGracefulShutdownLifecycle

We can see TomcatWebServer as the topmost facade. Encapsulate Tomcat; Tomcat is an abstraction of StandardServer, StandardService, Connector, and ProtocolHandler (interfaces, interaction protocols, and IO modes) Implementation classes include Http11NioProtocol, Http11Nio2Protocol, Http11AprProtocol, AjpAprProtocol, etc. Http11NioProtocol is the default implementation. StandardServer and StandardService are parent-child components, and StandardServer holds instances of StandardService. And you can hold multiple services. StandardService and Connector are also parent-child components, with StandardService holding an instance of the Connector. And you can hold multiple connectors. (Only one Connector and ProtocolHandler in SpringBoot embedded Tomcat mode) The Connector and ProtocolHandler relationship is also parent-child. The Connector and ProtocolHandler relationship is one-to-one. As an abstraction of protocol interaction, the ProtocolHandler abstracts the interaction protocol and IO mode, exposing a common interface.

The ProtocolHandler has the ability to handle requests by combining the Endpoint interface implementation class. The Endpoint implementation classes include AprEndpoint, NioEndpoint, and Nio2Endpoint. By default, the ProtocolHandler implementation class used is Http11NioProtocol, which holds a NioEndpoint instance inside. By name, Http11NioProtocol is a connector that handles the HTTP1.1 protocol and performs IO operations in NIO mode. Combine Http11Processor to handle Http1.1 protocol and NioEndpoint to handle requests and data reads and writes. ** Above is the overall process of Tomcat initialization and startup when the application is started. Here are three suspensions:

  • BlockPoller
  • Poller
  • Acceptor

What are these three for?

How are requests received and processed

Review the NioEndpoint startup process.

// org.apache.tomcat.util.net.AbstractEndpoint#start
public final void start(a) throws Exception {
    if (bindState == BindState.UNBOUND) {
        bindWithCleanup();
        bindState = BindState.BOUND_ON_START;
    }
    startInternal();
}

// org.apache.tomcat.util.net.AbstractEndpoint#bindWithCleanup
private void bindWithCleanup(a) throws Exception {
    try {
        bind();
    } catch (Throwable t) {
        // Ensure open sockets etc. are cleaned up if something goes
        // wrong during bind
        ExceptionUtils.handleThrowable(t);
        unbind();
        throwt; }}// org.apache.tomcat.util.net.NioEndpoint#bind
@Override
public void bind(a) throws Exception {
    initServerSocket();

    setStopLatch(new CountDownLatch(1));

    // Initialize SSL if needed
    initialiseSsl();

    // NioSelectorPool
    selectorPool.open(getName());
}

// org.apache.tomcat.util.net.NioEndpoint#initServerSocket
protected void initServerSocket(a) throws Exception {
    if(! getUseInheritedChannel()) {// Java niO network programming related interface
        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
}

// org.apache.tomcat.util.net.NioSelectorPool#open
public void open(String name) throws IOException {
    enabled = true;
    getSharedSelector();
    if (shared) {
		// Start the BlockPoller thread
        blockingSelector = newNioBlockingSelector(); blockingSelector.open(name, getSharedSelector()); }}// org.apache.tomcat.util.net.NioEndpoint#startInternal
@Override
public void startInternal(a) throws Exception {

    if(! running) { running =true;
        paused = false;

        // Three object pool cache
        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 for? The thread pool
        // Create worker collection
        if (getExecutor() == null) {
            createExecutor();
        }

        // Limit the maximum number of connections
        initializeConnectionLatch();

        // Start the thread to handle PollerEvent events and IO events
        // Start poller thread
        poller = new Poller();
        Thread pollerThread = new Thread(poller, getName() + "-ClientPoller");
        pollerThread.setPriority(threadPriority);
        pollerThread.setDaemon(true);
        pollerThread.start();

        // Accept new connections. PollerEvent is sent to read and write IO data.startAcceptorThread(); }}Copy the code

As you can see from the above code, NioEndpoint starts with an Acceptor thread that accepts connections and a Poller thread that processes events. Here is how Tomcat receives requests:





After a request is encapsulated as a Runnable task and dropped into the thread pool, the execution flow is as follows:





Where CoyoteAdapter is an implementation of the adapter pattern that acts as a bridge between Connector and Servlet container (i.e. StandardEngine), decoupling and coupling between the two.



Now that you know the whole process of processing a request, let’s look at the three cliffhangers that remain:

  • BlockPoller
  • Poller
  • Acceptor

What are these three for? We already know that acceptors are the threads that receive new connections, and pollers are the threads that handle the read and write operations of network requests (that is, the Selector events and pollerEvents). As for BlockPoller, to understand what it does you need to look at the flow of IO writing out data. When writing out the data, the following code is eventually called, mainly lines 30-38

// org.apache.tomcat.util.net.NioBlockingSelector#write
public int write(ByteBuffer buf, NioChannel socket, long writeTimeout)
            throws IOException {
    SelectionKey key = socket.getIOChannel().keyFor(socket.getSocketWrapper().getPoller().getSelector());
    if (key == null) {
        throw new IOException(sm.getString("nioBlockingSelector.keyNotRegistered"));
    }
    KeyReference reference = keyReferenceStack.pop();
    if (reference == null) {
        reference = new KeyReference();
    }
    NioSocketWrapper att = (NioSocketWrapper) key.attachment();
    int written = 0;
    boolean timedout = false;
    int keycount = 1; //assume we can write
    long time = System.currentTimeMillis(); //start the timeout timer
    try {
        while(! timedout && buf.hasRemaining()) {if (keycount > 0) { //only write if we were registered for a write
                int cnt = socket.write(buf); //write the data
                if (cnt == -1) {
                    throw new EOFException();
                }
                written += cnt;
                if (cnt > 0) {
                    time = System.currentTimeMillis(); //reset our timeout timer
                    continue; //we successfully wrote, try again without a selector}}// If CNT < 0, the network jitter or the output buffer is full, so the thread cannot continue to output data out of the heap
            try {
                if (att.getWriteLatch() == null || att.getWriteLatch().getCount() == 0) {
                    att.startWriteLatch(1);
                }
                // Added to the queue of the BlockPoller to wake up the current thread when the IO becomes writable
                poller.add(att, SelectionKey.OP_WRITE, reference);
                // The thread waits for the selector writable event
                att.awaitWriteLatch(AbstractEndpoint.toTimeout(writeTimeout), TimeUnit.MILLISECONDS);
            } catch (InterruptedException ignore) {
                // Ignore
            }
            if(att.getWriteLatch() ! =null && att.getWriteLatch().getCount() > 0) {
                //we got interrupted, but we haven't received notification from the poller.
                keycount = 0;
            } else {
                //latch countdown has happened
                keycount = 1;
                att.resetWriteLatch();
            }

            if (writeTimeout > 0 && (keycount == 0)) { timedout = (System.currentTimeMillis() - time) >= writeTimeout; }}if (timedout) {
            throw newSocketTimeoutException(); }}finally {
        poller.remove(att, SelectionKey.OP_WRITE);
        if(timedout && reference.key ! =null) {
            poller.cancelKey(reference.key);
        }
        reference.key = null;
        keyReferenceStack.push(reference);
    }
    return written;
}
Copy the code

The BlockPoller thread wakes up the thread waiting on the write call (via CountDownLatch) by rotating the SAME Selector IO events that it detects, with the main logic in lines 46 to 62.

@Override
public void run(a) {
    while (run) {
        try {
            events();
            int keyCount = 0;
            try {
                int i = wakeupCounter.get();
                if (i > 0) {
                    keyCount = selector.selectNow();
                } else {
                    wakeupCounter.set(-1);
                    keyCount = selector.select(1000);
                }
                wakeupCounter.set(0);
                if(! run) {break; }}catch (NullPointerException x) {
                // sun bug 5076772 on windows JDK 1.5
                if (selector == null) {
                    throw x;
                }
                if (log.isDebugEnabled()) {
                    log.debug("Possibly encountered sun bug 5076772 on windows JDK 1.5", x);
                }
                continue;
            } catch (CancelledKeyException x) {
                // sun bug 5076772 on windows JDK 1.5
                if (log.isDebugEnabled()) {
                    log.debug("Possibly encountered sun bug 5076772 on windows JDK 1.5", x);
                }
                continue;
            } catch (Throwable x) {
                ExceptionUtils.handleThrowable(x);
                log.error(sm.getString("nioBlockingSelector.selectError"), x);
                continue;
            }

            Iterator<SelectionKey> iterator = keyCount > 0
                ? selector.selectedKeys().iterator()
                : null;

            // Walk through the collection of ready keys and dispatch
            // any active event.
            while(run && iterator ! =null && iterator.hasNext()) {
                SelectionKey sk = iterator.next();
                NioSocketWrapper socketWrapper = (NioSocketWrapper) sk.attachment();
                try {
                    iterator.remove();
                    The Selector event is ignored and countDownlatch's waiting thread handles the event
                    sk.interestOps(sk.interestOps() & (~sk.readyOps()));
                    if (sk.isReadable()) {
                        // Wake up the thread
                        countDown(socketWrapper.getReadLatch());
                    }
                    if (sk.isWritable()) {
                        // Wake up the threadcountDown(socketWrapper.getWriteLatch()); }}catch(CancelledKeyException ckx) { sk.cancel(); countDown(socketWrapper.getReadLatch()); countDown(socketWrapper.getWriteLatch()); }}}catch (Throwable t) {
            log.error(sm.getString("nioBlockingSelector.processingError"), t);
        }
    }
    events.clear();
    // If using a shared selector, the NioSelectorPool will also try and
    // close the selector. Try and avoid the ClosedSelectorException
    // although because multiple threads are involved there is always
    // the possibility of an Exception here.
    if (selector.isOpen()) {
        try {
            // Cancels all remaining keys
            selector.selectNow();
        } catch (Exception ignore) {
            if (log.isDebugEnabled())
                log.debug("", ignore); }}try {
        selector.close();
    } catch (Exception ignore) {
        if (log.isDebugEnabled())
            log.debug("", ignore); }}Copy the code

conclusion

Through the above analysis, we can see the overall architecture of Tomcat, in which the designed classes are mainly as follows:





And the thread model for processing requests is as follows