HTTP方式文件分片斷點下載

前言

在進行大文件或網路頻寬不是很好的情況下,分片斷點下載就會顯得很有必要,目前各大下載工具,如:迅雷等,都是很好的支援分片斷點下載功能的。本文就通過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>

<unit>:範圍所採用的單位,通常是位元組(bytes)

<range-start>:一個整數,表示在特定單位下,範圍的起始值

<range-end>:一個整數,表示在特定單位下,範圍的結束值。這個值是可選的,如果不存在,表示此範圍一直延伸到文檔結束。

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 */@Servicepublic class FileServiceImpl implements FileService {      /**     * 文件分片下載     * @param range http請求頭Range,用於表示請求指定部分的內容。     *              格式為:Range: bytes=start-end  [start,end]表示,即是包含請求頭的start及end位元組的內容     * @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);          //響應頭設置        //https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Accept-Ranges        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 */@RestControllerpublic class FileController {    @Resource    private FileService fileService;      /**     * 文件分片下載     * @param range http請求頭Range,用於表示請求指定部分的內容。     *              格式為:Range: bytes=start-end  [start,end]表示,即是包含請求頭的start及end位元組的內容     * @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

源碼:https://github.com/xcbeyond/common-utils/tree/master/src/main/java/com/xcbeyond/common/file/chunk

(如果你覺得不錯,不妨留下腳步,在GitHub上給個Star)

參考:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Range