springboot后端实现断点续传(分片下载)
  9m65el8SCpbP 2023年11月30日 28 0

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

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

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

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

一个接口搞定;主要依赖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--");
        }
    }

参考文章:http://blog.ncmem.com/wordpress/2023/11/16/springboot后端实现断点续传分片下载/


【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月30日 0

暂无评论

推荐阅读
9m65el8SCpbP