ToB企服应用市场:ToB评测及商务社交产业平台

标题: Spring Boot 实现文件断点下载,实战来了! [打印本页]

作者: 忿忿的泥巴坨    时间: 2023-7-26 11:51
标题: Spring Boot 实现文件断点下载,实战来了!
来源:juejin.cn/post/7026372482110079012
前言

互联网的连接速度慢且不稳定,有可能由于网络故障导致断开连接。
在客户端下载一个大对象时,因网络断开导致上传下载失败的概率就会变得不可忽视。

客户端在GET对象请求时通过设置Range头部来告诉接口服务需要从什么位置开始输出对象的数据。
判断是否支持断点下载,根据文档:14.35.1 Byte Rangeshttps://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
  1. // 直接判断是否有 Accept-Ranges = bytes
  2. boolean support = urlConnection.getHeaderField("Accept-Ranges").equals("bytes");
  3. System.out.println("Partial content retrieval support = " + (support ? "Yes" : "No));
复制代码
例如:
  1. donald@donald-pro:~$ curl -i --range 0-9 http://localhost:8080/file/chunk/download
  2. HTTP/1.1 206
  3. Accept-Ranges: bytes
  4. Content-Disposition: inline;filename=pom.xml
  5. Content-Range: bytes 0-9/13485
  6. Content-Length: 10
  7. Date: Mon, 01 Nov 2021 09:53:25 GMT
复制代码
直接判断头部 HEAD,例如:
HeadObject 接口用于获取某个文件(Object)的元信息。使用此接口不会返回文件内容。
  1. HEAD /ObjectName HTTP/1.1
  2. Host: BucketName.oss-cn-hangzhou.aliyuncs.com
  3. Date: GMT Date
  4. Authorization: SignatureValue
复制代码
需知,对应 HTTP 状态码:
小结如下:
生产实战

开发也得依靠依据,设定好边界,才能掌控全局。
有现成的文档,来看阿里云文档https://help.aliyun.com/document_detail/39571.html
HTTP Range 是否合法对应处理:
如下为 HTTP Range 请求不合法的示例及错误说明: 假设 Object 资源大小为1000字节,Range 有效区间为0~999
举一些栗子:
  1. # 正常范围下载
  2. donald@donald-pro:~$ curl -i --range 0-9 http://localhost:8080/file/chunk/download
  3. HTTP/1.1 206
  4. Accept-Ranges: bytes
  5. Content-Disposition: inline;filename=Screen_Recording_20211101-162729_Settings.mp4
  6. Content-Range: bytes 0-9
  7. Content-Type: application/force-download;charset=UTF-8
  8. Content-Length: 16241985
  9. Date: Wed, 03 Nov 2021 09:50:50 GMT
复制代码
服务端 - 业务开发

这里以 SpringBoot 为栗子,Spring Boot 基础就不介绍了,推荐看这个实战项目:
https://github.com/javastacks/spring-boot-best-practice
  1. @Slf4j
  2. @RestController
  3. public class Controller {
  4.     @Autowired
  5.     private FileService fileService;
  6.     /**
  7.      * 下载文件
  8.      *
  9.      * 对外提供
  10.      *
  11.      * @param fileId 文件Id
  12.      * @param token token
  13.      * @param accountId 帐号Id
  14.      * @param response 响应
  15.      */
  16.     @GetMapping("/oceanfile/download")
  17.     public void downloadOceanfile(@RequestParam String fileId,
  18.                                   @RequestHeader(value = "Range") String range,
  19.                                   HttpServletResponse response) {
  20.         this.fileService.downloadFile(fileId, response, range);
  21.     }
  22. }
复制代码
  1. @Slf4j
  2. @Service
  3. public class FileService {
  4.     @Autowired
  5.     private CephUtils cephUtils;
  6.     /**
  7.      * 直接下载文件
  8.      *
  9.      * Tips: 支持断点下载
  10.      * @param fileId 文件Id
  11.      * @param response 返回
  12.      * @param range 范围
  13.      */
  14.     public void downloadFile(String fileId, HttpServletResponse response, String range) {
  15.         // 根据 fileId 获取文件信息
  16.         FileInfo fileInfo = getFileInfo(fileId);
  17.         String bucketName = fileInfo.getBucketName();
  18.         String relativePath = fileInfo.getRelativePath();
  19.         // 处理 range,范围信息
  20.         RangeDTO rangeInfo = executeRangeInfo(range, fileInfo.getFileSize());
  21.         // rangeInfo = null,直接下载整个文件
  22.         if (Objects.isNull(rangeInfo)) {
  23.             cephUtils.downloadFile(response, bucketName, relativePath);
  24.             return;
  25.         }
  26.         // 下载部分文件
  27.         cephUtils.downloadFileWithRange(response, bucketName, relativePath, rangeInfo);
  28.     }
  29.     private RangeDTO executeRangeInfo(String range, Long fileSize) {
  30.         if (StringUtils.isEmpty(range) || !range.contains("bytes=") || !range.contains("-")) {
  31.             return null;
  32.         }
  33.         long startByte = 0;
  34.         long endByte = fileSize - 1;
  35.         range = range.substring(range.lastIndexOf("=") + 1).trim();
  36.         String[] ranges = range.split("-");
  37.         if (ranges.length <= 0 || ranges.length > 2) {
  38.             return null;
  39.         }
  40.         try {
  41.             if (ranges.length == 1) {
  42.                 if (range.startsWith("-")) {
  43.                     //1. 如:bytes=-1024  从开始字节到第1024个字节的数据
  44.                     endByte = Long.parseLong(ranges[0]);
  45.                 } else if (range.endsWith("-")) {
  46.                     //2. 如:bytes=1024-  第1024个字节到最后字节的数据
  47.                     startByte = Long.parseLong(ranges[0]);
  48.                 }
  49.             } else {
  50.                 //3. 如:bytes=1024-2048  第1024个字节到2048个字节的数据
  51.                 startByte = Long.parseLong(ranges[0]);
  52.                 endByte = Long.parseLong(ranges[1]);
  53.             }
  54.         } catch (NumberFormatException e) {
  55.             startByte = 0;
  56.             endByte = fileSize - 1;
  57.         }
  58.         if (startByte >= fileSize) {
  59.             log.error("range error, startByte >= fileSize. " +
  60.                     "startByte: {}, fileSize: {}", startByte, fileSize);
  61.             return null;
  62.         }
  63.         return new RangeDTO(startByte, endByte);
  64.     }
  65. }
复制代码
以上内容,大家可以收藏起来,如果以后遇到这样的场景,分分钟搞定!
近期热文推荐:
1.1,000+ 道 Java面试题及答案整理(2022最新版)
2.劲爆!Java 协程要来了。。。
3.Spring Boot 2.x 教程,太全了!
4.别再写满屏的爆爆爆炸类了,试试装饰器模式,这才是优雅的方式!!
5.《Java开发手册(嵩山版)》最新发布,速速下载!
觉得不错,别忘了随手点赞+转发哦!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4