springboot后端实现断点续传(分片下载)

2022年05月12日 阅读数:2
这篇文章主要向大家介绍springboot后端实现断点续传(分片下载),主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

简介:

       你们应该都据说过度片上传(断点上传),那么断点下载又是什么呢?其实彻底能够按照上传的理解前端

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

最终获得一个完整的文件。后端

       可是呢,下载跟上传,后端的实现方式仍是有区别的,上传须要把接口分红4个接口;可是下载不须要,浏览器

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

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

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

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

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

(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--");
        }
    }

2.Range类 

private static class Range {
        long start;
        long end;
        long length;
        long total;

        /**
         * Range段构造方法.
         *
         * @param start range起始位置.
         * @param end   range结束位置.
         * @param total range段的长度.
         */
        public Range(long start, long end, long total) {
            this.start = start;
            this.end = end;
            this.length = end - start + 1;
            this.total = total;
        }

        public static long sublong(String value, int beginIndex, int endIndex) {
            String substring = value.substring(beginIndex, endIndex);
            return (substring.length() > 0) ? Long.parseLong(substring) : -1;
        }

        private static void copy(RandomAccessFile randomAccessFile, OutputStream output, long fileSize, long start, long length) throws IOException {
            byte[] buffer = new byte[4096];
            int read = 0;
            long transmitted = 0;
            if (fileSize == length) {
                randomAccessFile.seek(start);
                //须要下载的文件长度与文件长度相同,下载整个文件.
                while ((transmitted + read) <= length && (read = randomAccessFile.read(buffer)) != -1){
                    output.write(buffer, 0, read);
                    transmitted += read;
                }
                //处理最后不足buff大小的部分
                if(transmitted < length){
                    log.info("最后不足buff大小的部分大小为:" + (length - transmitted));
                    read = randomAccessFile.read(buffer, 0, (int)(length - transmitted));
                    output.write(buffer, 0, read);
                }
            } else {
                randomAccessFile.seek(start);
                long toRead = length;

                //若是须要读取的片断,比单次读取的4096小,则使用读取片断大小读取
                if(toRead < buffer.length){
                    output.write(buffer, 0, randomAccessFile.read(new byte[(int) toRead]));
                    return;
                }
                while ((read = randomAccessFile.read(buffer)) > 0) {
                    toRead -= read;
                    if (toRead > 0) {
                        output.write(buffer, 0, read);
                    } else {
                        output.write(buffer, 0, (int) toRead + read);
                        break;
                    }
                }

            }
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Range range = (Range) o;
            return start == range.start &&
                    end == range.end &&
                    length == range.length &&
                    total == range.total;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            return Objects.hash(prime, start, end, length, total);
        }
    }