以前接触到的文件下载基本上都是实时读取的,比如我们直接将一些服务器端的文件做输出,或是导出一些计算量不太大的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);
// 发送数据并关闭链接
outputStream.flush(); ➊
outputStream.close();
上述代码将导致执行到➊时,浏览器端才能够接收到响应的header信息。也就说是:只有后台的代码执行到➊时,才会触发浏览器弹出保存文件的对话框。
正确的方法如下:
ServletOutputStream outputStream = httpServletResponse.getOutputStream();
// 设置响应头 httpServletResponse.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;");
httpServletResponse.setHeader("Content-Disposition", "attachment;filename=text.xlsx"
outputStream.flush(); ➋
// 耗时的下载
Thread.sleep(10000);
// 发送数据并关闭链接
outputStream.flush();
outputStream.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.
通过阅读官方文档我们发现有两种禁用该缓存的方法:
- 设置proxy_buffering的值为off
- 在返回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转发的前提下,浏览器实时的弹出保存文件对话框的目的。
希望能对你有所帮助。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。