Review of the previous period

The last article “How to start Tomcat in SpringBoot” starts from the main method, snooping on how SpringBoot starts Tomcat, in the analysis of Tomcat we focus on, Tomcat mainly includes two components, Connector and Container and their internal structure diagram, so today we will analyze the Tomcat Connector design and what its role is.

Note: The Tomcat version of this article is 9.0.21. It is not recommended for zero-base readers.

Start with the Connector source code

Since we are going to parse the Connector, we will start with the source code directly. I will eliminate the unimportant parts of all the source code, so I will ignore most of the source details and only focus on the process. Source code as follows (high energy early warning, a lot of code) :

public class Connector extends LifecycleMBeanBase  {
    public Connector() {
        this("org.apache.coyote.http11.Http11NioProtocol");
    }


    public Connector(String protocol) {
        boolean aprConnector = AprLifecycleListener.isAprAvailable() &&
                AprLifecycleListener.getUseAprConnector();

        if ("HTTP / 1.1".equals(protocol) || protocol == null) {
            if (aprConnector) {
                protocolHandlerClassName = "org.apache.coyote.http11.Http11AprProtocol";
            } else {
                protocolHandlerClassName = "org.apache.coyote.http11.Http11NioProtocol"; }}else if ("AJP / 1.3".equals(protocol)) {
            if (aprConnector) {
                protocolHandlerClassName = "org.apache.coyote.ajp.AjpAprProtocol";
            } else {
                protocolHandlerClassName = "org.apache.coyote.ajp.AjpNioProtocol"; }}else{ protocolHandlerClassName = protocol; } // Instantiate protocol handler ProtocolHandler p = null; try { Class<? > clazz = Class.forName(protocolHandlerClassName); p = (ProtocolHandler) clazz.getConstructor().newInstance(); } catch (Exception e) { log.error(sm.getString("coyoteConnector.protocolHandlerInstantiationFailed"), e);
        } finally {
            this.protocolHandler = p;
        }

        // Default for Connector depends on this system property
        setThrowOnFailure(Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE"));
    }
    
  
Copy the code

This is a protocol-handling class, so an important submodule within the Connector is ProtocolHandler. This is a protocol-handling class.

About the life Cycle

See that Connector inherits LifecycleMBeanBase. Let’s look at the Connector’s final inheritance:

We see that the end implementation is going to be the Lifecycle interface and let’s see what that interface is. Let me take down the interface comment and explain

/**
 * Common interface for component life cycle methods.  Catalina components
 * may implement this interface (as well as the appropriate interface(s) for
 * the functionality they support) inorder to provide a consistent mechanism * to start and stop the component. * start() * ----------------------------- * | | | | * init () * NEW - » - the INITIALIZING * | | | | | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- "-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- * | | | auto | | | * | | \ | / start () \ | the \ | / auto auto stop () * | | | the INITIALIZED - » - STARTING_PREP - » - STARTING - » - STARTED -- » * | -- - | | | * | | | | destroy () | | | -- - | * » issue -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- "-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- ^ * | | | | * | | \ | / Auto auto start () * | | | STOPPING_PREP - » - STOPPING '-- -- -- -- -- -- -- -- -- -- -- STOPPED' -- -- -- -- -- -- -- -- -- - * | \ | / ^ | ^ * | | the stop () * | | | | | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - | | * | | | | | * | | | destroy (), destroy () | | * | | FAILED - » -- -- -- -- -- - DESTROYING the « -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - | * | | ^ | | * | | destroy () | | auto | | * -- -- -- -- -- -- -- -- » -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- \ | | | / * DESTROYED * | | | stop () | | * * '-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- » -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- * * Any state can transition to FAILED. * * Calling start()while a component is in states STARTING_PREP, STARTING or
 * STARTED has no effect.
 *
 * Calling start() while a component is in state NEW will cause init() to be
 * called immediately after the start() method is entered.
 *
 * Calling stop() while a component is in states STOPPING_PREP, STOPPING or
 * STOPPED has no effect.
 *
 * Calling stop() while a component is in state NEW transitions the component
 * to STOPPED. This is typically encountered when a component fails to start and
 * does not start all its sub-components. When the component is stopped, it will
 * try to stop all sub-components - even those it didn't start. * * Attempting any other transition will throw {@link LifecycleException}. * *  * The {@link LifecycleEvent}s fired during state changes are defined in the * methods that trigger the changed. No {@link LifecycleEvent}s are fired if the * attempted transition is not valid.Copy the code

This interface is provided for component declaration cycle management and provides a declaration cycle flow diagram. Here we just need to know the normal process:

New—>Init()—->Start()—->Stop()—>Destory()

Explore connectors from the life cycle

From the lifecycle description above, we know that the Connector is managed according to such a declaration cycle, so we have a clue, so the Connector must be initialized first and then started. We can see what the connector initialization does by looking at its initInternal() method:

    @Override
    protected void initInternal() throws LifecycleException {

        super.initInternal();

        if (protocolHandler == null) {
            throw new LifecycleException(
                    sm.getString("coyoteConnector.protocolHandlerInstantiationFailed"));
        }

        // Initialize adapter
        adapter = new CoyoteAdapter(this);
        protocolHandler.setAdapter(adapter);
        if(service ! = null) { protocolHandler.setUtilityExecutor(service.getServer().getUtilityExecutor()); } // Make sure parseBodyMethodsSet has a defaultif (null == parseBodyMethodsSet) {
            setParseBodyMethods(getParseBodyMethods());
        }

        if(protocolHandler.isAprRequired() && ! AprLifecycleListener.isInstanceCreated()) { throw new LifecycleException(sm.getString("coyoteConnector.protocolHandlerNoAprListener",
                    getProtocolHandlerClassName()));
        }
        if(protocolHandler.isAprRequired() && ! AprLifecycleListener.isAprAvailable()) { throw new LifecycleException(sm.getString("coyoteConnector.protocolHandlerNoAprLibrary",
                    getProtocolHandlerClassName()));
        }
        if(AprLifecycleListener.isAprAvailable() && AprLifecycleListener.getUseOpenSSL() && protocolHandler instanceof AbstractHttp11JsseProtocol) { AbstractHttp11JsseProtocol<? > jsseProtocolHandler = (AbstractHttp11JsseProtocol<? >) protocolHandler;if (jsseProtocolHandler.isSSLEnabled() &&
                    jsseProtocolHandler.getSslImplementationName() == null) {
                // OpenSSL is compatible with the JSSE configuration, so use it if APR is available
                jsseProtocolHandler.setSslImplementationName(OpenSSLImplementation.class.getName());
            }
        }

        try {
            protocolHandler.init();
        } catch (Exception e) {
            throw new LifecycleException(
                    sm.getString("coyoteConnector.protocolHandlerInitializationFailed"), e); }}}Copy the code

The protocolHandler is used to initialize the protocolHandler, and the protocolHandler is used to set up an adapter.

   /**
     * The adapter, used to call the connector.
     *
     * @param adapter The adapter to associate
     */
    public void setAdapter(Adapter adapter);
Copy the code

This comment makes it clear that the adapter is used to call the connector. Let’s continue with the protocolHandler initialization method

/** * Endpoint that provides low-level network I/O - must be matched to the * ProtocolHandler implementation (ProtocolHandler using NIO, requires NIO * Endpoint etc.). */ private final AbstractEndpoint<S,? > endpoint; public void init() throws Exception {if (getLog().isInfoEnabled()) {
            getLog().info(sm.getString("abstractProtocolHandler.init", getName()));
            logPortOffset();
        }

        if (oname == null) {
            // Component not pre-registered so register it
            oname = createObjectName();
            if (oname != null) {
                Registry.getRegistry(null, null).registerComponent(this, oname, null);
            }
        }

        if(this.domain ! = null) { rgOname = new ObjectName(domain +":type=GlobalRequestProcessor,name=" + getName());
            Registry.getRegistry(null, null).registerComponent(
                    getHandler().getGlobal(), rgOname, null);
        }

        String endpointName = getName();
        endpoint.setName(endpointName.substring(1, endpointName.length()-1));
        endpoint.setDomain(domain);

        endpoint.init();
    }
Copy the code

There is a new object, endpoint, which we can see from the comments is used to process network IO and must match the specified subclass (for example, Nio, which is NioEndPoint processing). Endpoint.init () actually does some network configuration, and then initialization is done. If init() is followed by start(), see Connector start().

 protected void startInternal() throws LifecycleException {

        // Validate settings before starting
        if (getPortWithOffset() < 0) {
            throw new LifecycleException(sm.getString(
                    "coyoteConnector.invalidPort", Integer.valueOf(getPortWithOffset())));
        }

        setState(LifecycleState.STARTING);

        try {
            protocolHandler.start();
        } catch (Exception e) {
            throw new LifecycleException(
                    sm.getString("coyoteConnector.protocolHandlerStartFailed"), e); }}Copy the code

This is done by calling the protocolHandler.start() method and continuing the trace. For the sake of presentation, I’ll put the following code together:

/ / 1. Categories: AbstractProtocol implements ProtocolHandler, MBeanRegistration public void start() throws Exception {// omits part of the code endpoint.start(); } //2. Class: AbstractEndPoint public final void start() throws Exception {// omits part of the code startInternal(); } / * * 3 categories: NioEndPoint extends AbstractJsseEndpoint<NioChannel,SocketChannel> * Start the NIO endpoint, creating acceptor, */ @override public void startInternal() throws Exception {// Omit some code. // Start POLler thread Poller = new  Poller(); Thread pollerThread = new Thread(poller, getName() +"-ClientPoller");
            pollerThread.setPriority(threadPriority);
            pollerThread.setDaemon(true); pollerThread.start(); startAcceptorThread(); }}Copy the code

At this point, the startup code is complete, and we see that at the end, we create a Poller on the NioEndPoint and start it up. Here, I just want to add that the NioEndPoint is listed. Tomcat provides three main implementations. AprEndPoint, NioEndPoint, Nio2EndPoint, here said the I/O model of tomcat support:

APR: Implemented by Apache portable runtime library, it rewrites most IO and system thread operation modules in C respectively according to different operating systems, and is said to have better performance than other modes (not tested).

NIO: non-blocking I/O

NIO.2: asynchronous I/O

Accept Acceptor (Poller, Acceptor, Acceptor, Acceptor);

//4. Class: Acceptor<U> implements Runnable public voidrun() {// omit some code U socket = null; socket = endpoint.serverSocketAccept(); // Configure the socketif(endpoint.isRunning() && ! endpoint.isPaused()) { //setSocketOptions() will hand the socket off to
                    // an appropriate processor ifSuccessful // Core logicif (!endpoint.setSocketOptions(socket)) {
                        endpoint.closeSocket(socket);
                    }
                } else{ endpoint.destroySocket(socket); } state = AcceptorState.ENDED; } // class: NioEndpoint protected BooleansetSocketOptions(SocketChannel socket) {// Process the connection try {// Disable blocking, polling will be used socket.configureBlocking(false); Socket sock = socket.socket(); socketProperties.setProperties(sock); NioSocketWrapper socketWrapper = new NioSocketWrapper(channel, this); channel.setSocketWrapper(socketWrapper); socketWrapper.setReadTimeout(getConnectionTimeout()); socketWrapper.setWriteTimeout(getConnectionTimeout()); socketWrapper.setKeepAliveLeft(NioEndpoint.this.getMaxKeepAliveRequests()); socketWrapper.setSecure(isSSLEnabled()); Poller. Register (channel, socketWrapper);return true;
  
    }
Copy the code

Acceptors accept sockets and register them with pollers.

Class NioEndpoint * Registers a newly created socket with the poller. ** @param socket the newly created socket * @param socketWrapper The socket wrapper */ public void register(final NioChannel socket, final NioSocketWrapper socketWrapper) { socketWrapper.interestOps(SelectionKey.OP_READ); //this is what OP_REGISTER turns into. PollerEvent r = null;if(eventCache ! = null) { r = eventCache.pop(); }if (r == null) {
                r = new PollerEvent(socket, OP_REGISTER);
            } else{ r.reset(socket, OP_REGISTER); } addEvent(r); PollerEvent implements Runnable public void PollerEvent implements Runnable public void PollerEvent implements Runnable public void PollerEvent implements Runnable public void PollerEvent implements Runnable public voidrunSocket.getiochannel ().register(socket.getsocketwrapper ().getPoller().getselector (), selectionKey.op_read, socket.getSocketWrapper()); }Copy the code

It turns out that the NIO model is ultimately used to register it in the channel. (This involves NIO network programming knowledge, do not know the students can be transmitted here). So after the registration, let’s see what Poller does.

Class: Poller implements Runnable **/ @override public voidrun() {
            // Loop until destroy() is called
            while (trueIterator<SelectionKey> Iterator = keyCount > 0? selector.selectedKeys().iterator() : null; // Walk through the collection of ready keys and dispatch // any active event.while(iterator ! = null && iterator.hasNext()) { SelectionKey sk = iterator.next(); NioSocketWrapper socketWrapper = (NioSocketWrapper) sk.attachment(); // Attachment may be nullif another thread has called
                    // cancelledKey()
                    if (socketWrapper == null) {
                        iterator.remove();
                    } else{ iterator.remove(); //sock handles processKey(sk, socketWrapper); }} // omit some code}Copy the code

So this is a selector that pulls out the previously registered event, and that completes the call.

/ / 9. Categories: Poller implements Runnable protected void processKey(SelectionKey SK, ProcessSocket (socketWrapper, socketevent.open_write, socketWrapper) {// omit most of the codetrue} //10. AbstractEndPoint public boolean processSocket(SocketWrapperBase<S> socketWrapper, SocketEvent event, Boolean dispatch) {// omit part of code Executor Executor = getExecutor();if(dispatch && executor ! = null) { executor.execute(sc); }else {
               sc.run();
           }
      
       return true; } //11. Class: SocketProcessorBase implements Runnable public final voidrun() {
       synchronized (socketWrapper) {
           // It is possible that processing may be triggered for read and
           // write at the same time. The sync above makes sure that processing
           // does not occur in parallel. The test below ensures that if the
           // first event to be processed results in the socket being closed,
           // the subsequent events are not processed.
           if (socketWrapper.isClosed()) {
               return;
           }
           doRun(); NioEndPoint extends AbstractJsseEndpoint<NioChannel,SocketChannel> protected voiddoRun() {// omit some codeif (handshake == 0) {
                   SocketState state = SocketState.OPEN;
                   // Process the request from this socket
                   if (event == null) {
                       state = getHandler().process(socketWrapper, SocketEvent.OPEN_READ);
                   } else {
                       state = getHandler().process(socketWrapper, event);
                   }
                   if(state == SocketState.CLOSED) { poller.cancelledKey(key, socketWrapper); }}}Copy the code

Poller calls the run method or uses the Executor thread pool to execute run(), which ends up calling doRun() in each child EndPoint and fetching a Handler to handle socketWrapper. Continue to look at the source code:

/ / class: AbstractProtocol internal class ConnectionHandler implements AbstractEndpoint.Handler<S> public SocketState Process (SocketWrapperBase<S> wrapper, SocketEvent status) {state = processor.process(wrapper, status);returnSocketState.CLOSED; } / / class: 14. AbstractProcessorLight implements Processor public SocketState process (SocketWrapperBase <? > socketWrapper, SocketEvent status) throws IOException {// omits the code state = service(socketWrapper);return state;
    }
Copy the code

The process is called through an implementation class of the Processor interface, which is also called into subclasses. The Processor is actually handling the application protocol, so we can look at the AbstractProcessorLight implementation class. AjpProcessor, Http11Processor, and StreamProcessor respectively represent that Tomcat supports three application layer protocols, which are as follows:

  • AJP protocol
  • HTTP. 1 protocol
  • HTTP2.0 agreement

Here we use the commonly used HTTP1.1 as an example, continue to look at the source:

Http11Processor extends AbstractProcessor public SocketState service(SocketWrapperBase<? > socketWrapper) throws IOException {// omits most code getAdapter().service(request, response); // omit most of the code} // class: 16 CoyoteAdapter implements Adapter public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) throws Exception { Request request = (Request) req.getNote(ADAPTER_NOTES); Response response = (Response) res.getNote(ADAPTER_NOTES); postParseSuccess = postParseRequest(req, request, res, response);if (postParseSuccess) {
                //check valves ifwe support async request.setAsyncSupported( connector.getService().getContainer().getPipeline().isAsyncSupported()); // Calling the container connector.getService().getContainer().getPipeline().getFirst().invoke( request, response); }}Copy the code

Here we see that the protocol handler will eventually call the CoyoteAdapter, and the adapter’s job is to convert the Request and Response objects into HttpServletRequest and HttpServletResponse so that it can call the container, At this point we have analyzed the flow and role of the connector.

summary

So to recall the process, I drew a sequence diagram to illustrate:

This diagram contains two processes, one for the initialization of the component and one for the invocation. The Connector primarily initializes two components, ProtcoHandler and the EndPoint, but we can see from the code structure that the two components are parent-child, that is, ProtcoHandler contains the EndPoint. To sum up, acceptors accept requests, register with Poller, Poller processes requests, and invoke the Processor to process them. Finally, the request is converted into a servlet-compliant request and response to invoke the Container.

Now that we have sorted out the process, let’s conduct a structured sorting:

The Connector contains only two submodules: the ProtocolHandler and the Adapter. The Connector contains only two submodules: the ProtocolHandler and the Adapter. Subsequent endpoints, acceptors, pollers, and processors are submodules of ProtocolHandler. The Acceptor and Poller modules implement core functions in the EndPoint. Therefore, they are submodules of the Acceptor and Poller modules, while Processor and EndPoint are independent of each other. Therefore, they are submodules of the same level as Processor.

Let’s use the graph to illustrate the above relationship:

As you can see from the figure above, the connector handles connection requests and then invokes the container through the adapter. Then the detailed process can be as follows:

  • AcceptorListen for network requests and get requests.
  • PollerGets the listening request submission thread pool for processing.
  • ProcessorGenerate Tomcat Request objects based on the specific application protocol (HTTP/AJP).
  • AdapterTo convert the Request object into a Servlet standard Request object, call the container.

conclusion

We from the connector source, step by step analysis, analysis of the connector mainly contains two modules, ProtocolHandler and Adapter. The ProtocolHandler consists of the Endpoint module and Processor module. The Endpoint module is mainly used for connection processing. It entrusts the Acceptor submodule to monitor and register the connection, and entrusts the Poller submodule to handle the connection. On the other hand, the Processor module processes application protocols and finally submits them to Adapter for object conversion so that containers can be called. In addition, we also added some additional knowledge points in the process of analyzing the source code:

  • The current Tomcat version supports THE FOLLOWING IO models: APR, NIO, and NIO.2
  • Tomcat supports AJP and HTTP, of which HTTP is divided into HTTP1.1 and HTTP2.0

Copyright notice: Original article, reprint please indicate the source.