写点什么

【Tomcat 源码分析】从零开始理解 HTTP 请求处理 (第一篇)

作者:派大星
  • 2024-09-20
    辽宁
  • 本文字数:11598 字

    阅读完需:约 38 分钟

前言

终于步入 Connector 的解析阶段,这无疑是 Tomcat 架构中最为复杂的一环。作为连接器,它的职责显而易见——连接。那么,它连接的究竟是什么呢?

Connector 宛如一座桥梁,将来自客户端的请求,经过精心封装成 RequestResponse 对象,传递给 Container 进行处理。Container 完成业务逻辑后,Connector 再将处理后的结果,通过 Response 对象返回给远方的客户端。

要深入理解 Connector 的精髓,需要我们从四个关键问题出发,逐一探索。

  1. Connector 如何接收来自远方的请求?

  2. 如何将这呼唤化作 Request 和 Response 的身影?

  3. 封装后的 Request 和 Response 如何被递交给 Container 处理?

  4. Container 处理完毕后,如何将结果托付给 Connector,并最终送回客户端手中?

为了更好地理解 Connector 的内部运作,让我们先来欣赏一幅 Connector 结构图,它将帮助我们更直观地感受其内部的精妙设计。

【注意】:不同的协议和通信方式,将催生出不同的 ProtocolHandler 实现。在 Tomcat 8.5 版本中,ProtocolHandler 的类继承关系图谱如下:

针对这幅类继承层级图,我们可以做如下解读:

ajp 和 http11 代表着两种截然不同的协议,而 nio、nio2 和 apr 则分别代表着三种不同的通信方式。值得注意的是,协议与通信方式并非相互独立,它们可以灵活组合,以适应不同的场景需求。

ProtocolHandler 内部,包含着三个核心部件:Endpoint、Processor 和 Adapter,它们共同协作,完成请求的接收、处理和响应。

  • Endpoint 负责处理底层的 Socket 网络连接,它就像是一位网络守卫,负责迎接来自网络的呼唤,并将其转化为可供处理的 Socket 连接。Processor 则肩负着将 Endpoint 接收到的 Socket 封装成 Request 对象的重任,它就像一位翻译官,将网络语言转化为服务器可以理解的语言。Adapter 则充当着连接器,它将 Request 对象传递给 Container,以便 Container 进行具体的处理。

  • 由于 Endpoint 负责处理底层的 Socket 网络连接,因此它需要实现 TCP/IP 协议,而 Processor 则需要实现 HTTP 协议,以解析 HTTP 请求。Adapter 则将请求适配到 Servlet 容器,使其能够理解并处理来自外部的请求。

  • Endpoint 的抽象实现类 AbstractEndpoint 定义了 Acceptor、AsyncTimeout 两个内部类和一个 Handler 接口。Acceptor 负责监听来自网络的请求,一旦有新的请求到来,便会将其捕获。AsyncTimeout 则负责检查异步 Request 的超时,确保请求在合理的时间内得到处理。Handler 则负责处理接收到的 Socket,它将调用 Processor 进行处理,将 Socket 转换为 Request 对象,并最终传递给 Container。

至此,我们已经解开了 Connector 如何接收请求、如何将请求封装成 Request 和 Response,以及封装后的 Request 和 Response 如何被传递给 Container 进行处理这三个关键问题。而对于最后一个问题,即 Container 处理完后如何将结果返回给客户端,我们将在深入了解 Container 的运作机制后自然明了,前面章节已对此进行了详细的分析。

Connector 源码分析入口

在 Service 的标准实现 StandardService 的源码中,我们发现其 init()、start()、stop() 和 destroy() 方法分别会对 Connectors 的同名方法进行调用。值得注意的是,一个 Service 通常会对应多个 Connector,这意味着 Service 的生命周期管理会影响到所有与其关联的 Connector。

Service.initInternal()

@Overrideprotected void initInternal() throws LifecycleException {    super.initInternal();
    if (engine != null) {        engine.init();    }
    // Initialize any Executors    for (Executor executor : findExecutors()) {        if (executor instanceof JmxEnabled) {            ((JmxEnabled) executor).setDomain(getDomain());        }        executor.init();    }
    // Initialize mapper listener    mapperListener.init();
    // Initialize our defined Connectors    synchronized (connectorsLock) {        for (Connector connector : connectors) {            try {                connector.init();            } catch (Exception e) {                String message = sm.getString(                        "standardService.connector.initFailed", connector);                log.error(message, e);
                if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE"))                    throw new LifecycleException(message);            }        }    }}
复制代码

Service.startInternal()

@Overrideprotected void startInternal() throws LifecycleException {    if(log.isInfoEnabled())        log.info(sm.getString("standardService.start.name", this.name));    setState(LifecycleState.STARTING);
    // Start our defined Container first    if (engine != null) {        synchronized (engine) {            engine.start();        }    }
    synchronized (executors) {        for (Executor executor: executors) {            executor.start();        }    }
    mapperListener.start();
    // Start our defined Connectors second    synchronized (connectorsLock) {        for (Connector connector: connectors) {            try {                // If it has already failed, don't try and start it                if (connector.getState() != LifecycleState.FAILED) {                    connector.start();                }            } catch (Exception e) {                log.error(sm.getString(                        "standardService.connector.startFailed",                        connector), e);            }        }    }}
复制代码

正如我们所知,Connector 实现了 Lifecycle 接口,这使得它成为一个拥有生命周期的组件。因此,Connector 的启动逻辑入口自然而然地落在 init() 和 start() 方法之中。

Connector 构造方法

在深入分析 Connector 的启动逻辑之前,不妨先来观摩一下 server.xml 文件。这份文件如同 Tomcat 架构的蓝图,清晰地展现了各个组件之间的联系和布局,为我们理解 Connector 的运作提供了一个宏观的视角。

<?xml version='1.0' encoding='utf-8'?><Server port="8005" shutdown="SHUTDOWN">  <Listener className="org.apache.catalina.startup.VersionLoggerListener" />  <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />  <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />  <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />  <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />
  <GlobalNamingResources>    <Resource name="UserDatabase" auth="Container"              type="org.apache.catalina.UserDatabase"              description="User database that can be updated and saved"              factory="org.apache.catalina.users.MemoryUserDatabaseFactory"              pathname="conf/tomcat-users.xml" />  </GlobalNamingResources>
  <Service name="Catalina">    <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />    <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
    <Engine name="Catalina" defaultHost="localhost">      <Realm className="org.apache.catalina.realm.LockOutRealm">        <Realm className="org.apache.catalina.realm.UserDatabaseRealm"               resourceName="UserDatabase"/>      </Realm>
      <Host name="localhost"  appBase="webapps"            unpackWARs="true" autoDeploy="true">        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"               prefix="localhost_access_log" suffix=".txt"               pattern="%h %l %u %t "%r" %s %b" />      </Host>    </Engine>  </Service></Server>
复制代码

在 server.xml 文件中,我们发现 Connector 拥有多个关键属性,其中 portprotocol 尤为重要。默认情况下,server.xml 支持两种协议:HTTP/1.1 和 AJP/1.3。HTTP/1.1 用于支持传统的 HTTP 1.1 协议,而 AJP/1.3 则专门用于支持与 Apache 服务器的通信,为 Apache 服务器提供一个与 Tomcat 交互的桥梁。

现在,让我们将目光转向 Connector 的构造方法:

public Connector() {    this(null); // 1. 无参构造方法,传入参数为空协议,会默认使用`HTTP/1.1`}
public Connector(String protocol) {    setProtocol(protocol);    // Instantiate protocol handler    // 5. 使用protocolHandler的类名构造ProtocolHandler的实例    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;    }
    if (Globals.STRICT_SERVLET_COMPLIANCE) {        uriCharset = StandardCharsets.ISO_8859_1;    } else {        uriCharset = StandardCharsets.UTF_8;    }}
@Deprecatedpublic void setProtocol(String protocol) {    boolean aprConnector = AprLifecycleListener.isAprAvailable() &&            AprLifecycleListener.getUseAprConnector();
    // 2. `HTTP/1.1`或`null`,protocolHandler使用`org.apache.coyote.http11.Http11NioProtocol`,不考虑apr    if ("HTTP/1.1".equals(protocol) || protocol == null) {        if (aprConnector) {            setProtocolHandlerClassName("org.apache.coyote.http11.Http11AprProtocol");        } else {            setProtocolHandlerClassName("org.apache.coyote.http11.Http11NioProtocol");        }    }    // 3. `AJP/1.3`,protocolHandler使用`org.apache.coyote.ajp.AjpNioProtocol`,不考虑apr    else if ("AJP/1.3".equals(protocol)) {        if (aprConnector) {            setProtocolHandlerClassName("org.apache.coyote.ajp.AjpAprProtocol");        } else {            setProtocolHandlerClassName("org.apache.coyote.ajp.AjpNioProtocol");        }    }    // 4. 其他情况,使用传入的protocol作为protocolHandler的类名    else {        setProtocolHandlerClassName(protocol);    }}
复制代码

在 Connector 的构造方法中,我们发现它主要完成了以下几项工作:

  • 当传入的参数为空协议时,它会默认使用 HTTP/1.1 协议。

  • 当传入的协议为 HTTP/1.1 或 null 时,它会选择 org.apache.coyote.http11.Http11NioProtocol 作为 ProtocolHandler,并忽略 apr 选项。

  • 当传入的协议为 AJP/1.3 时,它会选择 org.apache.coyote.ajp.AjpNioProtocol 作为 ProtocolHandler,同样忽略 apr 选项。

  • 对于其他情况,它会直接使用传入的 protocol 作为 ProtocolHandler 的类名。

  • 最后,它会使用 ProtocolHandler 的类名来构造 ProtocolHandler 的实例。

Connector.initInternal()

@Overrideprotected void initInternal() throws LifecycleException {    super.initInternal();
    // Initialize adapter    // 1. 初始化adapter    adapter = new CoyoteAdapter(this);    protocolHandler.setAdapter(adapter);
    // Make sure parseBodyMethodsSet has a default    // 2. 设置接受body的method列表,默认为POST    if (null == parseBodyMethodsSet) {        setParseBodyMethods(getParseBodyMethods());    }
    if (protocolHandler.isAprRequired() && !AprLifecycleListener.isAprAvailable()) {        throw new LifecycleException(sm.getString("coyoteConnector.protocolHandlerNoApr",                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());        }    }
    // 3. 初始化protocolHandler    try {        protocolHandler.init();    } catch (Exception e) {        throw new LifecycleException(                sm.getString("coyoteConnector.protocolHandlerInitializationFailed"), e);    }}
复制代码

Connector 的 init() 方法主要完成了三项重要的初始化工作:

  • 初始化 adapter:Adapter 负责将请求传递给 Container,因此需要在 init() 方法中完成初始化,以便后续能够正常地将请求传递给 Container 进行处理。

  • 设置接受 body 的 method 列表:默认情况下,Connector 只允许 POST 方法提交 body 数据,但在某些情况下,可能需要允许其他方法提交 body 数据,因此需要在 init() 方法中设置允许提交 body 的方法列表。

  • 初始化 protocolHandler:ProtocolHandler 是 Connector 的核心组件,负责处理请求和响应,因此需要在 init() 方法中完成 protocolHandler 的初始化,以便后续能够正常地处理请求和响应。

ProtocolHandler 的类继承层级关系图 中,我们可以看到 ProtocolHandler 的子类都必须实现 AbstractProtocol 抽象类。而 protocolHandler.init(); 方法的具体实现则取决于具体的 ProtocolHandler 子类,它会根据不同的协议和通信方式进行相应的初始化操作。

代码正是在这个抽象类里面。我们来分析一下。

@Overridepublic void init() throws Exception {    if (getLog().isInfoEnabled()) {        getLog().info(sm.getString("abstractProtocolHandler.init", getName()));    }
    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);    }
    // 1. 设置endpoint的名字,默认为:http-nio-{port}    String endpointName = getName();    endpoint.setName(endpointName.substring(1, endpointName.length()-1));    endpoint.setDomain(domain);
    // 2. 初始化endpoint    endpoint.init();}
复制代码

接下来,让我们一同探究 Endpoint.init() 方法的内部。它位于 AbstractEndpoint 抽象类中,采用模板方法模式,巧妙地将核心逻辑委托给子类的 bind() 方法。

public abstract void bind() throws Exception;public abstract void unbind() throws Exception;public abstract void startInternal() throws Exception;public abstract void stopInternal() throws Exception;
public void init() throws Exception {    // 执行bind()方法    if (bindOnInit) {        bind();        bindState = BindState.BOUND_ON_INIT;    }    if (this.domain != null) {        // Register endpoint (as ThreadPool - historical name)        oname = new ObjectName(domain + ":type=ThreadPool,name=\"" + getName() + "\"");        Registry.getRegistry(null, null).registerComponent(this, oname, null);
        ObjectName socketPropertiesOname = new ObjectName(domain +                ":type=ThreadPool,name=\"" + getName() + "\",subType=SocketProperties");        socketProperties.setObjectName(socketPropertiesOname);        Registry.getRegistry(null, null).registerComponent(socketProperties, socketPropertiesOname, null);
        for (SSLHostConfig sslHostConfig : findSslHostConfigs()) {            registerJmx(sslHostConfig);        }    }}
复制代码

继续追寻着代码的踪迹,我们终于来到了 bind() 方法,它揭示了 Connector 初始化的精髓所在。关键的代码片段 serverSock.socket().bind(addr, getAcceptCount());用于 将 ServerSocket 绑定到指定的 IP 地址和端口

@Overridepublic void bind() throws Exception {
    if (!getUseInheritedChannel()) {        serverSock = ServerSocketChannel.open();        socketProperties.setProperties(serverSock.socket());        InetSocketAddress addr = (getAddress()!=null?new InetSocketAddress(getAddress(),getPort()):new InetSocketAddress(getPort()));        //绑定ServerSocket到指定的IP和端口        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
    // Initialize thread count defaults for acceptor, poller    if (acceptorThreadCount == 0) {        // FIXME: Doesn't seem to work that well with multiple accept threads        acceptorThreadCount = 1;    }    if (pollerThreadCount <= 0) {        //minimum one poller thread        pollerThreadCount = 1;    }    setStopLatch(new CountDownLatch(pollerThreadCount));
    // Initialize SSL if needed    initialiseSsl();
    selectorPool.open();}
复制代码

至此,我们已将 Connector 的 init() 方法剖析完毕,接下来,让我们将目光转向 start() 方法。start() 方法的核心逻辑,仅仅是简洁的一行代码:调用 ProtocolHandler.start() 方法,将 Connector 的启动大任委托给 ProtocolHandler。

Connector.startInternal()

@Overrideprotected void startInternal() throws LifecycleException {
    // Validate settings before starting    if (getPort() < 0) {        throw new LifecycleException(sm.getString(                "coyoteConnector.invalidPort", Integer.valueOf(getPort())));    }
    setState(LifecycleState.STARTING);
    try {        protocolHandler.start();    } catch (Exception e) {        throw new LifecycleException(                sm.getString("coyoteConnector.protocolHandlerStartFailed"), e);    }}
复制代码

现在,让我们深入 ProtocolHandler.start() 方法,探索启动过程中的关键步骤。它首先会调用 Endpoint.start() 方法,启动 Endpoint,以便监听来自网络的请求。接着,它会开启异步超时线程,负责监控异步请求的超时情况。该线程的执行单元为 AsyncTimeout

@Overridepublic void start() throws Exception {    if (getLog().isInfoEnabled()) {        getLog().info(sm.getString("abstractProtocolHandler.start", getName()));    }
    // 1. 调用`Endpoint.start()`方法    endpoint.start();
    // Start async timeout thread    // 2. 开启异步超时线程,线程执行单元为`Asynctimeout`    asyncTimeout = new AsyncTimeout();    Thread timeoutThread = new Thread(asyncTimeout, getNameInternal() + "-AsyncTimeout");    int priority = endpoint.getThreadPriority();    if (priority < Thread.MIN_PRIORITY || priority > Thread.MAX_PRIORITY) {        priority = Thread.NORM_PRIORITY;    }    timeoutThread.setPriority(priority);    timeoutThread.setDaemon(true);    timeoutThread.start();}
复制代码

现在,我们将注意力集中在 Endpoint.start() 方法,它负责启动 Endpoint,为 Connector 迎接来自网络的请求做好准备。

public final void start() throws Exception {    // 1. `bind()`已经在`init()`中分析过了    if (bindState == BindState.UNBOUND) {        bind();        bindState = BindState.BOUND_ON_START;    }    startInternal();}
@Overridepublic void startInternal() throws Exception {    if (!running) {        running = true;        paused = false;
        processorCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,                socketProperties.getProcessorCache());        eventCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,                        socketProperties.getEventCache());        nioChannels = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,                socketProperties.getBufferPool());
        // Create worker collection        // 2. 创建工作者线程池        if ( getExecutor() == null ) {            createExecutor();        }
        // 3. 初始化连接latch,用于限制请求的并发量        initializeConnectionLatch();
        // Start poller threads        // 4. 开启poller线程。poller用于对接受者线程生产的消息(或事件)进行处理,poller最终调用的是Handler的代码        pollers = new Poller[getPollerThreadCount()];        for (int i=0; i<pollers.length; i++) {            pollers[i] = new Poller();            Thread pollerThread = new Thread(pollers[i], getName() + "-ClientPoller-"+i);            pollerThread.setPriority(threadPriority);            pollerThread.setDaemon(true);            pollerThread.start();        }        // 5. 开启acceptor线程        startAcceptorThreads();    }}
protected final void startAcceptorThreads() {    int count = getAcceptorThreadCount();    acceptors = new Acceptor[count];
    for (int i = 0; i < count; i++) {        acceptors[i] = createAcceptor();        String threadName = getName() + "-Acceptor-" + i;        acceptors[i].setThreadName(threadName);        Thread t = new Thread(acceptors[i], threadName);        t.setPriority(getAcceptorThreadPriority());        t.setDaemon(getDaemon());        t.start();    }}
复制代码

Endpoint.start() 方法中,我们首先会调用 bind() 方法,完成 Socket 的绑定,确保 Connector 能够监听来自网络的请求。接着,我们会创建工作者线程池,为后续处理请求提供充足的线程资源。随后,我们会初始化连接 latch,用于限制请求的并发量,避免过多的请求涌入,造成系统崩溃。

接下来,我们会创建一个轮询 Poller 线程,负责处理来自 Acceptor 线程的事件,并将处理后的事件传递给 Handler。Poller 线程会调用 Handler 的代码进行处理,最终完成对请求的处理。最后,我们会创建一个 Acceptor 线程,专门负责监听网络请求,并将接收到的请求传递给 Poller 线程进行处理。

至此,我们已将 Connector 源码入口的分析告一段落,揭开了 Connector 启动过程的神秘面纱。接下来我们将继续深入探索 Connector 的请求逻辑,深入理解 Connector 如何接收请求,如何将请求封装成 Request 和 Response 对象,以及如何将这些对象传递给 Container 进行处理。让我们一起探索 Tomcat 的内部世界。 欢迎点赞关注,持续更新。

如有问题,欢迎微信搜索【码上遇见你】。

好了,本章节到此告一段落。希望对你有所帮助,祝学习顺利。

发布于: 刚刚阅读数: 4
用户头像

派大星

关注

微信搜索【码上遇见你】,获取更多精彩内容 2021-12-13 加入

微信搜索【码上遇见你】,获取更多精彩内容

评论

发布
暂无评论
【Tomcat源码分析】从零开始理解 HTTP 请求处理 (第一篇)_tomcat源码解读_派大星_InfoQ写作社区