找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

5495

积分

0

好友

726

主题
发表于 1 小时前 | 查看: 4| 回复: 0

前言

互联网连接时常有波动,速度也不总是理想。在客户端下载大文件时,由于网络问题导致传输中断的概率不容忽视。一旦中断,如果只能从头再来,不仅浪费时间,也消耗带宽资源。

文件断点续传应运而生。其核心思路很简单:客户端记录已下载的部分,下次请求时告诉服务端“从上次中断的位置继续”。下面这张截图展示了一个典型的断点下载过程,可以直观看到多个线程协同工作、进度稳步逼近 100% 的场景。

文件断点下载进度与多线程下载状态输出截图

那么,从技术层面如何实现这一需求?客户端发送 GET 请求时,可以通过设置 Range 头部来告知服务端需要从哪个字节位置开始输出数据。

判断服务端是否支持断点下载,可以依据 HTTP 规范 (参见 14.35.1 Byte Ranges)。一个直接的办法是检查响应头中是否包含 Accept-Ranges: bytes

// 直接判断是否有 Accept-Ranges = bytes
boolean support = urlConnection.getHeaderField("Accept-Ranges").equals("bytes");
System.out.println("Partial content retrieval support = " + (support ? "Yes" : "No));

例如,我们用 curl 模拟一个范围请求,服务端的响应如下:

donald@donald-pro:~$ curl -i --range 0-9 http://localhost:8080/file/chunk/download
HTTP/1.1 206
Accept-Ranges: bytes
Content-Disposition: inline;filename=pom.xml
Content-Range: bytes 0-9/13485
Content-Length: 10
Date: Mon, 01 Nov 2021 09:53:25 GMT

当然,除了直接看 Accept-Ranges,还有一种更为精准的判断方式,就是发送 HEAD 请求来获取资源的元信息。

HeadObject 接口用于获取某个文件(Object)的元信息。使用此接口不会返回文件内容。

HEAD /ObjectName HTTP/1.1
Host: BucketName.oss-cn-hangzhou.aliyuncs.com
Date: GMT Date
Authorization: SignatureValue

在这个过程中,需要关注几个关键的 HTTP 状态码:

  • 206 Partial ContentHTTP Range 请求成功。
  • 416 Requested Range Not Satisfiable statusHTTP Range 请求超出文件范围。
  • 200 OK:服务器不支持范围请求。

小结一下,实现断点下载需要把握以下几点:

  1. HTTP 范围请求需要 HTTP/1.1 及以上版本支持,如果客户端或服务端任一端低于此版本,通常认为不支持。
  2. 通过响应头中的 Accept-Ranges 字段来确定服务端是否支持范围请求。
  3. 客户端在请求头中添加 Range 字段,来指定请求的内容字节范围。
  4. 服务端在响应头中,通过 Content-Range 来标识实际返回的内容范围,并用 Content-Length 标识此次返回内容的长度。
  5. 在请求过程中,可以通过 If-Range 来校验资源是否发生了变更,值可以来自 ETag 或者 Last-Modified。如果资源有变动,则会重新走完整下载流程。

生产实战

开发必须依循标准和规范。我们来看看阿里云 OSS 文档中对 Range 的定义:

  • Range: bytes=0-499:表示第0~499字节范围的内容。
  • Range: bytes=500-999:表示第500~999字节范围的内容。
  • Range: bytes=-500:表示最后500字节的内容。
  • Range: bytes=500-:表示从第500字节开始到文件末尾的内容。
  • Range: bytes=0-:表示完整的文件内容。

如果客户端发来的 HTTP Range 请求是合法的,服务端响应 206 并在响应头中包含 Content-Range;反之,如果 HTTP Range 请求不合法,或者指定范围不在有效区间,Range 就会失效,服务端会响应 200 并返回完整的 Object 内容。

假设 Object 资源大小为 1000 字节,有效区间为 0~999。以下为不合法请求示例:

  • Range: byte=0-499:格式错误,byte 应为 bytes
  • Range: bytes=0-1000:末字节 1000 超出有效区间。
  • Range: bytes=1000-2000:整个指定范围都超出有效区间。
  • Range: bytes=1000-:首字节超出有效区间。
  • Range: bytes=-2000:指定范围超出有效区间。

这里再举一个正常范围下载的例子,可以看到服务端返回了 206 状态码及详细的 Content-Range 信息。

# 正常范围下载
donald@donald-pro:~$ curl -i --range 0-9 http://localhost:8080/file/chunk/download
HTTP/1.1 206
Accept-Ranges: bytes
Content-Disposition: inline;filename=Screen_Recording_20211101-162729_Settings.mp4
Content-Range: bytes 0-9
Content-Type: application/force-download;charset=UTF-8
Content-Length: 16241985
Date: Wed, 03 Nov 2021 09:50:50 GMT

服务端 - 业务开发

理论知识已备齐,接下来以 Spring Boot 为例,演示如何在后端业务中实现支持 range 的下载功能。底层存储使用 ceph

  1. 对外暴露支持 range 下载的接口。
  2. 底层对接 ceph 存储。
  3. Controller 层核心代码:
@Slf4j
@RestController
public class Controller {
    @Autowired
    private FileService fileService;

    /**
     * 下载文件
     *
     * 对外提供
     *
     * @param fileId 文件Id
     * @param token token
     * @param accountId 帐号Id
     * @param response 响应
     */
    @GetMapping("/oceanfile/download")
    public void downloadOceanfile(@RequestParam String fileId,
                                  @RequestHeader(value = "Range") String range,
                                  HttpServletResponse response) {

        this.fileService.downloadFile(fileId, response, range);
    }
}
  1. Service 层的实现逻辑,关键在于 executeRangeInfo 方法对 Range 头部字符串的解析与校验。这正是 后端与架构 设计中需要细致处理的细节。
@Slf4j
@Service
public class FileService {
    @Autowired
    private CephUtils cephUtils;

    /**
     * 直接下载文件
     *
     * Tips: 支持断点下载
     * @param fileId 文件Id
     * @param response 返回
     * @param range 范围
     */
    public void downloadFile(String fileId, HttpServletResponse response, String range) {
        // 根据 fileId 获取文件信息
        FileInfo fileInfo = getFileInfo(fileId);

        String bucketName = fileInfo.getBucketName();
        String relativePath = fileInfo.getRelativePath();

        // 处理 range,范围信息
        RangeDTO rangeInfo = executeRangeInfo(range, fileInfo.getFileSize());

        // rangeInfo = null,直接下载整个文件
        if (Objects.isNull(rangeInfo)) {

            cephUtils.downloadFile(response, bucketName, relativePath);
            return;
        }
        // 下载部分文件
        cephUtils.downloadFileWithRange(response, bucketName, relativePath, rangeInfo);
    }

    private RangeDTO executeRangeInfo(String range, Long fileSize) {

        if (StringUtils.isEmpty(range) || !range.contains("bytes=") || !range.contains("-")) {

            return null;
        }

        long startByte = 0;
        long endByte = fileSize - 1;

        range = range.substring(range.lastIndexOf("=") + 1).trim();
        String[] ranges = range.split("-");

        if (ranges.length <= 0 || ranges.length > 2) {

            return null;
        }

        try {
            if (ranges.length == 1) {
                if (range.startsWith("-")) {

                    //1. 如:bytes=-1024  从开始字节到第1024个字节的数据
                    endByte = Long.parseLong(ranges[0]);
                } else if (range.endsWith("-")) {

                    //2. 如:bytes=1024-  第1024个字节到最后字节的数据
                    startByte = Long.parseLong(ranges[0]);
                }
            } else {
                //3. 如:bytes=1024-2048  第1024个字节到2048个字节的数据
                startByte = Long.parseLong(ranges[0]);
                endByte = Long.parseLong(ranges[1]);
            }
        } catch (NumberFormatException e) {
            startByte = 0;
            endByte = fileSize - 1;
        }

        if (startByte >= fileSize) {

            log.error("range error, startByte >= fileSize. " +
                "startByte: {}, fileSize: {}", startByte, fileSize);
            return null;
        }

        return new RangeDTO(startByte, endByte);
    }
}

以上便是 Java 文件断点下载从理论到生产的完整实战过程。这套方案已在云栈社区多位开发者的生产环境中稳定运行,如果你之后遇到类似的大文件传输需求,相信可以直接拿来即用,高效搞定!




上一篇:Kafka 暂停消费:Thread.sleep 引发的 Rebalance 死循环与 pause/resume 正解
下一篇:GitHub Copilot 切换按 Token 计费:Pro/Pro+ 模型倍率调整与 Copilot 免费模型取消
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-4-30 22:29 , Processed in 0.825343 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表