tomca是如何处理一个http请求的?

问题:tomca是如何处理一个http请求的?

日常工作中,tomcat在大家心目中可能没有那么重要,大家还是主要关注ssm,springboot这些应用框架,但其实我们日常开发和构建一个Java Web应用程序,真正运行的时候还是需要JVM提供的运行环境,以及Tomcat这样的servlet容器来部署我们的应用。这样三个协同工作的三者常常会被忽略掉Tomcat和JVM,在考虑系统吞吐量时才会加以关注。Tomcat和JVM对系统吞吐量有着密切的关系,它们共同决定了Java Web应用程序的性能,通过调整Tomcat的线程池、连接池、缓冲区大小等配置参数,可以优化Tomcat的性能,从而提高系统吞吐量;通过调整JVM的堆大小、垃圾回收策略、线程池、JIT编译等参数,可以优化JVM的性能,从而也可以提供系统的吞吐量。

所以,本次主要是针对一个http请求是怎么被部署在tomcat中的一个web应用接收到,然后处理请求并返回对应的响应这个问题进行学习与分享。

tomcat配置文件解析

先从配置文件看一下tomcat是如何通过请求找到对应的web应用来处理对应的请求的,主要是与server.xml配置文件有关。

server.xml位于$TOMCAT_HOME/conf目录下;下面是目前正在使用的一个server.xml实例。

<?xml version='1.0' encoding='utf-8'?>
<!--顶层元素:<Server>,<Server>元素是整个配置文件的根元素,一个Server元素中可以有一个或多个Service元素,shutdown属性表示关闭Server的指令;port属性表示Server接收shutdown指令的端口号,设为-1可以禁掉该端口-->
<!--Server代表整个Tomcat容器;一个Server元素中可以有一个或多个Service元素。-->
<Server port="8005" shutdown="SHUTDOWN">
    <!--Listener(即监听器)定义的组件,可以在特定事件发生时执行特定的操作-->
    <Listener className="org.apache.catalina.core.JasperListener"/>
    <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener"/>
    <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener"/>
    <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener"/>
    <!--Service的作用,是在Connector和Engine外面包了一层,把它们组装在一起,对外提供服务。一个Service可以包含多个Connector,但是只能包含一个Engine;其中Connector的作用是从客户端接收请求,Engine的作用是处理接收进来的请求。-->
    <Service name="Catalina">
        <!--Connector的主要功能,是接收连接请求,创建Request和Response对象用于和请求端交换数据;然后分配线程让Engine来处理这个请求,并把产生的Request和Response对象传给Engine。-->
        <Connector port="8080" protocol="HTTP/1.1"
                   relaxedQueryChars="[]|{}^&#x5c;&#x60;&quot;&lt;&gt;"
                   maxThreads="400" connectionTimeout="5000"
                   enableLookups="false" compression="on"
                   redirectPort="8443"
                   URIEncoding="UTF-8"
                   compressableMimeType="text/csv,text/html,text/xml,text/css,text/plain,text/javascript,application/javascript,application/x-javascript,application/json,application/xml"
        />
        <!--Engine组件在Service组件中有且只有一个;Engine是Service组件中的请求处理组件。Engine组件从一个或多个Connector中接收请求并处理,并将完成的响应返回给Connector,最终传递给客户端。-->
        <Engine name="Catalina" defaultHost="localhost">
            <!--Host是Engine的子容器。Engine组件中可以内嵌1个或多个Host组件,每个Host组件代表Engine中的一个虚拟主机。Host组件至少有一个,且其中一个的name必须与Engine组件的defaultHost属性相匹配。-->
			<!--如果autoDeploy是一个自动部署的参数,appBase属性指定Web应用所在的目录,unpackWARs代表将Web应用的WAR文件解压-->
            <Host name="localhost" appBase="webapps"
                  unpackWARs="false" autoDeploy="false">
                <!--<Context path="/" docBase="D:\Program Files\app1.war" reloadable="true"/>-->
					<!--<Context/>节点之内还可以继续配置<wrapper/>节点,⼀个Wrapper表示⼀个Servlet的包装,定义Servlet时如果实现了SingleThreadModel接⼝,那么在Tomcat中可能会产⽣多个该Servlet的实
例对象,多个请求同时访问该Servlet,那么每个请求线程会有⼀个单独的Servlet对象,所以Tomcat可以再抽象出来⼀层,这⼀层就是Wrapper,⼀个Wrapper对应⼀个Servlet类型-->
 					<Wrapper className="org.example.MyServlet1" name="servlet1" />
                <!--单词Valve的意思是“阀门”,在Tomcat中代表了请求处理流水线上的一个组件;Valve可以与Tomcat的容器(Engine、Host或Context)关联。-->
                <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs" prefix="access" suffix=".log"
                       fileDateFormat=".yyyy-MM-dd.HH"
                       pattern="%h %l %u %t &quot;%r&quot; %s %b %D &quot;%{Referer}i&quot; &quot;%{User-Agent}i&quot; &quot;%{QunarGlobal}c&quot; %{X-Forwarded-For}i QTraceId[%{qtraceId}i] start###%{PostData}r end###"
                       resolveHosts="false"/>
            </Host>
        </Engine>
    </Service>
</Server>

如何确定请求由谁处理?

当请求被发送到Tomcat所在的主机时,如何确定最终哪个Web应用来处理该请求呢?

(1)根据协议和端口号选定Service和Engine

Service中的Connector组件可以接收特定端口的请求,因此,当Tomcat启动时,Service组件就会监听特定的端口。在上面的例子中,Catalina这个Service监听了8080端口(基于HTTP协议)。当请求进来时,Tomcat便可以根据协议和端口号选定处理请求的Service;Service一旦选定,Engine也就确定。通过在Server中配置多个Service,可以实现通过不同的端口号来访问同一台机器上部署的不同应用。

(2)根据域名或IP地址选定Host

Service确定后,Tomcat在Service中寻找名称与域名/IP地址匹配的Host处理该请求。如果没有找到,则使用Engine中指定的defaultHost来处理该请求。

(3)根据URI选定Context/Web应用

Tomcat根据应用的 path属性与URI的匹配程度来选择Web应用处理相应请求,有了请求路径以后,根据web.xml配置文件找到对应的servlet处理器处理请求。以下是一个web.xml实例:

<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
         version="2.5">
    <display-name>sirius-server</display-name>
    <!--配置一个叫web的servlet,实现为DispatcherServlet-->
    <servlet>
        <servlet-name>web</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
	<!--配置web的映射,以/galaxy/*为路径的请求交给DispatcherServlet处理-->
    <servlet-mapping>
        <servlet-name>web</servlet-name>
        <url-pattern>/galaxy/*</url-pattern>
    </servlet-mapping>

    <servlet>
        <servlet-name>CityDataReloadFileServlet</servlet-name>
        <servlet-class>com.qunar.hotel.web.CityDataReloadFileServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>CityDataReloadFileServlet</servlet-name>
        <url-pattern>/basicdata/reload</url-pattern>
    </servlet-mapping>


    <!--sirius定义的过滤器,用来记录接口调用方fromSource监控-->
	<filter>
   		<filter-name>InterfaceAccessFilter</filter-name>
  		<filter-class>com.qunar.hotel.price.systemcore.context.filter.InterfaceAccessFilter</filter-class>
	</filter>

	<filter-mapping>
    	<filter-name>InterfaceAccessFilter</filter-name>
    	<url-pattern>/nprice/*</url-pattern>
	</filter-mapping>
</web-app>

(4)举例

以请求http://localhost:8080/app1/index.html为例,首先通过协议和端口号(http和8080)选定Service;然后通过主机名(localhost)选定Host;然后通过uri(/app1/index.html)选定Web应用。

tomcat的连接数与连接池

在前面的Tomcat配置文件server.xml解析中提到:Connector的主要功能,是接收连接请求,创建Request和Response对象用于和请求端交换数据;然后分配线程让Engine(也就是Servlet容器)来处理这个请求,并把产生的Request和Response对象传给Engine。当Engine处理完请求后,也会通过Connector将响应返回给客户端。可以说,Servlet容器处理请求,是需要Connector进行调度和控制的,Connector是Tomcat处理请求的主干,因此Connector的配置和使用对Tomcat的性能有着重要的影响。

根据协议的不同,Connector可以分为HTTP Connector、AJP Connector等,这里只讨论HTTP Connector。

Connector的protocol

Connector在处理HTTP请求时,会使用不同的protocol。不同的Tomcat版本支持的protocol不同,其中最典型的protocol包括BIO、NIO和APR(Tomcat7中支持这3种,Tomcat8增加了对NIO2的支持,而到了Tomcat8.5和Tomcat9.0,则去掉了对BIO的支持)。

BIO是Blocking IO,顾名思义是阻塞的IO;NIO是Non-blocking IO,则是非阻塞的IO。而APR是Apache Portable Runtime,是Apache可移植运行库,利用本地库可以实现高可扩展性、高性能;Apr是在Tomcat上运行高并发应用的首选模式,但是需要安装apr、apr-utils、tomcat-native等包。

如何指定protocol

Connector使用哪种protocol,可以通过<connector>元素中的protocol属性进行指定,也可以使用默认值。

指定的protocol取值及对应的协议如下:

  • HTTP/1.1:默认值,使用的协议与Tomcat版本有关
  • org.apache.coyote.http11.Http11Protocol:BIO
  • org.apache.coyote.http11.Http11NioProtocol:NIO
  • org.apache.coyote.http11.Http11Nio2Protocol:NIO2
  • org.apache.coyote.http11.Http11AprProtocol:APR

如果没有指定protocol,则使用默认值HTTP/1.1,其含义如下:在Tomcat7中,自动选取使用BIO或APR(如果找到APR需要的本地库,则使用APR,否则使用BIO);在Tomcat8中,自动选取使用NIO或APR(如果找到APR需要的本地库,则使用APR,否则使用NIO)。

Connector的请求流程

在accept队列中接收连接(当客户端向服务器发送请求时,如果客户端与OS完成三次握手建立了连接,则OS将该连接放入accept队列);在连接中获取请求的数据,生成request;调用servlet容器处理请求;返回response为了便于后面的说明,首先明确一下连接与请求的关系:连接是TCP层面的(传输层),对应socket;请求是HTTP层面的(应用层),必须依赖于TCP的连接实现;一个TCP连接中可能传输多个HTTP请求。

在BIO实现的Connector中,处理请求的主要实体是JIoEndpoint对象。JIoEndpoint维护了Acceptor和Worker:Acceptor接收socket,然后从Worker线程池中找出空闲的线程处理socket,如果worker线程池没有空闲线程,则Acceptor将阻塞。其中Worker是Tomcat自带的线程池,如果通过<Executor>配置了其他线程池,原理与Worker类似。在NIO实现的Connector中,处理请求的主要实体是NIoEndpoint对象。NIoEndpoint中除了包含Acceptor和Worker外,还使用了Poller,Acceptor接收socket后,不是直接使用Worker中的线程处理请求,而是先将请求发送给了Poller,Acceptor向Poller发送请求通过队列实现,使用了典型的生产者-消费者模式。在Poller中,维护了一个Selector对象;当Poller从队列中取出socket后,注册到该Selector中;然后通过遍历Selector,找出其中可读的socket,并使用Worker中的线程处理相应请求。因此使用NIO,“读取socket并交给Worker中的线程”这个过程是非阻塞的,当socket在等待下一个请求或等待释放时,并不会占用工作线程,因此Tomcat可以同时处理的socket数目远大于最大线程数,并发性能大大提高。

相对应的,Connector中的几个参数功能如下:

acceptCount

accept队列的长度;当accept队列中连接的个数达到acceptCount时,队列满,进来的请求一律被拒绝。默认值是100。

maxConnections

Tomcat在任意时刻接收和处理的最大连接数。当Tomcat接收的连接数达到maxConnections时,Acceptor线程不会读取accept队列中的连接;这时accept队列中的线程会一直阻塞着,直到Tomcat接收的连接数小于maxConnections。如果设置为-1,则连接数不受限制。

默认值与连接器使用的协议有关:NIO的默认值是10000,APR/native的默认值是8192,而BIO的默认值为maxThreads(如果配置了Executor,则默认值是Executor的maxThreads)。

在windows下,APR/native的maxConnections值会自动调整为设置值以下最大的1024的整数倍;如设置为2000,则最大值实际是1024。

maxThreads

请求处理线程的最大数量。默认值是200(Tomcat7和8都是的)。如果该Connector绑定了Executor,这个值会被忽略,因为该Connector将使用绑定的Executor,而不是内置的线程池来执行任务。

maxThreads规定的是最大的线程数目,并不是实际running的CPU数量;实际上,maxThreads的大小比CPU核心数量要大得多。这是因为,处理请求的线程真正用于计算的时间可能很少,大多数时间可能在阻塞,如等待数据库返回数据、等待硬盘读写数据等。因此,在某一时刻,只有少数的线程真正的在使用物理CPU,大多数线程都在等待;因此线程数远大于物理核心数才是合理的。

换句话说,Tomcat通过使用比CPU核心数量多得多的线程数,可以使CPU忙碌起来,大大提高CPU的利用率。

Connector参数设置

(1)maxThreads的设置既与应用的特点有关,也与服务器的CPU核心数量有关。通过前面介绍可以知道,maxThreads数量应该远大于CPU核心数量;而且CPU核心数越大,maxThreads应该越大;应用中CPU越不密集(IO越密集),maxThreads应该越大,以便能够充分利用CPU。当然,maxThreads的值并不是越大越好,如果maxThreads过大,那么CPU会花费大量的时间用于线程的切换,整体效率会降低。

(2)maxConnections的设置与Tomcat的运行模式有关。如果tomcat使用的是BIO,那么maxConnections的值应该与maxThreads一致;如果tomcat使用的是NIO,maxConnections值应该远大于maxThreads。

(3)通过前面的介绍可以知道,虽然tomcat同时可以处理的连接数目是maxConnections,但服务器中可以同时接收的连接数为maxConnections+acceptCount 。acceptCount的设置,与应用在连接过高情况下希望做出什么反应有关系。如果设置过大,后面进入的请求等待时间会很长;如果设置过小,后面进入的请求立马返回connection refused。

tomcat是如何启动的?

Tomcat源码就从它的main方法开始。Tomcat的main方法在org.apache.catalina.startup.Bootstrap 里:

public final class Bootstrap {
    ……
    
    /**
     * Daemon object used by main.
     */
    private static final Object daemonLock = new Object();
    
    ……
    
    
   /**
     * Main method and entry point when starting Tomcat via the provided
     * scripts.
     *
     * @param args Command line arguments to be processed
     */
    public static void main(String args[]) {
        // 创建一个 Bootstrap 对象,调用它的 init 方法初始化
        synchronized (daemonLock) {
            if (daemon == null) {
                // Don't set daemon until init() has completed
                Bootstrap bootstrap = new Bootstrap();
                try {
                    bootstrap.init();
                } catch (Throwable t) {
                    handleThrowable(t);
                    t.printStackTrace();
                    return;
                }
                daemon = bootstrap;
            } else {
                // When running as a service the call to stop will be on a new
                // thread so make sure the correct class loader is used to
                // prevent a range of class not found exceptions.
                Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
            }
        }

        // 根据启动参数,分别调用 Bootstrap 对象的不同方法
        try {
            String command = "start"; // 默认是start
            if (args.length > 0) {
                command = args[args.length - 1];
            }

            if (command.equals("startd")) {
                args[args.length - 1] = "start";
                daemon.load(args);
                daemon.start();
            } else if (command.equals("stopd")) {
                args[args.length - 1] = "stop";
                daemon.stop();
            } else if (command.equals("start")) {
                daemon.setAwait(true);
                daemon.load(args);
                daemon.start();
                if (null == daemon.getServer()) {
                    System.exit(1);
                }
            } else if (command.equals("stop")) {
                daemon.stopServer(args);
            } else if (command.equals("configtest")) {
                daemon.load(args);
                if (null == daemon.getServer()) {
                    System.exit(1);
                }
                System.exit(0);
            } else {
                log.warn("Bootstrap: command \"" + command + "\" does not exist.");
            }
        } catch (Throwable t) {
            // Unwrap the Exception for clearer error reporting
            if (t instanceof InvocationTargetException &&
                    t.getCause() != null) {
                t = t.getCause();
            }
            handleThrowable(t);
            t.printStackTrace();
            System.exit(1);
        }

    }
    
    ……
}

LifecycleBase是Tomcat中组件生命周期的统一管理接口的实现类,该类对相关组件的生命周期进行了统一管理。tomcat中的LifecycleBase模版组件的init()方法中实现组件的初始化,在start()方法中实现组件的启动。

再看Tomcat总体架构

从上面的tomcat配置文件解析可以大致清楚tomcat的总体架构以及如何使用Connector与客户端请求连接。以下是一个经典的tomcat架构图:

前面是从架构层面了解了tomcat是如何处理请求的,接下来从一个请求的角度真正了解一下一个http请求(以get请求为例)是如何被响应的。如下图所示,以Request请求为分界线,左边是一个http请求被tonmcat解析为能被Servlet处理的Request请求流程,右边是tomcat如何通过配置文件找到对应的Servlet的流程。

Servlet如何处理请求

Servlet规范

Servlet(Server Applet),是用Java编写的服务器端程序,用户请求使Servlet容器调用Servlet的Service()方法,并传入一个ServletRequest对象和一个ServletResponse对象。ServletRequest对象和ServletResponse对象都是由Servlet容器(例如TomCat)封装好的,并不需要程序员去实现,程序员可以直接使用这两个对象。以下是Servlet接口的定义,这几个接口也表明的Servlet的生命周期:

public interface Servlet {
    void init(ServletConfig var1) throws ServletException;
    ServletConfig getServletConfig();
    void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
    String getServletInfo();
    void destroy();
}

自定义Servlet

使用demo自定义一个Servlet:

public class MhbHttpServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().println("Hello Mhb");
    }
}

在web.xml中配置对应的Servlet映射:

<servlet>
    <servlet-name>MhbHttpServlet</servlet-name>
    <servlet-class>com.example.demo.MhbHttpServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>MhbHttpServlet</servlet-name>
    <url-pattern>/mhb</url-pattern>
</servlet-mapping>

tomcat拿到Request以后,经过不同的容器层层传递,最后找到对应的Servlet处理器,因此tomcat本质就是一个Servlet容器。但是如果tomcat只是为了多封装几层Servlet,其实也没有太多的意义,因此,tomcat里面每一层容器还提供了一个组件叫做pipeLine,每一个pipeLine中维护了自定义的一些valve组件(可以参考前面给的server.xml实例配置的valve)。这样设计的目的就是为了让Request在每一层容器之间传递的时候都去执行一次该层的valve组件,当执行完所有的valve之后,Request请求才能真正交给Servlet执行。

值得注意的是,每一层容器的最后一个valve需要把Request传递到下一层容器,最终就是由wrapper容器的最后一个valve传递给Servlet,由于逻辑都差不多,所以直接看一下wrapper里的valve是怎么实现的:

public StandardWrapper() {
    super();
    swValve = new StandardWrapperValve();
    // 给管道默认设置了basic valve,也就是说每一层容器的最后一个valve是tomcat内部实现的
    pipeline.setBasic(swValve);
    broadcaster = new NotificationBroadcasterSupport();
}


// 接下来看一下调用valve时的重点方法
public void invoke(Request request, Response response) throws IOException, ServletException {
	// 分配一个Servlet实例
	servlet = wrapper.allocate();
    	// allocate()方法里会调用Servlet的加载一个Servlet
		instance = loadServlet();
			//loadServlet()方法里会调用对应的Servlet(与配置的Servlet类有关)的类加载器实例化一个Servlet
			servlet = (Servlet) instanceManager.newInstance(servletClass);
   	// 至此,拿到了一个Servlet实例以后,按照我们的理解,就可以调用doGet()方法来执行了,但是tomcat并没有直接调用doGet(),而是构造了一个filter链,这里只要是为了实现我们配置的filter(如前面web.xml)
	ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
	// createFilterChain()方法里会去调用filter.doFilter(request, response, this);然后回调用
    servlet.service(request, response);
    // 按照理解,应该是执行对应Servlet的doGet方法,但是确是执行的service方法,进入到service方法,就能找到执行doget方法的地方
  protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

    String method = req.getMethod();

    if (method.equals(METHOD_GET)) {
        long lastModified = getLastModified(req);
        if (lastModified == -1) {
            // servlet doesn't support if-modified-since, no reason
            // to go through further expensive logic
            doGet(req, resp);
        } else {
            long ifModifiedSince;
            try {
                ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
            } catch (IllegalArgumentException iae) {
                // Invalid date header - proceed as if none was set
                ifModifiedSince = -1;
            }
            if (ifModifiedSince < (lastModified / 1000 * 1000)) {
                // If the servlet mod time is later, call doGet()
                // Round down to the nearest second for a proper compare
                // A ifModifiedSince of -1 will always be less
                maybeSetLastModified(resp, lastModified);
                doGet(req, resp);
            } else {
                resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
            }
        }
	}
}

Servlet如何接收请求

TCP链接传输请求数据

首先,Request对象本质是请求的数据,这个数据来源于客户端,或者说是客户端服务器,而tomcat是服务端服务器,两个服务器之间交换数据则需要建立TCP链接(先不考虑UDP链接)。建立Tcp链接是操作系统完成的(三次握手等),具体实现可以参考linux源码中linux-6.4.9/net/ipv4/tcp_output.c:3836的tcp_connect(struct sock *sk)方法,tcp协议属于传输层。具体调用tcp_connect的是封装了tcp协议的socket,具体可以参考linux源码 linux-6.4.9/net/socket.c:2009中的__sys_connect(int fd, struct sockaddr __user *uservaddr, int addrlen)方法,这也是为什么说socket是基于tcp协议的接口。

在java里面创建一个socket链接,其实调用的是:java.net.DualStackPlainSocketImpl#socket0这个本地方法

public static void main(String[] args) throws IOException {
    Socket socket = new Socket(); 
    socket.connect(new InetSocketAddress("localhost", 8080));
    DatagramSocket datagramSocket = new DatagramSocket();
}


void socketCreate(boolean stream) throws IOException {
    if (fd == null)
        throw new SocketException("Socket closed");
	//	层层调用后,最终调用到一个建立链接的本地方法
    int newfd = socket0(stream, false /*v6 Only*/);
    fdAccess.set(fd, newfd);
}

由于是本地方法,因此只能在openJDK的开源代码中找到定义:

JNIEXPORT jint JNICALL Java_java_net_DualStackPlainSocketImpl_socket0
  (JNIEnv *env, jclass clazz, jboolean stream, jboolean v6Only /*unused*/) {
    int fd, rv, opt=0;
    //  这里调用了NET_Socket方法
    fd = NET_Socket(AF_INET6, (stream ? SOCK_STREAM : SOCK_DGRAM), 0);
    if (fd == INVALID_SOCKET) {
        NET_ThrowNew(env, WSAGetLastError(), "create");
        return -1;
    }
    rv = setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, (char *) &opt, sizeof(opt));
    if (rv == SOCKET_ERROR) {
        NET_ThrowNew(env, WSAGetLastError(), "create");
    }
    SetHandleInformation((HANDLE)(UINT_PTR)fd, HANDLE_FLAG_INHERIT, FALSE);
    return fd;
}


// NET_Socket 方法调用了socket (domain, type, protocol);建立链接,而socket定义在操作系统的winsock2.h中
int NET_Socket (int domain, int type, int protocol) {
    SOCKET sock;
    sock = socket (domain, type, protocol);
    if (sock != INVALID_SOCKET) {
        SetHandleInformation((HANDLE)(uintptr_t)sock, HANDLE_FLAG_INHERIT, FALSE);
    }
    return (int)sock;
}


层层调用后发现,socket链接最终由操作系统完成,具体实现没有开源,大概率最终会调用前面提到的__sys_connect(int fd, struct sockaddr __user *uservaddr, int addrlen)做socket链接,然后调用tcp_connect(struct sock *sk)实现真正的tcp链接。

Http协议解析数据

TCP协议只管数据传输,tomcat拿到数据以后需要读取数据,这个时候涉及到IO,而前面已经提到,tomcat中的IO有BIO、NIO等,具体配置方式也在前面提到。以 <Connector port="8080" protocol="HTTP/1.1">为例,这里配置的协议为http1.1,在tomcat7中,配置http1.1会被解析为org.apache.coyote.http11.Http11Protocol(是一种BIO实现),而在tomcat8中会被默认解析为org.apache.coyote.http11.Http11NioProtocol(是一种NIO实现)这个类来处理。

以Http11Protocol为例具体看一下是怎么将socket拿到的数据转换成Request请求的:

public Http11Protocol() {
    // Http11Protocol中维护了一个JIoEndpoint
    endpoint = new JIoEndpoint();
    cHandler = new Http11ConnectionHandler(this);
    ((JIoEndpoint) endpoint).setHandler(cHandler);
    setSoLinger(Constants.DEFAULT_CONNECTION_LINGER);
    setSoTimeout(Constants.DEFAULT_CONNECTION_TIMEOUT);
    setTcpNoDelay(Constants.DEFAULT_TCP_NO_DELAY);
}
// JIoEndpoint中维护了一个Acceptor 对象接收一个socket链接
protected class Acceptor extends AbstractEndpoint.Acceptor {

    @Override
    public void run() {
		// Accept the next incoming connection from the server
		// socket
		socket = serverSocketFactory.acceptSocket(serverSocket);
	}
}
// 最终在org.apache.coyote.http11.AbstractHttp11Processor#process中读取socket中的数据,并使用http协议解析请求头与请求行等,以下以解析请求行为例:
if (!getInputBuffer().parseRequestLine(keptAlive)) {
    if (handleIncompleteRequestLineRead()) {
        break;
    }
}
// parseRequestLine()方法按照以下http协议规范进行数据解析。
// 同时为Request赋值,这里以赋值请求方法为例:
request.method().setBytes(buf, start, pos - start);

总结

最后还是借助前面提到的这种图总结一下tomcat是怎么作为Servlet容器来处理web应用的http请求的:

  1. tomcat接收到一个http请求以后,通过HttpServletRequest对象,也就是请求信息,找到该请求对应的Host、Context、Wrapper
  2.  然后将请求交给Engine层处理
  3. Engine层处理完,就会将请求交给Host层处理
  4. Host层处理完,就会将请求交给Context层处理
  5. Context层处理完,就会将请求交给Wrapper层处理
  6. Wrapper层在拿到⼀个请求后,就会⽣成⼀个请求所要访问的Servlet实例对象
  7. 调⽤Servlet实例对象的service()⽅法,并把HttpServletRequest对象当做⼊参
  8. 从⽽就调⽤到Servlet所定义的逻辑
  9. 那么HttpServletRequest对象是怎么来的?
    1. 首先我们查看socket.connect()可以知道客户端与服务端之间建立socket连接是操作系统完成的。
    2. Tomcat会在指定的端口上监听传入的HTTP连接请求,这通常由配置文件中的Connector元素指定。
    3. 一旦有HTTP连接请求到达,Tomcat内部会创建一个底层的Socket连接,由操作系统完成
    4. 然后通过建立tcp连接发送数据
    5. 最终在对应的配置的协议里完成请求的解析,生成对应的Request对象,发送给对应的Servlet处理。ProtocolHandler 里面有3个非常重要的组件:Endpoint、Processor和Adapter。Endpoint用来实现TCP/IP协议,SocketProcessor用来实现HTTP 协议,Adapter 将请求适配到 Servlet 容器进行具体处理。
    6. 在tomcat中,使用了门面模式实现了ServletRequest规范,具体的实现类是RequestFacade

最终可以得到如下的请求流程图:

  • 40
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值