HTTP方式文件分片斷點(diǎn)下載

作者:xcbeyond
瘋狂源自夢(mèng)想,技術(shù)成就輝煌!微信公眾號(hào):《程序猿技術(shù)大咖》號(hào)主,專注后端開發(fā)多年,擁有豐富的研發(fā)經(jīng)驗(yàn),樂(lè)于技術(shù)輸出、分享,現(xiàn)階段從事微服務(wù)架構(gòu)項(xiàng)目的研發(fā)工作,涉及架構(gòu)設(shè)計(jì)、技術(shù)選型、業(yè)務(wù)研發(fā)等工作。對(duì)于Java、微服務(wù)、數(shù)據(jù)庫(kù)、Docker有深入了解,并有大量的調(diào)優(yōu)經(jīng)驗(yàn)。

前言

      在進(jìn)行大文件或網(wǎng)絡(luò)帶寬不是很好的情況下,分片斷點(diǎn)下載就會(huì)顯得很有必要,目前各大下載工具,如:迅雷,都是很好的支持分片斷點(diǎn)下載功能的。本文就通過(guò)http方式進(jìn)行文件分片斷點(diǎn)下載,進(jìn)行實(shí)戰(zhàn)說(shuō)明。
HTTP之Range

     在開始之前有必要了解一下相關(guān)概念及原理,即:HTTP之Range,才能更好的理解分片斷點(diǎn)下載的原理。
什么是Range

     Range是一個(gè)HTTP請(qǐng)求頭,告知服務(wù)器要返回文件的哪一部分,即:哪個(gè)區(qū)間范圍(字節(jié))的數(shù)據(jù),在 Range 中,可以一次性請(qǐng)求多個(gè)部分,服務(wù)器會(huì)以 multipart 文件的形式將其返回。如果服務(wù)器返回的是范圍響應(yīng),需要使用 206 Partial Content 狀態(tài)碼。假如所請(qǐng)求的范圍不合法,那么服務(wù)器會(huì)返回  416 Range Not Satisfiable 狀態(tài)碼,表示客戶端錯(cuò)誤。服務(wù)器允許忽略  Range  頭,從而返回整個(gè)文件,狀態(tài)碼用 200 。

     因?yàn)橛辛薍TTP中Range請(qǐng)求頭的存在,分片斷點(diǎn)下載,便簡(jiǎn)單了許多。

     當(dāng)你正在看大片時(shí),網(wǎng)絡(luò)斷了,你需要繼續(xù)看的時(shí)候,文件服務(wù)器不支持?jǐn)帱c(diǎn)的話,則你需要重新等待下載這個(gè)大片,才能繼續(xù)觀看。而Range支持的話,客戶端就會(huì)記錄了之前已經(jīng)看過(guò)的視頻文件范圍,網(wǎng)絡(luò)恢復(fù)之后,則向服務(wù)器發(fā)送讀取剩余Range的請(qǐng)求,服務(wù)端只需要發(fā)送客戶端請(qǐng)求的那部分內(nèi)容,而不用整個(gè)視頻文件發(fā)送回客戶端,以此節(jié)省網(wǎng)絡(luò)帶寬。
Range規(guī)范

    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>:范圍所采用的單位,通常是字節(jié)(bytes)

<range-start>:一個(gè)整數(shù),表示在特定單位下,范圍的起始值

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

Range: bytes=1024-2048

分片斷點(diǎn)下載之實(shí)現(xiàn)

以Java Spring Boot的方式來(lái)實(shí)現(xiàn),核心代碼如下:

    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請(qǐng)求頭Range,用于表示請(qǐng)求指定部分的內(nèi)容。
         *              格式為:Range: bytes=start-end  [start,end]表示,即是包含請(qǐng)求頭的start及end字節(jié)的內(nèi)容
         * @param request
         * @param response
         */
        public void fileChunkDownload(String range, HttpServletRequest request, HttpServletResponse response) {
            //要下載的文件,此處以項(xiàng)目pom.xml文件舉例說(shuō)明。實(shí)際項(xiàng)目請(qǐng)根據(jù)實(shí)際業(yè)務(wù)場(chǎng)景獲取
            File file = new File(System.getProperty("user.dir") + "\\pom.xml");
     
            //開始下載位置
            long startByte = 0;
            //結(jié)束下載位置
            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 {
                    //根據(jù)range解析下載分片的位置區(qū)間
                    if (ranges.length == 1) {
                        //情況1,如:bytes=-1024  從開始字節(jié)到第1024個(gè)字節(jié)的數(shù)據(jù)
                        if (range.startsWith("-")) {
                            endByte = Long.parseLong(ranges[0]);
                        }
                        //情況2,如:bytes=1024-  第1024個(gè)字節(jié)到最后字節(jié)的數(shù)據(jù)
                        else if (range.endsWith("-")) {
                            startByte = Long.parseLong(ranges[0]);
                        }
                    }
                    //情況3,如:bytes=1024-2048  第1024個(gè)字節(jié)到2048個(gè)字節(jié)的數(shù)據(jù)
                    else if (ranges.length == 2) {
                        startByte = Long.parseLong(ranges[0]);
                        endByte = Long.parseLong(ranges[1]);
                    }
     
                } catch (NumberFormatException e) {
                    startByte = 0;
                    endByte = file.length() - 1;
                }
            }
     
            //要下載的長(zhǎng)度
            long contentLength = endByte - startByte + 1;
            //文件名
            String fileName = file.getName();
            //文件類型
            String contentType = request.getServletContext().getMimeType(fileName);
     
            //響應(yīng)頭設(shè)置
            //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 表示響應(yīng)內(nèi)容以何種形式展示,是以內(nèi)聯(lián)的形式(即網(wǎng)頁(yè)或者頁(yè)面的一部分),還是以附件的形式下載并保存到本地。
            // 這里文件名換成下載后你想要的文件名,inline表示內(nèi)聯(lián)的形式,即:瀏覽器直接下載
            response.setHeader("Content-Disposition", "inline;filename=pom.xml");
            //Content-Length 表示資源內(nèi)容長(zhǎng)度,即:文件大小
            response.setHeader("Content-Length", String.valueOf(contentLength));
            //Content-Range 表示響應(yīng)了多少數(shù)據(jù),格式為:[要下載的開始位置]-[結(jié)束位置]/[文件總大小]
            response.setHeader("Content-Range", "bytes " + startByte + "-" + endByte + "/" + file.length());
     
            response.setStatus(response.SC_OK);
            response.setContentType(contentType);
     
            BufferedOutputStream outputStream = null;
            RandomAccessFile randomAccessFile = null;
            //已傳送數(shù)據(jù)大小
            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)個(gè)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請(qǐng)求頭Range,用于表示請(qǐng)求指定部分的內(nèi)容。
         *              格式為:Range: bytes=start-end  [start,end]表示,即是包含請(qǐng)求頭的start及end字節(jié)的內(nèi)容
         * @param request   http請(qǐng)求
         * @param response  http響應(yīng)
         */
        @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);
        }
    }

 






通過(guò)postman進(jìn)行測(cè)試驗(yàn)證,啟動(dòng)Spring Boot后,如:下載文件前1024個(gè)字節(jié)的數(shù)據(jù)(Range:bytes=0-1023),如下:























注:此處 實(shí)現(xiàn)中沒(méi)有提供客戶端,客戶端可循環(huán)調(diào)用本例中下載接口,每次調(diào)用指定實(shí)際的下載偏移區(qū)間range。

 

請(qǐng)注意響應(yīng)頭Accept-Ranges、Content-Range

























Accept-Ranges: 表示響應(yīng)標(biāo)識(shí)支持范圍請(qǐng)求,字段的具體值用于定義范圍請(qǐng)求的單位,如:bytes。當(dāng)發(fā)現(xiàn)Accept-Range
頭時(shí),可以嘗試?yán)^續(xù)之前中斷的下載,而不是重新開始。

Content-Range: 表示響應(yīng)了多少數(shù)據(jù),格式為:[要下載的開始位置]-[結(jié)束位置]/[文件總大小],如:bytes 0-1023/2185

 

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