Springboot文件下载实现和原理分析
需求
- 客户端发送请求,可以下载服务端指定文件
- 无论什么文件,不允许浏览器自动解析,必须作为附件下载
分析
- 采用springboot实现文件下载,本质上使用的也是javaEE的Servlet+Tomcat技术
- 下载文件的本质是获取文件的读取流,在服务端需要将文件内容写入到Response的OutputStream中(注意这个写入流不需要flush)
- 为了防止浏览器解析,需要在响应头中,将文件类型设置为附件。
代码
/**
* 文件下载的controller
*/
@GetMapping("/download")
public void downloadFile(HttpServletResponse response) throws IOException {
downloadFileService.download(response);
}
/**
* 文件下载的service代码
*/
public void download(HttpServletResponse response) throws IOException {
/**
* 1、获取文件读取流
* 2、获取response写入流
* 3、将文件读取流的数据写入到写入流中
*/
File file = new File("d:\\01.jpg");
// 在常规的HTTP应答中,Content-Disposition 响应头指示回复的内容该以何种形式展示,
// 是以内联的形式(即网页或者页面的一部分), 还是以附件的形式下载并保存到本地。
// fileName不是必须的,用于作为文件下载之后的默认名称
response.setHeader("content-disposition", "attachment;filename=" + file.getName());
try (FileInputStream fileInputStream = new FileInputStream(file)) {
ServletOutputStream outputStream = response.getOutputStream();
byte[] bytes = new byte[1024 * 8];
int readBytes = 0;
while ((readBytes = fileInputStream.read(bytes)) != -1) {
outputStream.write(bytes, 0, readBytes);
}
}
}
下载过程Tomcat源码分析
通过http实现文件下载的原理是怎样的?
-
Servlet下载文件的核心代码是从某个输入流中读取数据,写到response的输出流中
ServletOutputStream outputStream = response.getOutputStream(); byte[] bytes = new byte[1024 * 8]; int readBytes = 0; while ((readBytes = fileInputStream.read(bytes)) != -1) { outputStream.write(bytes, 0, readBytes); }
-
从上面的代码中自然就带出第一个问题:输出流的数据目的地是哪里?数据被写到哪里去了?接下来通过debug跟write方法的源码来分析这个问题:
//首先调用write方法 outputStream.write(bytes, 0, readBytes); //然后调用org.apache.catalina.connector.CoyoteOutputStream的write方法 ob.write(b, off, len); //然后调用org.apache.catalina.connector.OutputBuffer的方法 writeBytes(b, off, len); //进一步调用org.apache.catalina.connector.OutputBuffer的方法 append(b, off, len);//Add data to the buffer //当达到一定条件时,将数据写到客户端的output中 realWriteBytes(ByteBuffer.wrap(src, off, limit)); //再进一步调用org.apache.coyote.http11.filters.ChunkedOutputFilter doWrite(ByteBuffer chunk) buffer.doWrite(chunk); //在进一步调用org.apache.tomcat.util.net.NioEndpoint doWrite(boolean block, ByteBuffer from) //继续调用org.apache.tomcat.util.net.NioSelectorPool,到这一步,已经快到了NIO的层面了 write(ByteBuffer buf, NioChannel socket, Selector selector, long writeTimeout) //继续调用org.apache.tomcat.util.net.NioBlockingSelector write(ByteBuffer buf, NioChannel socket, long writeTimeout) //进一步调用org.apache.tomcat.util.net.NioChannel socket.write(buf) //再进一步调用java.nio.channels.SocketChannel sc.write(src);//此时就调用到了java的nio包,这部分的源码好像就不开放了,只能看到反编译的内容
- 通过以上源码分析可以推断,数据从原始文件到客户端,从最上层的outputStream的write一直调用到socket的write,最底层的实现还是socket通信。而且数据不是一次性发送完成的,而是分成多个数据包发送,每次数据会首先缓存到io buffer中,当buffer达到临界值,就会调用socket的方法,把数据发送到客户端。
- 为了从效果上更加明显地验证数据的分包传输,可以直接把断点打在outputStream的write方法上,可以发现每执行几次write方法,客户端下载的临时文件就会变大。因此,第一个问题可以有比较靠谱的答案了。那就是outputStream的目标是io buffer,最终分批次通过socket写到客户端。
文件下载的buffer原理【高级】
通过分析ServletResponse下载文件的过程,可以得出以下非常有意思的结论:
- 定理1:文件下载的本质是网络数据传输,虽然表现为io流的方式,但是逐层跟源码可以发现,response io流的底层还是socket通信。
- 定理2:response的outputStream数据实际上是先写入到io buffer中,达到临界条件时,数据通过socket写到客户端去,如此循环,实现分批次传输。
- 定理3:文件下载不是一次性把一个文件写出去,而是按照buffer分批次传输,是一个流式的过程。
- 推论1:无论下载多大的文件,都不会导致服务内存溢出
- 推论2:由于下载文件的过程是按buffer分批次传输的,因此用中间服务代理底层存储不会导致较大的性能损失。