【问题标题】:Servlet buffering response despite calls to flush()尽管调用了 flush(),但 Servlet 缓冲响应
【发布时间】:2011-10-22 12:36:55
【问题描述】:

我们有一个系统,其中客户端发出 HTTP GET 请求,系统在后端进行一些处理,压缩结果并将其发送给客户端。由于处理可能需要一些时间,我们将其作为 ZipOutputStream 包装 response.getOutputStream() 发送。

但是,当我们在第一个ZipEntry 中的数据量非常少,而第二个条目需要很长时间时,客户端正在使用的浏览器就会超时。我们已经尝试刷新流缓冲区,但在至少有 1000 个字节被写入流之前,似乎没有响应发送到浏览器。奇怪的是,一旦发送了前 1000 个字节,随后的刷新似乎工作正常。

我尝试将代码简化为示例:

protected void doGet(HttpServletRequest request,
        HttpServletResponse response) throws ServletException, IOException {
    try {
        ZipOutputStream _zos = new ZipOutputStream( response.getOutputStream());
        ZipEntry _ze = null;
        long startTime = System.currentTimeMillis();
        long _lByteCount = 0;

        response.setContentType("application/zip");

        while (_lByteCount < 2000) {
            _ze = new ZipEntry("foo");
            _zos.putNextEntry( _ze );

            //writes 100 bytes and then waits 10 seconds
            _lByteCount += StreamWriter.write( 
                    new ByteArrayInputStream(DataGenerator.getOutput().toByteArray()),
                    _zos );
            System.out.println("Zip: " + _lByteCount + " Time: " + ((System.currentTimeMillis() - startTime) / 1000));

            //trying to flush
            _zos.finish();
            _zos.flush();
            response.flushBuffer();
            response.getOutputStream().flush();
        }
    } catch (Throwable e) {
        e.printStackTrace();
    }
}

我将浏览器超时设置为大约 20 秒,以便于复制。尽管多次写入 100 字节,但没有任何内容发送到浏览器并且浏览器超时。如果我扩大浏览器超时,则在写入 1000 个字节之前不会发送任何内容,然后浏览器会弹出“您要保存...”对话框。同样,在最初的 1000 字节之后,每增加 100 字节就可以正常发送,而不是缓冲到 1000 字节块。

如果我将 while 条件中的最大字节数设置为 200 左右,它工作正常,只发送 200 个字节。

我能做些什么来强制 servlet 发回非常少量的初始数据?

【问题讨论】:

  • 您在什么操作系统下运行?我见过一个类似的问题,报告的 servlet 不会返回任何内容,除非输出的大小大于 1024 字节。通过查看 JavaDocs,它可能与操作系统如何处理其缓冲区有关。
  • Win7.我们最终决定它不在应用程序端,并强制应用程序只接受具有一定数据量的请求。
  • 我相信这可能是一个应用服务器问题。请参阅my post

标签: java tomcat servlets


【解决方案1】:

事实证明,为了提高效率,底层 Apache/Windows IP 堆栈缓冲来自流的数据是有限制的。由于大多数人都有太多数据的问题,而不是太少数据的问题,所以大多数时候这是对的。我们最终做的是要求用户请求足够的数据,以便我们在超时之前达到 1000 字节的限制。很抱歉花了这么长时间来回答这个问题。

【讨论】:

  • 如果您对此有任何参考,我肯定希望看到它们。我面临着类似的问题,因此最好了解有关如何/为什么成为问题的更多信息。
【解决方案2】:

我知道这是一个非常非常古老的问题,但为了记录,我想发布一个答案,应该可以解决您遇到的问题。

关键是您要刷新响应流,而不是 zip 流。因为 ZIP 流无法刷新尚未准备好写入的内容。正如您所提到的,您的客户端正在超时,因为它没有在预定的时间内收到响应,但是一旦它收到数据,它就会耐心等待很长时间才能下载文件,因此修复很容易,只要您刷新正确的流。我推荐以下:

protected void doGet(HttpServletRequest request,
    HttpServletResponse response) throws ServletException, IOException {
try {
    ZipOutputStream _zos = new ZipOutputStream( response.getOutputStream());
    ZipEntry _ze = null;
    long startTime = System.currentTimeMillis();
    long _lByteCount = 0;

    response.setContentType("application/zip");
    // force an immediate response of the expected content
    // so the client can begin the download process
    response.flushBuffer();

    while (_lByteCount < 2000) {
        _ze = new ZipEntry("foo");
        _zos.putNextEntry( _ze );

        //writes 100 bytes and then waits 10 seconds
        _lByteCount += StreamWriter.write( 
                new ByteArrayInputStream(DataGenerator.getOutput().toByteArray()),
                _zos );
        System.out.println("Zip: " + _lByteCount + " Time: " + ((System.currentTimeMillis() - startTime) / 1000));

        //trying to flush
        _zos.finish();
        _zos.flush();
    }
} catch (Throwable e) {
    e.printStackTrace();
}

现在,这里应该发生的是,标头和响应代码将与响应缓冲区的 OutputStream 中的任何内容一起提交。这不会关闭流,因此会附加对流的任何其他写入。这样做的缺点是您无法知道分配给标题的内容长度。积极的一面是您立即开始下载,并且不允许浏览器超时。

【讨论】:

    【解决方案3】:

    我的猜测是 zip 输出流在能够压缩内容之前实际上并没有写任何东西。用于压缩的 Huffmann 算法要求在实际能够压缩任何内容之前知道所有数据。在一切基本都知道之前,它不能开始。

    如果数据量很大,压缩可能是一个胜利,但我认为你不能在压缩数据时实现异步响应。

    【讨论】:

    • 我认为也可能是这种情况,但我可以使用FilterOutputStream 重现这一点。此外,由于每次写入都是一个单独的ZipEntry,并且它们是单独编码的,因此它不应该等待更多的压缩,因为流有一个完整的条目。最后,它没有解释为什么它在前 1000 个字节之后突然开始发送 100 个字节的块
    • 因为霍夫曼算法需要实际计算用于压缩的代码的“字母”。这可能就是您等待第一个块的原因。之后,它可以逐步编码所有数据。
    • 感谢您的回复,但您是否错过了将ZipOutputStream 更改为FilterOutputStream 时出现相同问题的部分?我什至可以使用响应输出流重现这种行为,无论是发送二进制数据还是文本数据。我认为这个问题与压缩数据无关,这正是我遇到问题的地方。
    • @Snicolas:请read this 并考虑修复您之前的所有答案。
    • @BalusC,抱歉我没听明白。签名?
    【解决方案4】:

    我完全无法重现您的问题。下面是您的代码,稍作改动,在嵌入式 Jetty 服务器中运行。我在 IntelliJ 中运行它并从 Firefox 请求 http://localhost:8080。正如预期的那样,“保存或打开”对话框在 1 秒后弹出。选择“保存”并等待 20 秒会生成一个 zip 文件,该文件可以打开并包含 20 个单独的条目,名为 foo,每个条目包含一行 100 个字符宽并以 结尾。这是在带有 JDK 1.6.0_26 的 Windows 7 Premium 64 上。 Chrome 的行为方式相同。另一方面,IE 似乎通常等待 5 秒(500 字节),尽管一旦它立即显示对话框,而另一次它似乎等待 9 或 10 秒。在不同的浏览器中尝试:

    import org.eclipse.jetty.server.Server;
    import org.eclipse.jetty.servlet.ServletContextHandler;
    import org.eclipse.jetty.servlet.ServletHolder;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.*;
    import java.io.IOException;
    import java.util.zip.ZipEntry;
    import java.util.zip.ZipOutputStream;
    
    public class ZippingAndStreamingServlet {
        public static void main(String[] args) throws Exception {
            Server server = new Server(8080);
            ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
            context.setContextPath("/");
            server.setHandler(context);
            context.addServlet(new ServletHolder(new BufferingServlet()), "/*");
            server.start();
            System.out.println("Listening on 8080");
            server.join();
        }
    
        static class BufferingServlet extends HttpServlet {
            protected void doGet(HttpServletRequest request,
                                 HttpServletResponse response) throws ServletException, IOException {
                ZipOutputStream _zos = new ZipOutputStream(response.getOutputStream());
                ZipEntry _ze;
                long startTime = System.currentTimeMillis();
                long _lByteCount = 0;
                int count = 1;
                response.setContentType("application/zip");
                response.setHeader("Content-Disposition", "attachment; filename=my.zip");
                while (_lByteCount < 2000) {
                    _ze = new ZipEntry("foo" + count);
                    _zos.putNextEntry(_ze);
                    byte[] bytes = String.format("%100d", count++).getBytes();
                    System.out.println("Sending " + bytes.length + " bytes");
                    _zos.write(bytes);
                    _lByteCount += bytes.length;
                    sleep(1000);
                    System.out.println("Zip: " + _lByteCount + " Time: " + ((System.currentTimeMillis() - startTime) / 1000));
                    _zos.flush();
                }
                _zos.close();
            }
    
            private void sleep(int millis) {
                try {
                    Thread.sleep(millis);
                } catch (InterruptedException e) {
                    throw new IllegalStateException("Unexpected interrupt!", e);
                }
            }
        }
    }
    

    【讨论】:

      【解决方案5】:

      您可能被 Java API 搞砸了。

      查看各种 OutputStream 系列类(OutputStreamServletOutputStreamFilterOutputStreamZipOutputStream)的 JavaDocs,他们要么提到他们依赖底层流进行 flush(),要么他们声明那个 flush() 没有做任何事情(OutputStream)。

      ZipOutputStream 继承自 FilterOutputStream 的 flush() 和 write()。

      来自 FilterOutputStream JavaDoc:

      FilterOutputStream 的flush 方法调用它的flush 方法 底层输出流。

      在 ZipOutputStream 的情况下,它被包裹在从 ServletResponse.getOutputStream() 返回的流中,这是一个 ServletOutputStream。事实证明,ServletOutputStream 也没有实现 flush(),它从 OutputStream 继承,在其 JavaDoc 中特别提到:

       flush public void flush()
                  throws IOExceptionFlushes 
       this output stream and forces any
       buffered output bytes to be written out. The general contract of flush
       is that calling it is an indication that, if any bytes previously
       written have been buffered by the implementation of the output stream,
       such bytes should immediately be written to their intended
       destination.  If the intended destination of this stream is an
       abstraction provided by the underlying operating system, for example a
       file, then flushing the stream guarantees only that bytes previously
       written to the stream are passed to the operating system for writing;
       it does not guarantee that they are actually written to a physical
       device such as a disk drive. 
      
        **The flush method of OutputStream does nothing.**
      

      也许这是个特例,我不知道。我确实知道 flush() 已经存在了很长时间,而且不太可能没有人注意到那里的功能漏洞。

      这让我想知道流缓冲是否有一个操作系统组件可以配置为消除 1k 缓冲效应。

      related question 有一个类似的问题,但它直接使用文件而不是来自 Java 的 Stream 抽象,this answer 指向 MSDN 文章中涉及的有关 file bufferingfile caching 的文章。

      错误数据库中列出了similar scenario

      总结

      Java IO 库依赖于 Streams 的操作系统实现。如果操作系统打开了缓存,Java 代码可能无法强制执行不同的行为。在 Windows 的情况下,您必须打开文件并发送非标准参数以允许 write-through-cache 或无缓冲功能。我怀疑 Java SDK 是否提供了此类特定于操作系统的选项,因为它们正在尝试创建平台通用 API。

      【讨论】:

      • 不,正如 Snicolas 回答的那样,压缩流需要 整个 流完全在内存中。在两者之间冲洗是不可能/不建议的。
      • @BalusC - 我指的是他使用 FilterOutputStream 时的场景。他需要将小页面数据集作为自己的测试用例,以便隔离不同的层(浏览器/服务器)。
      • 哦,我明白了,您基本上是在评论评论而不是回答问题。
      • 我在 IE9、FF5 和 Chrome 中遇到了同样的问题;它似乎与浏览器无关。我最近尝试了 Firebug,问题似乎与我描述的一样,但我也会尝试 Fiddler 并报告。
      • 刚刚检查过 Fiddler。我从服务器收到一个响应,但它只是标题数据 - 没有任何实际数据返回,导致浏览器超时。如果我延长超时,我会得到我之前看到的行为,没有数据从服务器返回,直到达到任意数量并且它实际上刷新。用FilterOutputStream 尝试了这个,以防止任何担心压缩是错误(尽管我怀疑这是开始的错误)。
      【解决方案6】:

      问题在于,默认情况下,每个 servlet 实现都会缓冲数据,而 SSE 和其他自定义需求可能/将立即需要数据。

      解决办法如下:

      response.setBufferSize(1) // or some similar small number for such servlets. 
      

      这将确保更早地写出数据(导致性能损失)

      【讨论】:

        猜你喜欢
        • 2014-06-29
        • 2015-09-28
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-07-06
        • 1970-01-01
        • 1970-01-01
        • 2011-05-10
        相关资源
        最近更新 更多