乐趣区

关于nginx:ServletOutputStream在nginx转发下输出文件下载的一种方法

以前接触到的文件下载基本上都是实时读取的,比方咱们间接将一些服务器端的文件做输入,或是导出一些计算量不太大的 excel,所以没有太注意文件下载的细节。

昨天坐了一个数据导出,因为导出的数据量还不算小,而后在导出的过程中还须要做少许的运行,导致下载的时候大概须要 5 分钟左右。这时候以前没有留神到的细节便浮出了水面。

本次的问题次要呈现在浏览器端未及时弹出文件下载框,这给了用户一种下载页面没法关上的假象。

文件流

HTTP 在进行连贯时,会接管到响应头与响应主体。依据 HTTP 协定的形容,响应头与主体间应用空行来分隔。当响应的内容比拟大时,服务器先把响应的内容由上至下的发送给客户端。这更像数据结构中的队列,header 头信息发入队,body 的主体的信息后入队,而后因为网速的限度,没有方法一次性将队列中的内容全副发送结束,所以在发送时便应用了先进先出的准则,将位于队头的 header 的信息先发送给客户端。

浏览器做为客户端,接管到 http 的 header 头信息后,便能够得悉后盾将发送一个大的文件给咱们,而后弹出保留文件操作的对话框。

弹出文件下载框

所以下载大的文件时,如果想让浏览器及时的弹出下载对话框,最要害的就是让浏览器及时的收到相干的 header 信息。

故以下的代码是谬误的:

    ServletOutputStream outputStream = httpServletResponse.getOutputStream();
   // 设置响应头 httpServletResponse.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;");
    httpServletResponse.setHeader("Content-Disposition", "attachment;filename=text.xlsx"

    // 模仿耗时的下载
    Thread.sleep(10000);

    // 发送数据并敞开链接
    httpServletResponse.flush(); ➊
    httpServletResponse.close();

上述代码将导致执行到➊时,浏览器端才可能接管到响应的 header 信息。也就说是:只有后盾的代码执行到➊时,才会触发浏览器弹出保留文件的对话框。

正确的办法如下:

    ServletOutputStream outputStream = httpServletResponse.getOutputStream();
   // 设置响应头 httpServletResponse.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;");
    httpServletResponse.setHeader("Content-Disposition", "attachment;filename=text.xlsx"
    httpServletResponse.flush(); ➋

    // 耗时的下载
    Thread.sleep(10000);

    // 发送数据并敞开链接
    httpServletResponse.flush();
    httpServletResponse.close();

此时代码执行到➋时,浏览器便接管到了必要的 header 头信息,进而触发其弹出对话框。

本认为曾经高枕无忧,然而用很多老手的话说:浏览器 就是 不弹出对话框。其实,浏览器不弹出对话框必然是咱们还没有弄明确的,不存在 就是 一说。咱们在发问时,如果退出了 就是,往往阐明本人的心态曾经解体了。

NGINX

排除这种 就是 的问题,往往还须要简化环境,一层层的把一些环境扔掉,看看是否依然报错。通过尝试我发现原来是本人应用 nginx 反向代理的起因导致 header 的信息没有被浏览器及时的接管,所以我大胆的猜想应该是 nginx 做了数据缓存。

因为发送的 header 的数据量比拟小,而后 NGINX 出于某些效率的起因,并没有抉择实时地将数据转发给浏览器,这导致了尽管后盾将 HEADER 头信息发送了进去,但却没有被浏览器接管到,所以浏览器便没有实时的弹出下载的对话框。

有了大略的方向后,通过 google 查问发现 nginx 确实默认有缓存性能。而后找到了相应的官网文档中对于 proxy_buffer 的一节。内容如下:


Syntax:    proxy_buffering on | off;
Default:    
proxy_buffering on;
Context:    http, server, location

Enables or disables buffering of responses from the proxied server.

When buffering is enabled, nginx receives a response from the proxied server as soon as possible, saving it into the buffers set by the proxy_buffer_size and proxy_buffers directives. If the whole response does not fit into memory, a part of it can be saved to a temporary file on the disk. Writing to temporary files is controlled by the proxy_max_temp_file_size and proxy_temp_file_write_size directives.

When buffering is disabled, the response is passed to a client synchronously, immediately as it is received. nginx will not try to read the whole response from the proxied server. The maximum size of the data that nginx can receive from the server at a time is set by the proxy_buffer_size directive.

Buffering can also be enabled or disabled by passing“yes”or“no”in the“X-Accel-Buffering”response header field. This capability can be disabled using the proxy_ignore_headers directive.


通过浏览官网文档咱们发现有两种禁用该缓存的办法:

  1. 设置 proxy_buffering 的值为 off
  2. 在返回 header 时,减少一项X-Accel-Buffering,设置值为no

通过测试发现两种状况均能够失常工作。思考到 nginx 的缓存机制必然有它本人的情理,所以在这里咱们采纳第二种:在返回 header 时,减少一项X-Accel-Buffering,设置值为no

    ServletOutputStream outputStream = httpServletResponse.getOutputStream();
    httpServletResponse.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");
    httpServletResponse.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));
    httpServletResponse.setHeader("X-Accel-Buffering", "no");
    outputStream.flush();

至此,便能够欢快的出工了。

总结

下载大文件未及时弹出对话框的问题,使我意识到了 flush()办法的作用,也如同明确了为什么 response 中只有 set 办法,而没有 clear 办法。同时这还使我大胆地猜想:一旦调用了 response 中的 write()办法后再调用 setHeader 办法,则应该会报一个谬误或是异样。同时理解了 nginx 为了某些不晓得的起因,主动启用了缓存,这应该是一种优良的机制,所以在解决办法中咱们并没有间接将其敞开。而是抉择了另一种自定义 header 值的办法,该办法来告之 nginx:这里的数据不须要缓存,请间接发送给客户端。从而达到了在 nginx 转发的前提下,浏览器实时的弹出保留文件对话框的目标。

心愿能对你有所帮忙。

退出移动版