Springboot文件下载实现和原理分析

Springboot文件下载实现和原理分析

需求
  1. 客户端发送请求,可以下载服务端指定文件
  2. 无论什么文件,不允许浏览器自动解析,必须作为附件下载
分析
  1. 采用springboot实现文件下载,本质上使用的也是javaEE的Servlet+Tomcat技术
  2. 下载文件的本质是获取文件的读取流,在服务端需要将文件内容写入到Response的OutputStream中(注意这个写入流不需要flush)
  3. 为了防止浏览器解析,需要在响应头中,将文件类型设置为附件。
代码
/**
 * 文件下载的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实现文件下载的原理是怎样的?

  1. 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);
    }
    
  2. 从上面的代码中自然就带出第一个问题:输出流的数据目的地是哪里?数据被写到哪里去了?接下来通过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包,这部分的源码好像就不开放了,只能看到反编译的内容
    
    1. 通过以上源码分析可以推断,数据从原始文件到客户端,从最上层的outputStream的write一直调用到socket的write,最底层的实现还是socket通信。而且数据不是一次性发送完成的,而是分成多个数据包发送,每次数据会首先缓存到io buffer中,当buffer达到临界值,就会调用socket的方法,把数据发送到客户端。
    2. 为了从效果上更加明显地验证数据的分包传输,可以直接把断点打在outputStream的write方法上,可以发现每执行几次write方法,客户端下载的临时文件就会变大。因此,第一个问题可以有比较靠谱的答案了。那就是outputStream的目标是io buffer,最终分批次通过socket写到客户端。
文件下载的buffer原理【高级】

通过分析ServletResponse下载文件的过程,可以得出以下非常有意思的结论:

  1. 定理1:文件下载的本质是网络数据传输,虽然表现为io流的方式,但是逐层跟源码可以发现,response io流的底层还是socket通信。
  2. 定理2:response的outputStream数据实际上是先写入到io buffer中,达到临界条件时,数据通过socket写到客户端去,如此循环,实现分批次传输。
  3. 定理3:文件下载不是一次性把一个文件写出去,而是按照buffer分批次传输,是一个流式的过程。
    1. 推论1:无论下载多大的文件,都不会导致服务内存溢出
    2. 推论2:由于下载文件的过程是按buffer分批次传输的,因此用中间服务代理底层存储不会导致较大的性能损失。
  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值