简介
对一个java web应用来说,Tomcat是首,SSM是中,JVM是尾。我们通常对于SSM是比较了解的,而忽略了首尾,而Tomcat在目前的网络编程中是举足轻重的。如果能够掌握Tomcat的原理,那么是非常有用的,比如:
- Tomcat到底是如何处理一个请求的?这对于针对Tomcat的性能调优是必备的。
- 目前Spring Boot和Dubbo等框架中都是使用的内嵌Tomcat,那么一个内嵌的Tomcat到底是如何运行的?
- Tomcat的架构设计其实非常优秀的,如果能明白Tomcat为什么要那么设计,那么对于Tomcat的原理和自己的架构设计思维都能有很大提升。
《how tomcat works》的书写方式类似于 从0到1 写tomcat,从一个入门级程序猿的demo 代码开始,逐渐演化 出一个web 容器。
代码的演化
新手的直观感觉
While(true){
Socket socket = serverSocket.accept();
inputStream in = xxx
OutputStream out = xx
按照http协议读取和写入
}
进化
Whle(true){
Socket socket = serverSocket.accept();
inputStream in = xxx
OutputStream out = xx
Request request= new Request(in)
Response reponse = new Response (out)
HttpProcessor processor = new HttpProcessor(request,response)
}
进化
class Bootstrap{
public static void main(String[] args){
HttpConnector connector = new HttpConnector();
connector.start();
}
}
HttpConnector implements Runnalbe{
public void run(){
socket = serversSocket.accept();
while(){
HttpProcessor processor = new HttpProcessor(this);
processor.process(socket)
}
}
}
class HttpProcessor{
public void process(Socket socket){
request = xxx
response = xxx
if(request.getRequestURI().startsWith(“/servlet/”)){
ServletProcessor processor = new ServletProcessor();
Processor.process(request,response);
}else{
StaticResourceProcessor processor = new StaticResourceProcessor();
processor.process(request,response)
}
}
}
httpConnector,接受socket,传给HttpProcessor,解析request和response。HttpProcessor传给Container,container.invoke(request,response)
实现具体的业务逻辑。
Class Container{
Invoke(request,response){
Pipeline.invoke(request,response)
}
}
更准确的说:详解tomcat的连接数与线程池 Connector的主要功能,是接收连接请求,创建Request和Response对象用于和请求端交换数据;然后分配线程让Engine(也就是Servlet容器)来处理这个请求,并把产生的Request和Response对象传给Engine。当Engine处理完请求后,也会通过Connector将响应返回给客户端。
Container容器(tomcat 中叫 Catalina )有父子关系,有四种容器:
- engine(引擎) , 为一个域名找到 合适的 host
- host(主机) , 为一个域名下的url返回处理它的context
- context(上下文),代表一个web 项目,加载配置文件,调用sessionManager 等
- wrapper(包装器),包装一个servlet
父子关系,就是分层,在不同的层次解决不同的问题。
对于每一个连接,连接器都会调用关联容器的 invoke 方法。接下来容器调用它 的所有子容器的 invoke 方法。但容器并不是invoke方法的简单包装,为了invoke方法可以正常执行,容器必须“加载servlet(对于wrapper容器来讲,适当时机执行servlet.init,servlet.destroy),加载配置文件(对于context来讲)等”外围工作,不同级别的容器,所做的外围工作不同。
过滤器
== 接口定义 ==
public interface Filter {
.....
//执行过滤
public void doFilter ( ServletRequest request, ServletResponse response, FilterChain chain ) throws IOException, ServletException;
}
public interface FilterChain {
public void doFilter ( ServletRequest request, ServletResponse response ) throws IOException, ServletException;
}
== 实现类 ==
class ApplicationFilterChain implements FilterChain {
private int pos=0;
public void doFilter ( ServletRequest request, ServletResponse response ) throws IOException, ServletException{
//pos为当前filter的所在位置,n为filters数组的长度
if (pos < n) {
//pos++执行后,把filterchain的当前filter指向下一个
ApplicationFilterConfig filterConfig = filters[pos++];
Filter filter = null;
try {
filter = filterConfig.getFilter();
//filter执行过滤操作
filter.doFilter(request, response, this);
}
...
}
}
}
class SampleFilter implements Filter {
........
public void doFilter(ServletRequest request, ServletResponse response,FilterChain chain)
throws IOException, ServletException {
//do something
.....
//request, response传递给下一个过滤器进行过滤
chain.doFilter(request, response);
}
}
chain.doFilter 就是一个内部递归,只是分散在了两个对象上执行。
== 我当时想起来的方式 ==
Filter{
Boolean doFilter(req,resp);
}
FilterChain{
doFilter(req,resp){
Filters = xxx;
for(int i=0;i<n;i++){
if(! filters[i].doFilter(req,resp)){
break;
}
}
servlet.service(req,resp)
}
}
这种方式有以下缺点
- 处理完filter,还得单独调用一下servlet。而tomcat的模式,filter和servlet是兼容的
- 这种方式只能实现前向过滤,不能实现后置过滤,要知道,在tomcat的filter中可以
线程池
- HttpProcessor 会被封装成 runnable 交給Executor 执行。 所以,所谓丢弃 连接,或者服务端执行 超时,都要从线程池 提交任务 这个事情来理解
- 线程池的核心 就两个事儿:核心线程数、等待队列。因此,tomcat中 也会对应有 最小线程数、最大线程数、队列长度(tomcat 中叫acceptCount)等配置。可见,tomcat 某一个时刻能处理的最大请求数 由最大线程数 + 队列长度 决定的。
- 线程池 线程数 是有限的,超过线程数 会在队列中等待。如果队列已满,则会执行reject 策略。 默认策略是:线程池 拒绝 HttpProcessor 为主体的 runnable,服务端关闭 socket,抛异常。客户端感知到 连接被关闭了 connection refused。
-
如果 一个线程 执行超时,则客户端会断开连接。此时,服务端线程 仍然继续 持有 socket 并做运算,只是最终向socket 写入数据时(
connector.OutputBuffer.realWriteBytes(OutputBuffer.java:393)
),会报Broken pipe异常(当然,这只是引起Broken pipe 原因之一)。org.apache.catalina.connector.ClientAbortException: java.io.IOException: Broken pipe at org.apache.catalina.connector.OutputBuffer.realWriteBytes(OutputBuffer.java:393) at org.apache.tomcat.util.buf.ByteChunk.flushBuffer(ByteChunk.java:426) at org.apache.catalina.connector.OutputBuffer.doFlush(OutputBuffer.java:342) at org.apache.catalina.connector.OutputBuffer.close(OutputBuffer.java:295) at org.apache.catalina.connector.Response.finishResponse(Response.java:453) at org.apache.catalina.core.StandardHostValve.throwable(StandardHostValve.java:378)
客户端tcp 连接关闭时,服务端会有大量的CLOSE_WAIT 状态的连接,检查服务端CLOSE_WAIT 连接数 也是定位问题的手段之一。
其它
- 容器通常会 recycle 请求对象,以避免创建请求对象的性能开销。
- 每个 request 对象只在 servlet 的服务方法的范围内有效,或者在过滤器的 doFilter 方法的范围内有效。除非异步处理功能被启用,并且在 request 上调用了 startAsync 方法,此时request 对象的生命周期一直会延续到在 AsyncContext 上调用 complete 方法之前。此外,调用 complete 方法之后 response 流才会关闭。
if (调用过 request 的 startAsync 方法) { 先不回收 }
异步的正确打开方式
AsyncContext asyncContext = request.startAsync(request,response) new Thread(()->{ ... PrintWriter out = response.getWriter() out.xx out.flush() asyncContext.complete() }).start()