当前位置:   article > 正文

springboot后端实现断点续传(分片下载)_断点下载 是前端处理 还是接口处理

断点下载 是前端处理 还是接口处理

简介:
大家应该都听说过分片上传(断点上传),那么断点下载又是什么呢?其实完全可以按照上传的理解

来理解断点续传、分片下载。下载文件的时候将一个大文件分成N个部分进行下载,然后前端再进行组合。

最终得到一个完整的文件。

   但是呢,下载跟上传,后端的实现方式还是有区别的,上传需要把接口分成4个接口;但是下载不需要,
  • 1

一个接口搞定;主要依赖http的Range(关于range,网上资料应该不少)头来进行处理(其实个人还考虑过

另外一种方式,未验证不知道是否可行;方式就是后端将文件进行切割,然后提供一个接口告诉前端某个文

件有多少个分片,前端分别调用接口获取各个分片,然后将分片文件进行合并,此方式是参考到分片上传的

假想)。此方法同样支持普通下载,不传入Range头就可进行普通下载;也可一次只下载一段(传入一个

range:bytes=0-10240);也可下载多段(传入多个range:bytes=0-10240,10241-20480);也可一次下载完文件

(range范围为整个文件即可:bytes=0-102400);前端怎样配合实现完全不知道,如果有哪位大佬知道的话,

真心求教!下面开始进行代码的编码

实现:

  1. 下载接口实现:
/**
 * 文件下载
 * @author kevin
 * @param response :
 * @param range :
 * @param filePath :
 * @date 2021/1/17
 */
@ApiOperation(value = "文件下载", notes = "downloadFile")
@GetMapping(value = "/downloadFile")
public void downloadFile(@RequestParam("fileId") String fileId, @RequestParam(name = "filePath",
        required = false) String filePath, HttpServletResponse response,
        @RequestHeader(name = "Range", required = false) String range) {
 
    List<FileInfo> fileInfo= fileMapper.getFileById(fileId);
    if(null == fileInfo){
        throw new RuntimeException("下载失败,未找到需要下载的文件");
    }
    filePath = StringUtils.isNotBlank(filePath) ? filePath : fileInfo.getFilePath();
 
    File file = new File(filePath);
    String filename = file.getName();
    long length = file.length();
    Range full = new Range(0, length - 1, length);
    List<Range> ranges = new ArrayList<>();
    //处理Range
    try {
        if (!file.exists()) {
            String msg = "需要下载的文件不存在:" + file.getAbsolutePath();
            log.error(msg);
            throw new RuntimeException(msg);
        }
 
        if (file.isDirectory()) {
            String msg = "需要下载的文件的路径对应的是一个文件夹:" + file.getAbsolutePath();
            log.error(msg);
            throw new RuntimeException(ResponseState.REQUEST_ERROR.getCode(), msg);
        }            
        dealRanges(full, range, ranges, response, length);
    }catch (IOException e){
        e.printStackTrace();
        throw new RuntimeException("文件下载异常:" + e.getMessage());
    }
    // 如果浏览器支持内容类型,则设置为“内联”,否则将弹出“另存为”对话框. attachment inline
    String disposition = "attachment";
 
    // 将需要下载的文件段发送到客服端,准备流.
    try (RandomAccessFile input = new RandomAccessFile(file, "r");
         ServletOutputStream output = response.getOutputStream()) {
        //最后修改时间
        FileTime lastModifiedObj = Files.getLastModifiedTime(file.toPath());
        long lastModified = LocalDateTime.ofInstant(lastModifiedObj.toInstant(),
                ZoneId.of(ZoneId.systemDefault().getId())).toEpochSecond(ZoneOffset.UTC);
        //初始化response.
        response.reset();
        response.setBufferSize(20480);
        response.setHeader("Content-type", "application/octet-stream;charset=UTF-8");
        response.setHeader("Content-Disposition", disposition + ";filename=" +
                URLEncoder.encode(filename, StandardCharsets.UTF_8.name()));
        response.setHeader("Accept-Ranges", "bytes");
        response.setHeader("ETag", URLEncoder.encode(filename, StandardCharsets.UTF_8.name()));
        response.setDateHeader("Last-Modified", lastModified);
        response.setDateHeader("Expires", System.currentTimeMillis() + 604800000L);
        //输出Range到response
        outputRange(response, ranges, input, output, full, length);
        output.flush();
        response.flushBuffer();
    }catch (Exception e){
        e.printStackTrace();
        throw new RuntimeException("文件下载异常:" + e.getMessage());
    }
}
 
/**
  * 处理请求中的Range(多个range或者一个range,每个range范围)
  * @author kevin
  * @param range :
  * @param ranges :
  * @param response :
  * @param length :
  * @date 2021/1/17
  */
private void dealRanges(Range full, String range, List<Range> ranges, HttpServletResponse response,
                        long length) throws IOException {
    if (range != null) {
        // Range 头的格式必须为 "bytes=n-n,n-n,n-n...". 如果不是此格式, 返回 416.
        if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$")) {
            response.setHeader("Content-Range", "bytes */" + length);
            response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
            return;
        }
 
        // 处理传入的range的每一段.
        for (String part : range.substring(6).split(",")) {
            part = part.split("/")[0];
            // 对于长度为100的文件,以下示例返回:
            // 50-80 (50 to 80), 40- (40 to length=100), -20 (length-20=80 to length=100).
            int delimiterIndex = part.indexOf("-");
            long start = Range.sublong(part, 0, delimiterIndex);
            long end = Range.sublong(part, delimiterIndex + 1, part.length());
 
            //如果未设置起始点,则计算的是最后的 end 个字节;设置起始点为 length-end,结束点为length-1
            //如果未设置结束点,或者结束点设置的比总长度大,则设置结束点为length-1
            if (start == -1) {
                start = length - end;
                end = length - 1;
            } else if (end == -1 || end > length - 1) {
                end = length - 1;
            }
 
            // 检查Range范围是否有效。如果无效,则返回416.
            if (start > end) {
                response.setHeader("Content-Range", "bytes */" + length);
                response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
                return;
            }
            // 添加Range范围.
            ranges.add(new Range(start, end, end - start + 1));
        }
    }else{
        //如果未传入Range,默认下载整个文件
        ranges.add(full);
    }
}
 
 
 
/**
 * output写流输出到response
 * @author kevin
 * @param response :
 * @param ranges :
 * @param input :
 * @param output :
 * @param full :
 * @param length :
 * @date 2021/1/17
 */
private void outputRange(HttpServletResponse response, List<Range> ranges, RandomAccessFile input,
                       ServletOutputStream output, Range full, long length) throws IOException {
    if (ranges.isEmpty() || ranges.get(0) == full) {
        // 返回整个文件.
        response.setContentType("application/octet-stream;charset=UTF-8");
        response.setHeader("Content-Range", "bytes " + full.start + "-" + full.end + "/" + full.total);
        response.setHeader("Content-length", String.valueOf(full.length));
        response.setStatus(HttpServletResponse.SC_OK); // 200.
        Range.copy(input, output, length, full.start, full.length);
    } else if (ranges.size() == 1) {
        // 返回文件的一个分段.
        Range r = ranges.get(0);
        response.setContentType("application/octet-stream;charset=UTF-8");
        response.setHeader("Content-Range", "bytes " + r.start + "-" + r.end + "/" + r.total);
        response.setHeader("Content-length", String.valueOf(r.length));
        response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.
        // 复制单个文件分段.
        Range.copy(input, output, length, r.start, r.length);
    } else {
        // 返回文件的多个分段.
        response.setContentType("multipart/byteranges; boundary=MULTIPART_BYTERANGES");
        response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.
 
        // 复制多个文件分段.
        for (Range r : ranges) {
            //为每个Range添加MULTIPART边界和标题字段
            output.println();
            output.println("--MULTIPART_BYTERANGES");
            output.println("Content-Type: application/octet-stream;charset=UTF-8");
            output.println("Content-length: " + r.length);
            output.println("Content-Range: bytes " + r.start + "-" + r.end + "/" + r.total);
            // 复制多个需要复制的文件分段当中的一个分段.
            Range.copy(input, output, length, r.start, r.length);
        }
 
        // 以MULTIPART文件的边界结束.
        output.println();
        output.println("--MULTIPART_BYTERANGES--");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178

参考文章:http://blog.ncmem.com/wordpress/2023/11/16/springboot%e5%90%8e%e7%ab%af%e5%ae%9e%e7%8e%b0%e6%96%ad%e7%82%b9%e7%bb%ad%e4%bc%a0%e5%88%86%e7%89%87%e4%b8%8b%e8%bd%bd/
欢迎入群一起讨论

在这里插入图片描述

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/528078
推荐阅读
相关标签
  

闽ICP备14008679号