前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【Tomcat源码分析】从零开始理解 HTTP 请求处理 (第一篇)

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

作者头像
@派大星
发布2024-09-18 16:31:45
600
发布2024-09-18 16:31:45
举报
文章被收录于专栏:码上遇见你

前言

终于步入 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()

代码语言:javascript
复制
@Override
protected 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()

代码语言:javascript
复制
@Override
protected 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 的运作提供了一个宏观的视角。

代码语言:javascript
复制
<?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 的构造方法:

代码语言:javascript
复制
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;
    }
}

@Deprecated
public 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()

代码语言:javascript
复制
@Override
protected 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 子类,它会根据不同的协议和通信方式进行相应的初始化操作。

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

代码语言:javascript
复制
@Override
public 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() 方法。

代码语言:javascript
复制
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 地址和端口

代码语言:javascript
复制
@Override
public 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()

代码语言:javascript
复制
@Override
protected 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

代码语言:javascript
复制
@Override
public 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 迎接来自网络的请求做好准备。

代码语言:javascript
复制
public final void start() throws Exception {
    // 1. `bind()`已经在`init()`中分析过了
    if (bindState == BindState.UNBOUND) {
        bind();
        bindState = BindState.BOUND_ON_START;
    }
    startInternal();
}

@Override
public 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 的内部世界。 欢迎点赞关注,持续更新。

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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-09-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码上遇见你 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • Connector 源码分析入口
    • Service.initInternal()
      • Service.startInternal()
        • Connector 构造方法
          • Connector.initInternal()
            • Connector.startInternal()
            相关产品与服务
            容器服务
            腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档