使用 Servlet3.1 WriteListener进行异步响应输出以及踩过的坑

在标准的spring mvc项目中,当对象数据已经产生时,不管是输出json,还是view,最终均是将相应的数据直接写入response的outputStream中。这里的输出是同步式调用的,即意味着只有当整个数据完整地写到输出流中时,整个业务线程才会被认为执行结束。

这就意味着在传统的基于请求的慢速攻击之外,还有一种基于响应的慢速攻击,其思路即是客户端强制让整个接收变慢,而服务端因为缓存区不足以存储完整的响应数据,而被动地阻塞调用端,进而阻塞业务线程。

在Servlet 3.0版本中,提出了业务中的异步处理,即 通过 HttpServletRequest#startAsync 来开启异步处理。等业务处理完成之后,才通过 AsyncContext#complete 来结束处理。在中间,可以仍然通过 HttpServletResponse#getOutputStream 来输出数据. 这里的异步处理仅仅是将容器的处理线程解放出来,即当业务处理需要长时间或者需要故意挂起请求(如LongPull)时,才有相应的意义。在最终输出数据时,这里仍然是阻塞的,只不过是阻塞业务线程(而非容器线程).造成的后果即是缓慢地IO操作造成线程阻塞。

随着 NIO 的出现,所有Web容器均使用了Nio来处理请求,即当整个请求已经完整读取完毕之后,才转交由具体的业务线程来处理,即在这里的io读取均为非阻塞读取,由很少数的线程就可以处理上百(千)个请求的网络请求. 在 Servlet 3.1 版本中, 规范中给出了 异步响应的概念,即可以通过响应式来达到异步非阻塞输出数据的效果. 从调用端来看,调用write之后即刻返回,并不会阻塞业务线程。后续未刷新到缓冲区的数据将在后续通过标准Nio流程由web容器的IO线程负责处理.

一个参考使用例子如下所示(参考于 https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/HTML5andServlet31/HTML5andServlet%203.1.html):

//开启异步
val context = request.startAsync();
val response = context.getResponse();
val servletOut = response.getOutputStream();

//定义响应监听器,以可写时输出相应的数据
val writeListener = new WriteListenerImpl(context, servletOut, actionQueue);
servletOut.setWriteListener(writeListener);

//一个参考的监听器实现
public class WriteListenerImpl implements WriteListener {
    private ServletOutputStream output = null;
    private Queue queue = null;
    private AsyncContext context = null;

    private boolean flag = false;

    @Override
    public void onWritePossible() throws IOException {
        if (flag) {
            return;
        }

        //这里仅当output.isReady() 时才会调用write方法,否则会报 IllegalStateException
        while (queue.peek() != null && output.isReady()) {
            val action = queue.poll();
            doWrite(action);
        }
    }

    private void doWrite(Action action) throws IOException {
        //已完成标记位
        if (action == Action.COMPLETE) {
            flag = true;
        }

        action.run(context, servletOut);
    }
}
继续阅读“使用 Servlet3.1 WriteListener进行异步响应输出以及踩过的坑”

Jetty中如何实现servlet的异步Request调用

在servlet3.1规范当中,已经支持在servlet中通过调用request.startAsync来开启1个异步调用,然后在相应的业务线程里面进行一些业务操作,再通过asyncContext.complete即完成业务的整个操作。一个参考的demo如下所示:

    val context = req.startAsync();
    //重新设置业务超时时间
    context.setTimeout(40_000);

    Runnable runnable = () -> {
        try{
            //执行你的业务操作

            //输出数据
            context.getResponse().getWriter().println(totalMoney);

            //完成业务
            context.complete();
        } catch(Exception e) {
            e.printStackTrace();
        }
    };
    
    new Thread(runnable).start();

在上面的参考中,原来的servlet在调用完 thread.start之后,相应的逻辑即完成。相应的容器线程则已经还给线程池,此线程即可以接收其它客户端的请求并处理了. 异步servlet的目的即在于将接收请求的io线程与实际的业务执行相分开,避免过慢的业务阻塞了整个容器,而不能再接收更多的请求了.甚至可以在后端的业务池中定义一个队列,将要进行执行的业务逻辑放入队列里面慢慢执行。

由于异步Servlet的目的在于将web容器的io线程与业务线程分离,那么关键的部分即在于当servlet方法执行完之后,当context.complete时,如何重新触发相应io流的操作。本文尝试以jetty为参考,从源码角度查看其实现的原理和机制。

本文参考的jetty版本为 9.4.11.v20180605

继续阅读“Jetty中如何实现servlet的异步Request调用”

(转)Servlet 工作原理解析(以tomcat7为基础)

     Web 技术成为当今主流的互联网 Web 应用技术之一,而 Servlet 是 Java Web 技术的核心基础。因而掌握 Servlet 的工作原理是成为一名合格的 Java Web 技术开发人员的基本要求。本文将带你认识 Java Web 技术是如何基于 Servlet 工作,你将知道:以 Tomcat 为例了解 Servlet 容器是如何工作的?一个 Web 工程在 Servlet 容器中是如何启动的? Servlet 容器如何解析你在 web.xml 中定义的 Servlet ?用户的请求是如何被分配给指定的 Servlet 的? Servlet 容器如何管理 Servlet 生命周期?你还将了解到最新的 Servlet 的 API 的类层次结构,以及 Servlet 中一些难点问题的分析。
    本文转自 许令波(来自淘宝),原文为 Servlet 工作原理解析 

继续阅读“(转)Servlet 工作原理解析(以tomcat7为基础)”