一、Http请求过程总览
浏览器请求
http://localhost/test/index.jsp
用户点击网页内容,请求被发送到本机端口8080,被在那里监听的Coyote HTTP/1.1 Connector获得。
Connector将Request包装成ServletRequest给它所在的Service的Engine来处理,并等待Engine的回应。
Engine获得请求localhost/test/index.jsp,匹配所有的虚拟主机Host。
Engine匹配到名为localhost的Host(即使匹配不到也把请求交给该Host处理,因为该Host被定义为该Engine的默认主机),名为localhost的Host获得请求/test/index.jsp,匹配它所拥有的所有的Context。Host匹配到路径为/test的Context(如果匹配不到就把该请求交给路径名为""的Context去处理)。
path="/test"的Context获得请求/index.jsp,在它的mapping table中寻找出对应的Servlet。Context匹配到URL PATTERN为*.jsp的Servlet,对应于JspServlet类。
构造HttpServletRequest对象和HttpServletResponse对象,作为参数调用JspServlet的doGet()或doPost().执行业务逻辑、数据存储等程序。
Context把执行完之后的HttpServletResponse对象返回给Host。
Host把HttpServletResponse对象返回给Engine。
Engine把HttpServletResponse对象返回Connector,Connector把ServletResponse对象封装成Response。
Connector把Response返回给客户Browser。
二、具体组件及源码分析
2.1 外部网络请求
2.1.1 Connector
用户在浏览器中输入一个URL地址之后浏览器将发起一个Http的请求,通过网络传输数据,请求到我们的Tomcat服务器中,Tomcat使用Connector接收Socket请求数据并通过Coyote链接器封装底层网络通信,为Catalina容器提供了统一的接口。
在Coyote中,Tomcat支持一下3种协议:
- HTTP/1.1协议:目前最常用的访问协议
- AJP协议:Apache提供的一种协议,用于和Apache HTTP Server集成。
- HTTP/2.0协议:下一代HTTP协议,自Tomcat8.5版本开始支持。
针对HTTP和AJP协议,Coyote又按照I/O方式分别提供了不同的方案。
- BIO:同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成,Tomcat早期网络请求的默认实现方式(自8.5版本开始,已经移除)
- NIO:同步非阻塞的I/O模型(当前Tomcat的默认实现)
- NIO2:异步非阻塞I/O模型
- APR:Apache可移植运行库。
2.1.2 NioEndpoint
我们以NioEndpoint为例,来看一下Tomcat是如何实现网络请求收发的。
/**
* Defaults to using HTTP/1.1 NIO implementation.
* 默认使用NIO实现的http/1.1协议
*/
public Connector() {
this("HTTP/1.1");
}
public Connector(String protocol) {
boolean apr = AprStatus.getUseAprConnector() && AprStatus.isInstanceCreated()
&& AprLifecycleListener.isAprAvailable();
ProtocolHandler p = null;
try {
//根据指定的协议,创建不同的处理器
p = ProtocolHandler.create(protocol, apr);
} catch (Exception e) {
log.error(sm.getString(
"coyoteConnector.protocolHandlerInstantiationFailed"), e);
}
//省略部分代码...
}
我们发现Tomcat默认使用HTTP/1.1协议创建Http11NioProtocol,它默认使用NioEndpoint
public Http11NioProtocol() {
super(new NioEndpoint());
}
当Connector执行init()的同时运行了protocolHandler.init()最终运行了NioEndpoint.init(),最终就是启动了一个ServerSocket并绑定监听端口。
protected void initServerSocket() throws Exception {
//省略部分代码
serverSock = ServerSocketChannel.open();
socketProperties.setProperties(serverSock.socket());
InetSocketAddress addr = new InetSocketAddress(getAddress(), getPortWithOffset());
serverSock.bind(addr, getAcceptCount());
}
当Connector执行start()方法时,最终执行了:NioEndpoint.startInternal()方法。该方法内部启动了一个Poller线程,一个Acceptor线程。其中,Acceptor用于监听客户端连接,并将Socket连接放入待处理事件缓存池。Poller循环从待处理的事件缓存队列中拿到请求交给线程池处理。Poller将Socket请求包装成为:SocketProcessor对象,执行processor.process()方法,最终调用:getAdapter().service(request, response);这里的getAdapter即CoyoteAdapter,最终找到容器中管道方法的第一个阀门方法,发起调用,此时,网络请求正式进入我们容器中。
// Calling the container 调用容器
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
TIPS 如果想要详细了解Tomcat接收请求的流程图可以查看官网提供的资料
http://tomcat.apache.org/tomcat-9.0-doc/architecture/requestProcess/request-process.png
2.2 容器内部请求路径匹配
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res)
throws Exception {
//将coyote链接器中的Request,Response转换为我们常用的HttpServletRequest,HttpServletResponse
Request request = (Request) req.getNote(ADAPTER_NOTES);
Response response = (Response) res.getNote(ADAPTER_NOTES);
//省略部分代码...
// 匹配请求路径,将当前request请求到匹配到的servlet上
postParseSuccess = postParseRequest(req, request, res, response);
if (postParseSuccess) {
//check valves if we support async
request.setAsyncSupported(
connector.getService().getContainer().getPipeline().isAsyncSupported());
// Calling the container 调用容器
connector.getService().getContainer().getPipeline().getFirst().invoke(
request, response);
// 省略部分代码...
}
在Tomcat启动的时候 Service持有了一个Mapper对象,它借助MapperListener在应用启动的过程中,将Engine,Host,Context,Wrapper路径都搜集了起来。当一个请求过来的时候它会去当前 Service中去匹配,最终将请求执行到我们StandardWrapperValve。此时会创建ApplicationFilterChain过滤器链;通过调用过滤器内部方法internalDoFilter;最终调用:javax.servlet.http.HttpServlet.service()方法,请求到应用中!
三、本文小结
我们通过一个Http请求路径分析了Tomcat是使用Socket接收网络请求,使用Coyote转换我们的网络协议参数,包装为ServletRequest和ServletResponse,使用匹配请求路径的方式最终找到我们应用中的Servlet地址。
至此,可以说我们对Tomcat的主要功能基本上已经了解的七七八八了,后续我们将会陆续的分享一些工作中常用的小技巧。
程序员的核心竞争力其实还是技术,因此对技术还是要不断的学习,关注 “IT巅峰技术” 公众号 ,该公众号内容定位:中高级开发、架构师、中层管理人员等中高端岗位服务的,除了技术交流外还有很多架构思想和实战案例。
作者是 《 消息中间件 RocketMQ 技术内幕》 一书作者,同时也是 “RocketMQ 上海社区”联合创始人,曾就职于拼多多、德邦等公司,现任上市快递公司架构负责人,主要负责开发框架的搭建、中间件相关技术的二次开发和运维管理、混合云及基础服务平台的建设。