HTTP方式文件分片斷點下載
作者:xcbeyond
瘋狂源自夢想,技術成就輝煌!微信公眾號:《程序猿技術大咖》號主,專注后端開發(fā)多年,擁有豐富的研發(fā)經驗,樂于技術輸出、分享,現(xiàn)階段從事微服務架構項目的研發(fā)工作,涉及架構設計、技術選型、業(yè)務研發(fā)等工作。對于Java、微服務、數(shù)據庫、Docker有深入了解,并有大量的調優(yōu)經驗。
前言
在進行大文件或網絡帶寬不是很好的情況下,分片斷點下載就會顯得很有必要,目前各大下載工具,如:迅雷,都是很好的支持分片斷點下載功能的。本文就通過http方式進行文件分片斷點下載,進行實戰(zhàn)說明。
HTTP之Range
在開始之前有必要了解一下相關概念及原理,即:HTTP之Range,才能更好的理解分片斷點下載的原理。
什么是Range
Range是一個HTTP請求頭,告知服務器要返回文件的哪一部分,即:哪個區(qū)間范圍(字節(jié))的數(shù)據,在 Range 中,可以一次性請求多個部分,服務器會以 multipart 文件的形式將其返回。如果服務器返回的是范圍響應,需要使用 206 Partial Content 狀態(tài)碼。假如所請求的范圍不合法,那么服務器會返回 416 Range Not Satisfiable 狀態(tài)碼,表示客戶端錯誤。服務器允許忽略 Range 頭,從而返回整個文件,狀態(tài)碼用 200 。
因為有了HTTP中Range請求頭的存在,分片斷點下載,便簡單了許多。
當你正在看大片時,網絡斷了,你需要繼續(xù)看的時候,文件服務器不支持斷點的話,則你需要重新等待下載這個大片,才能繼續(xù)觀看。而Range支持的話,客戶端就會記錄了之前已經看過的視頻文件范圍,網絡恢復之后,則向服務器發(fā)送讀取剩余Range的請求,服務端只需要發(fā)送客戶端請求的那部分內容,而不用整個視頻文件發(fā)送回客戶端,以此節(jié)省網絡帶寬。
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>:一個整數(shù),表示在特定單位下,范圍的起始值
<range-end>:一個整數(shù),表示在特定單位下,范圍的結束值。這個值是可選的,如果不存在,表示此范圍一直延伸到文檔結束。
Range: bytes=1024-2048
分片斷點下載之實現(xiàn)
以Java Spring Boot的方式來實現(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請求頭Range,用于表示請求指定部分的內容。
* 格式為:Range: bytes=start-end [start,end]表示,即是包含請求頭的start及end字節(jié)的內容
* @param request
* @param response
*/
public void fileChunkDownload(String range, HttpServletRequest request, HttpServletResponse response) {
//要下載的文件,此處以項目pom.xml文件舉例說明。實際項目請根據實際業(yè)務場景獲取
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解析下載分片的位置區(qū)間
if (ranges.length == 1) {
//情況1,如:bytes=-1024 從開始字節(jié)到第1024個字節(jié)的數(shù)據
if (range.startsWith("-")) {
endByte = Long.parseLong(ranges[0]);
}
//情況2,如:bytes=1024- 第1024個字節(jié)到最后字節(jié)的數(shù)據
else if (range.endsWith("-")) {
startByte = Long.parseLong(ranges[0]);
}
}
//情況3,如:bytes=1024-2048 第1024個字節(jié)到2048個字節(jié)的數(shù)據
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 表示響應內容以何種形式展示,是以內聯(lián)的形式(即網頁或者頁面的一部分),還是以附件的形式下載并保存到本地。
// 這里文件名換成下載后你想要的文件名,inline表示內聯(lián)的形式,即:瀏覽器直接下載
response.setHeader("Content-Disposition", "inline;filename=pom.xml");
//Content-Length 表示資源內容長度,即:文件大小
response.setHeader("Content-Length", String.valueOf(contentLength));
//Content-Range 表示響應了多少數(shù)據,格式為:[要下載的開始位置]-[結束位置]/[文件總大小]
response.setHeader("Content-Range", "bytes " + startByte + "-" + endByte + "/" + file.length());
response.setStatus(response.SC_OK);
response.setContentType(contentType);
BufferedOutputStream outputStream = null;
RandomAccessFile randomAccessFile = null;
//已傳送數(shù)據大小
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]表示,即是包含請求頭的start及end字節(jié)的內容
* @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個字節(jié)的數(shù)據(Range:bytes=0-1023),如下:
注:此處 實現(xiàn)中沒有提供客戶端,客戶端可循環(huán)調用本例中下載接口,每次調用指定實際的下載偏移區(qū)間range。
請注意響應頭Accept-Ranges、Content-Range
Accept-Ranges: 表示響應標識支持范圍請求,字段的具體值用于定義范圍請求的單位,如:bytes。當發(fā)現(xiàn)Accept-Range
頭時,可以嘗試繼續(xù)之前中斷的下載,而不是重新開始。
Content-Range: 表示響應了多少數(shù)據,格式為:[要下載的開始位置]-[結束位置]/[文件總大小],如:bytes 0-1023/2185
源碼:https://github.com/xcbeyond/common-utils/tree/master/src/main/java/com/xcbeyond/common/file/chunk