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