Tomcat对异步Servlet的支持(上)

简介: Tomcat对异步Servlet的支持

线程分类

Tomcat启动的线程和Web应用本身启动的线程:


  • Tomcat线程池中的线程会调用Servlet#service,叫Tomcat线程
  • Web程序在service方法的实现里启动的新线程,叫Web应用线程


当一个新请求到达,Tomcat会从线程池取一个线程处理,该线程会调用你的Web应用,Web应用在处理请求过程中,Tomcat线程会一直阻塞,直到Web应用处理完,才输出响应,最后Tomcat回收该线程。


假如Web应用需很长时间处理一个请求(比如DB查询或等待下游的服务调用返回),则Tomcat线程一直不回收,就会占用系统资源,极端情况下会导致“线程饥饿”,即Tomcat没有更多线程处理新请求了。


怎么办呢?



于是,Servlet 3.0引入异步Servlet:在Web应用里启动一个单独线程执行这些耗时请求,而Tomcat线程立即返回,不再等待Web应用将请求处理完,这样Tomcat线程可立即被回收到线程池,以响应其他请求,降低系统资源消耗。

异步Servlet只能说让Tomcat有机会接收更多请求,但并不能提升服务吞吐量,因为若业务操作本身还是很慢,业务线程池仍会被占满,后面提交的任务还是要等待。


业务处理一般阻塞在I/O等待,越是I/O密集型应用,越需要配置更多线程。


异步Servlet案例

@WebServlet(urlPatterns = {"/async"}, asyncSupported = true)
public class AsyncServlet extends HttpServlet {
    // Web应用线程池,用来处理异步Servlet
    ExecutorService executor = Executors.newSingleThreadExecutor();
    public void service(HttpServletRequest req, HttpServletResponse resp) {
        // 1. 调用startAsync或者异步上下文
        final AsyncContext ctx = req.startAsync();
       //用线程池来执行耗时操作
        executor.execute(new Runnable() {
            @Override
            public void run() {
                //在这里做耗时的操作
                try {
                    ctx.getResponse().getWriter().println("Handling Async Servlet");
                } catch (IOException e) {}
                //3. 异步Servlet处理完了调用异步上下文的complete方法
                ctx.complete();
            }
        });
    }
}

通过注解注册Servlet:@WebServlet注解,asyncSupported=true表明当前的Servlet是一个异步Servlet。

Web应用程序需要调用Request对象的startAsync方法来拿到一个异步上下文AsyncContext。这个上下文保存了请求和响应对象。

Web应用需要开启一个新线程处理耗时操作:处理完成后调用AsyncContext#complete,通知Tomcat,请求已经处理完。


虽然异步Servlet允许用更长的时间来处理请求,但是也有超时限制的,默认是30秒,如果30秒内请求还没处理完,Tomcat会触发超时机制,向浏览器返回超时错误,如果这个时候你的Web应用再调用ctx.complete方法,会得到IllegalStateException。


异步Servlet原理

Tomcat在这个过程的关键:

startAsync方法


创建一个异步上下文AsyncContext对象,保存请求的中间信息,比如Request和Response对象等上下文信息。

这是因为Tomcat的工作线程在request.startAsync调用之后,就直接结束回到线程池中了,线程本身不会保存任何信息。也就是说一个请求到服务端,执行到一半,你的Web应用正在处理,这个时候Tomcat的工作线程没了,这就需要有个缓存能够保存原始的Request和Response对象,而这个缓存就是AsyncContext。


有了AsyncContext,你的Web应用通过它拿到Request和Response对象,拿到Request对象后就可以读取请求信息,请求处理完了还需要通过Response对象将HTTP响应发送给浏览器。


除了创建AsyncContext对象,startAsync还需要完成一个关键任务,那就是告诉Tomcat当前的Servlet处理方法返回时,不要把响应发到浏览器,因为这个时候,响应还没生成呢;并且不能把Request对象和Response对象销毁,因为后面Web应用还要用呢。


在Tomcat中,负责flush响应数据的是CoyoteAdapter,它还会销毁Request对象和Response对象,因此需要通过某种机制通知CoyoteAdapter,具体来说是通过下面这行代码:


this.request.getCoyoteRequest().action(ActionCode.ASYNC_START, this);

你可以把它理解为一个Callback,在这个action方法里设置了Request对象的状态,设置它为一个异步Servlet请求。


连接器调用CoyoteAdapter#service处理请求的,而CoyoteAdapter会调用容器的service,当容器的service方法返回,CoyoteAdapter判断当前的请求是不是异步Servlet请求:


  • 如果是,就不会销毁Request和Response对象,也不会把响应信息发到浏览器。

CoyoteAdapter#service

public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) {
   //调用容器的service方法处理请求
    connector.getService().getContainer().getPipeline().
           getFirst().invoke(request, response);
   //如果是异步Servlet请求,仅仅设置一个标志,
   //否则说明是同步Servlet请求,就将响应数据刷到浏览器
    if (request.isAsync()) {
        async = true;
    } else {
        request.finishRequest();
        response.finishResponse();
    }
   //如果不是异步Servlet请求,就销毁Request对象和Response对象
    if (!async) {
        request.recycle();
        response.recycle();
    }
}

当CoyoteAdapter#service返回到ProtocolHandler组件,ProtocolHandler判断返回值,如果当前请求是一个异步Servlet请求,它会把当前Socket的协议处理者Processor缓存起来,将SocketWrapper对象和相应的Processor存到Map

private final Map<S,Processor> connections = new ConcurrentHashMap<>();

缓存是因为这个请求接下来还要接着处理,还由原来Processor处理,通过SocketWrapper就能从Map里找到相应Processor。


complete方法


当请求处理完成时,Web应用调用这个方法:把响应数据发送到浏览器。


这事不能由Web应用线程负责,即ctx.complete不能直接把响应数据发送到浏览器,因为这件事情应该由Tomcat线程处理。


连接器中的Endpoint组件检测到有请求数据达到时,会创建一个SocketProcessor对象交给线程池去处理,因此Endpoint的通信处理和具体请求处理在两个线程里运行。


在异步Servlet场景,Web应用通过调用ctx.complete方法时,也可生成一个新的SocketProcessor任务类,交给线程池处理。


对于异步Servlet请求来说,相应的Socket和协议处理组件Processor都被缓存起来了,并且这些对象都可以通过Request对象拿到。

public void complete() {
    //检查状态合法性,我们先忽略这句
    check();
    //调用Request对象的action方法,其实就是通知连接器,这个异步请求处理完了
request.getCoyoteRequest().action(ActionCode.ASYNC_COMPLETE, null);
}

调用了Request#action。action里则调用了Processor#processSocketEvent,并传入操作码OPEN_READ

case ASYNC_COMPLETE: {
    clearDispatches();
    if (asyncStateMachine.asyncComplete()) {
        processSocketEvent(SocketEvent.OPEN_READ, true);
    }
    break;
}

processSocketEvent

调用SocketWrapper#processSocket

protected void processSocketEvent(SocketEvent event, boolean dispatch) {
    SocketWrapperBase<?> socketWrapper = getSocketWrapper();
    if (socketWrapper != null) {
        socketWrapper.processSocket(event, dispatch);
    }
}
目录
相关文章
|
7月前
|
前端开发 应用服务中间件 C++
使用Servlet实现表白墙网站(前后端互联)小项目,Mac的M1(没有setting)在哪里找到Setting页面,下载smart tomcat及smart tomcat的配置。(二)
使用Servlet实现表白墙网站(前后端互联)小项目,Mac的M1(没有setting)在哪里找到Setting页面,下载smart tomcat及smart tomcat的配置。
使用Servlet实现表白墙网站(前后端互联)小项目,Mac的M1(没有setting)在哪里找到Setting页面,下载smart tomcat及smart tomcat的配置。(二)
|
5天前
|
XML 前端开发 Java
Tomcat和Servlet
Tomcat和Servlet
9 0
|
7月前
|
JSON 前端开发 JavaScript
使用Servlet实现表白墙网站(前后端互联)小项目,Mac的M1(没有setting)在哪里找到Setting页面,下载smart tomcat及smart tomcat的配置。(一)
使用Servlet实现表白墙网站(前后端互联)小项目,Mac的M1(没有setting)在哪里找到Setting页面,下载smart tomcat及smart tomcat的配置。
|
5天前
|
网络协议 前端开发 Java
异步Servlet学习笔记(一)
异步Servlet学习笔记(一)
|
5天前
|
前端开发 Java 应用服务中间件
HTTP&Tomcat&Servlet
HTTP&Tomcat&Servlet
60 0
|
7月前
|
Java 应用服务中间件 Linux
HTTPS && Tomcat && Servlet && 博客系统 && 软件测试的概念 && Linux
HTTPS && Tomcat && Servlet && 博客系统 && 软件测试的概念 && Linux
33 0
|
8月前
|
Java 数据库连接 应用服务中间件
java-初识Servlet,Tomcat,JDBC
我们在此文章知道了servlet 是基于Java语言编写的服务器端程序,可以处理Web容器(如Tomcat)发送过来的HTTP请求,也写了实例代码,另外servlet的过滤器,可以用来处理请求前与请求后的一些逻辑。;接着简单了介绍了tomcat,知道Tomcat 作为一个 Web 服务器,可以通过 Servlet 容器来管理和运行 Servlet;最后介绍jdbc,以及jdbc连接数据库的实例代码。
57 0
|
Java 应用服务中间件 容器
Tomcat怎么实现异步Servlet
有时Servlet在生成响应报文前必须等待某些耗时的操作,比如在等待一个可用的JDBC连接或等待一个远程Web服务的响应。
1513 0
|
5天前
|
XML Java 应用服务中间件
Tomcat_servlet部署、编译、配置、打包
Tomcat_servlet部署、编译、配置、打包
23 0
|
5天前
|
IDE Java 应用服务中间件
JDK1.6.0+Tomcat6.0的安装配置(配置JAVA环境)
JDK1.6.0+Tomcat6.0的安装配置(配置JAVA环境)
20 1