HTTP方式文件分片断点下载

2,851 阅读4分钟

​这是我参与更文挑战的第 30 天,活动详情查看: 更文挑战

前言

在进行大文件或网络带宽不是很好的情况下,分片断点下载就会显得很有必要,目前各大下载工具,如:迅雷,都是很好的支持分片断点下载功能的。本文就通过http方式进行文件分片断点下载,进行实战说明。

HTTP之Range

在开始之前有必要了解一下相关概念及原理,即:HTTP之Range,才能更好的理解分片断点下载的原理。

什么是Range

Range是一个HTTP请求头,告知服务器要返回文件的哪一部分,即:哪个区间范围(字节)的数据,在 Range 中,可以一次性请求多个部分,服务器会以 multipart 文件的形式将其返回。如果服务器返回的是范围响应,需要使用 206 Partial Content 状态码。假如所请求的范围不合法,那么服务器会返回 416 Range Not Satisfiable 状态码,表示客户端错误。服务器允许忽略 Range 头,从而返回整个文件,状态码用 200

因为有了HTTP中Range请求头的存在,分片断点下载,便简单了许多。

当你正在看大片时,网络断了,你需要继续看的时候,文件服务器不支持断点的话,则你需要重新等待下载这个大片,才能继续观看。而Range支持的话,客户端就会记录了之前已经看过的视频文件范围,网络恢复之后,则向服务器发送读取剩余Range的请求,服务端只需要发送客户端请求的那部分内容,而不用整个视频文件发送回客户端,以此节省网络带宽。

Range规范

Range: <unit>=<range-start>-
Range: <unit>=<range-start>-<range-end>
Range: <unit>=<range-start>-<range-end>, <range-start>-<range-end>
Range: <unit>=<range-start>-<range-end>, <range-start>-<range-end>, <range-start>-<range-end>

:范围所采用的单位,通常是字节(bytes)

:一个整数,表示在特定单位下,范围的起始值

:一个整数,表示在特定单位下,范围的结束值。这个值是可选的,如果不存在,表示此范围一直延伸到文档结束。

Range: bytes=1024-2048

分片断点下载之实现

以Java Spring Boot的方式来实现,核心代码如下:

  • serivce层

    package com.xcbeyond.common.file.chunk.service.impl;

    import com.xcbeyond.common.file.chunk.service.FileService; import org.springframework.stereotype.Service;

    import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.BufferedOutputStream; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile;

    /**

    • 文件分片操作Service

    • @Auther: xcbeyond

    • @Date: 2019/5/9 23:02 */ @Service public class FileServiceImpl implements FileService {

      /**

      • 文件分片下载

      • @param range http请求头Range,用于表示请求指定部分的内容。

      •          格式为:Range: bytes=start-end  [start,end]表示,即是包含请求头的startend字节的内容
        
      • @param request

      • @param response */ public void fileChunkDownload(String range, HttpServletRequest request, HttpServletResponse response) { //要下载的文件,此处以项目pom.xml文件举例说明。实际项目请根据实际业务场景获取 File file = new File(System.getProperty("user.dir") + "\pom.xml");

        //开始下载位置 long startByte = 0; //结束下载位置 long endByte = file.length() - 1;

        //有range的话 if (range != null && range.contains("bytes=") && range.contains("-")) { range = range.substring(range.lastIndexOf("=") + 1).trim(); String ranges[] = range.split("-"); try { //根据range解析下载分片的位置区间 if (ranges.length == 1) { //情况1,如:bytes=-1024 从开始字节到第1024个字节的数据 if (range.startsWith("-")) { endByte = Long.parseLong(ranges[0]); } //情况2,如:bytes=1024- 第1024个字节到最后字节的数据 else if (range.endsWith("-")) { startByte = Long.parseLong(ranges[0]); } } //情况3,如:bytes=1024-2048 第1024个字节到2048个字节的数据 else if (ranges.length == 2) { startByte = Long.parseLong(ranges[0]); endByte = Long.parseLong(ranges[1]); }

         } catch (NumberFormatException e) {
             startByte = 0;
             endByte = file.length() - 1;
         }
        

        }

        //要下载的长度 long contentLength = endByte - startByte + 1; //文件名 String fileName = file.getName(); //文件类型 String contentType = request.getServletContext().getMimeType(fileName);

        //响应头设置 //developer.mozilla.org/zh-CN/docs/… response.setHeader("Accept-Ranges", "bytes"); //Content-Type 表示资源类型,如:文件类型 response.setHeader("Content-Type", contentType); //Content-Disposition 表示响应内容以何种形式展示,是以内联的形式(即网页或者页面的一部分),还是以附件的形式下载并保存到本地。 // 这里文件名换成下载后你想要的文件名,inline表示内联的形式,即:浏览器直接下载 response.setHeader("Content-Disposition", "inline;filename=pom.xml"); //Content-Length 表示资源内容长度,即:文件大小 response.setHeader("Content-Length", String.valueOf(contentLength)); //Content-Range 表示响应了多少数据,格式为:[要下载的开始位置]-[结束位置]/[文件总大小] response.setHeader("Content-Range", "bytes " + startByte + "-" + endByte + "/" + file.length());

        response.setStatus(response.SC_OK); response.setContentType(contentType);

        BufferedOutputStream outputStream = null; RandomAccessFile randomAccessFile = null; //已传送数据大小 long transmitted = 0; try { randomAccessFile = new RandomAccessFile(file, "r"); outputStream = new BufferedOutputStream(response.getOutputStream()); byte[] buff = new byte[2048]; int len = 0; randomAccessFile.seek(startByte); //判断是否到了最后不足2048(buff的length)个byte while ((transmitted + len) <= contentLength && (len = randomAccessFile.read(buff)) != -1) { outputStream.write(buff, 0, len); transmitted += len; } //处理不足buff.length部分 if (transmitted < contentLength) { len = randomAccessFile.read(buff, 0, (int) (contentLength - transmitted)); outputStream.write(buff, 0, len); transmitted += len; }

         outputStream.flush();
         response.flushBuffer();
         randomAccessFile.close();
        

        } catch (IOException e) { e.printStackTrace(); } finally { try { if (randomAccessFile != null) { randomAccessFile.close(); } } catch (IOException e) { e.printStackTrace(); } } } }

  • controller层

    package com.xcbeyond.common.file.chunk.controller;

    import com.xcbeyond.common.file.chunk.service.FileService; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController;

    import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse;

    /**

    • 文件分片操作Controller

    • @Auther: xcbeyond

    • @Date: 2019/5/9 22:56 */ @RestController public class FileController { @Resource private FileService fileService;

      /**

      • 文件分片下载
      • @param range http请求头Range,用于表示请求指定部分的内容。
      •          格式为:Range: bytes=start-end  [start,end]表示,即是包含请求头的startend字节的内容
        
      • @param request http请求
      • @param response http响应 */ @RequestMapping(value = "/file/chunk/download", method = RequestMethod.GET) public void fileChunkDownload(@RequestHeader(value = "Range") String range, HttpServletRequest request, HttpServletResponse response) { fileService.fileChunkDownload(range,request,response); } }

通过postman进行测试验证,启动Spring Boot后,如:下载文件前1024个字节的数据(Range:bytes=0-1023),如下:

注:此处 实现中没有提供客户端,客户端可循环调用本例中下载接口,每次调用指定实际的下载偏移区间range。

请注意响应头Accept-Ranges、Content-Range

Accept-Ranges: 表示响应标识支持范围请求,字段的具体值用于定义范围请求的单位,如:bytes。当发现Accept-Range
头时,可以尝试继续之前中断的下载,而不是重新开始。

Content-Range: 表示响应了多少数据,格式为:[要下载的开始位置]-[结束位置]/[文件总大小],如:bytes 0-1023/2185

源码:github.com/xcbeyond/co…

(如果你觉得不错,不妨留下脚步,在GitHub上给个Star)

参考:developer.mozilla.org/zh-CN/docs/…