Java實現(xiàn)斷點下載功能的示例代碼
介紹
當下載一個很大的文件時,如果下載到一半暫停,如果繼續(xù)下載呢?斷點下載就是解決這個問題的。
具體原理:
利用indexedDb,將下載的數(shù)據(jù)存儲到用戶的本地中,這樣用戶就算是關(guān)電腦那么下次下載還是從上次的位置開始的
- 先去看看本地緩存中是否存在這個文件的分片數(shù)據(jù),如果存在那么就接著上一個分片繼續(xù)下載(起始位置)
- 下載前先去后端拿文件的大小,然后計算分多少次下載(n/(1024*1024*10)) (結(jié)束位置)
- 每次下載的數(shù)據(jù)放入一個Blob中,然后存儲到本地indexedDB
- 當全部下載完畢后,將所有本地緩存的分片全部合并,然后給用戶
有很多人說必須使用content-length、Accept-Ranges、Content-Range還有Range。 但是這只是一個前后端的約定而已,所有沒必須非要遵守,只要你和后端約定好怎么拿取數(shù)據(jù)就行
難點都在前端:
- 怎么存儲
- 怎么計算下載多少次
- 怎么獲取最后下載的分片是什么
- 怎么判斷下載完成了
- 怎么保證下載的分片都是完整的
- 下載后怎么合并然后給用戶
效果

前端代碼
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>html5大文件斷點下載傳</h1>
<div id="progressBar"></div>
<Button id="but">下載</Button>
<Button id="stop">暫停</Button>
<script type="module">
import FileSliceDownload from '/src/file/FileSliceDownload.js'
let downloadUrl = "http://localhost:7003/fileslice/dwnloadsFIleSlice"
let fileSizeUrl = "http://localhost:7003/fileslice/fIleSliceDownloadSize"
let fileName = "Downloads.zip"
let but = document.querySelector("#but")
let stop = document.querySelector("#stop")
let fileSliceDownload = new FileSliceDownload(downloadUrl, fileSizeUrl);
fileSliceDownload.addProgress("#progressBar")
but.addEventListener("click", function () {
fileSliceDownload.startDownload(fileName)
})
stop.addEventListener("click", function () {
fileSliceDownload.stop()
})
</script>
</body>
</html>
class BlobUtls{
// blob轉(zhuǎn)文件并下載
static downloadFileByBlob(blob, fileName = "file") {
let blobUrl = window.URL.createObjectURL(blob)
let link = document.createElement('a')
link.download = fileName || 'defaultName'
link.style.display = 'none'
link.href = blobUrl
// 觸發(fā)點擊
document.body.appendChild(link)
link.click()
// 移除
document.body.removeChild(link)
}
}
export default BlobUtls;
//導包要從項目全路徑開始,也就是最頂部
import BlobUtls from '/web-js/src/blob/BlobUtls.js'
//導包
class FileSliceDownload{
#m1=1024*1024*10 //1mb 每次下載多少
#db //indexedDB庫對象
#downloadUrl // 下載文件的地址
#fileSizeUrl // 獲取文件大小的url
#fileSiez=0 //下載的文件大小
#fileName // 下載的文件名稱
#databaseName="dbDownload"; //默認庫名稱
#tableDadaName="tableDada" //用于存儲數(shù)據(jù)的表
#tableInfoName="tableInfo" //用于存儲信息的表
#fIleReadCount=0 //文件讀取次數(shù)
#fIleStartReadCount=0//文件起始的位置
#barId = "bar"; //進度條id
#progressId = "progress";//進度數(shù)值ID
#percent=0 //百分比
#checkDownloadInterval=null; //檢測下載是否完成定時器
#mergeInterval=null;//檢測是否滿足合并分片要求
#stop=false; //是否結(jié)束
//下載地址
constructor(downloadUrl,fileSizeUrl) {
this.check()
this.#downloadUrl=downloadUrl;
this.#fileSizeUrl=fileSizeUrl;
}
check(){
let indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB ;
if(!indexedDB){
alert('不支持');
}
}
//初始化
#init(fileName){
return new Promise((resolve,reject)=>{
this.#fileName=fileName;
this.#percent=0;
this.#stop=false;
const request = window.indexedDB.open(this.#databaseName, 1)
request.onupgradeneeded = (e) => {
const db = e.target.result
if (!db.objectStoreNames.contains(this.#tableDadaName)) {
db.createObjectStore(this.#tableDadaName, { keyPath: 'serial',autoIncrement:false })
db.createObjectStore(this.#tableInfoName, { keyPath: 'primary',autoIncrement:false })
}
}
request.onsuccess = e => {
this.#db = e.target.result
resolve()
}
})
}
#getFileSize(){
return new Promise((resolve,reject)=>{
let ref=this;
var xhr = new XMLHttpRequest();
//同步
xhr.open("GET", this.#fileSizeUrl+"/"+this.#fileName,false)
xhr.send()
if (xhr.readyState === 4 && xhr.status === 200) {
let ret = JSON.parse(xhr.response)
if (ret.code === 20000) {
ref.#fileSiez=ret.data
}
resolve()
}
})
}
#getTransactionDadaStore(){
let transaction = this.#db.transaction([this.#tableDadaName], 'readwrite')
let store = transaction.objectStore(this.#tableDadaName)
return store;
}
#getTransactionInfoStore(){
let transaction = this.#db.transaction([this.#tableInfoName], 'readwrite')
let store = transaction.objectStore(this.#tableInfoName)
return store;
}
#setBlob(begin,end,i,last){
return new Promise((resolve,reject)=>{
var xhr = new XMLHttpRequest();
xhr.open("GET", this.#downloadUrl+"/"+this.#fileName+"/"+begin+"/"+end+"/"+last)
xhr.responseType="blob" // 只支持異步,默認使用 text 作為默認值。
xhr.send()
xhr.onload = ()=> {
if (xhr.status === 200) {
let store= this.#getTransactionDadaStore()
let obj={serial:i,blob:xhr.response}
//添加分片到用戶本地的庫中
store.add(obj)
let store2= this.#getTransactionInfoStore()
//記錄下載了多少個分片了
store2.put({primary:"count",count:i})
//調(diào)整進度條
let percent1= Math.ceil( (i/this.#fIleReadCount)*100)
if(this.#percent<percent1){
this.#percent=percent1;
}
this.#dynamicProgress()
resolve()
}
}
})
}
#mergeCallback(){
// 讀取全部字節(jié)到blob里,處理合并
let arrayBlobs = [];
let store1 = this.#getTransactionDadaStore()
//按順序找到全部的分片
for (let i = 0; i <this.#fIleReadCount; i++) {
let result= store1.get(IDBKeyRange.only(i))
result.onsuccess=(data)=>{
arrayBlobs.push(data.target.result.blob)
}
}
//分片合并下載
this.#mergeInterval= setInterval(()=> {
if(arrayBlobs.length===this.#fIleReadCount){
clearInterval(this.#mergeInterval);
//多個Blob進行合并
let fileBlob = new Blob(arrayBlobs);//合并后的數(shù)組轉(zhuǎn)成?個Blob對象。
BlobUtls.downloadFileByBlob(fileBlob,this.#fileName)
//下載完畢后清除數(shù)據(jù)
this. #clear()
}
},200)
}
#clear(){
let store2 = this.#getTransactionDadaStore()
let store3 = this.#getTransactionInfoStore()
store2.clear() //清除本地全下載的數(shù)據(jù)
store3.delete("count")//記錄清除
this.#fIleStartReadCount=0 //起始位置
this.#db=null;
this.#fileName=null;
this.#fileSiez=0;
this.#fIleReadCount=0 //文件讀取次數(shù)
this.#fIleStartReadCount=0//文件起始的位置
}
//檢測是否有分片在本地
#checkSliceDoesIsExist(){
return new Promise((resolve,reject)=>{
let store1 = this.#getTransactionInfoStore()
let result= store1.get(IDBKeyRange.only("count"))
result.onsuccess=(data)=>{
let count= data.target.result?.count
if(count){
//防止因為網(wǎng)絡的原因?qū)е路制瑩p壞,所以不要最后一個分片
this.#fIleStartReadCount=count-1;
}
resolve();
}
})
}
/**
* 樣式可以進行修改
* @param {*} progressId 需要將進度條添加到那個元素下面
*/
addProgress (progressSelect) {
let bar = document.createElement("div")
bar.setAttribute("id", this.#barId);
let num = document.createElement("div")
num.setAttribute("id", this.#progressId);
num.innerText = "0%"
bar.appendChild(num);
document.querySelector(progressSelect).appendChild(bar)
}
#dynamicProgress(){
//調(diào)整進度
let bar = document.getElementById(this.#barId)
let progressEl = document.getElementById(this.#progressId)
bar.style.width = this.#percent + '%';
bar.style.backgroundColor = 'red';
progressEl.innerHTML = this.#percent + '%'
}
stop(){
this.#stop=true;
}
startDownload(fileName){
//同步代碼塊
;(async ()=>{
//初始化
await this.#init(fileName)
//自動調(diào)整分片,如果本地以下載了那么從上一次繼續(xù)下載
await this.#checkSliceDoesIsExist()
//拿到文件的大小
await this.#getFileSize()
let begin=0; //開始讀取的字節(jié)
let end=this.#m1; // 結(jié)束讀取的字節(jié)
let last=false; //是否是最后一次讀取
this.#fIleReadCount= Math.ceil( this.#fileSiez/this.#m1)
for (let i = this.#fIleStartReadCount; i < this.#fIleReadCount; i++) {
if(this.#stop){
return
}
begin=i*this.#m1;
end=begin+this.#m1
if(i===this.#fIleReadCount-1){
last=true;
}
//添加分片
await this.#setBlob(begin,end,i,last)
}
//定時檢測存下載的分片數(shù)量是否夠了
this.#checkDownloadInterval= setInterval(()=> {
let store = this.#getTransactionDadaStore()
let result = store.count()
result.onsuccess = (data) => {
if (data.target.result === this.#fIleReadCount) {
clearInterval(this.#checkDownloadInterval);
//如果分片夠了那么進行合并下載
this.#mergeCallback()
}
}
},200)
})()
}
}
export default FileSliceDownload;
后端代碼
package com.controller.commontools.fileDownload;
import com.application.Result;
import com.container.ArrayByteUtil;
import com.file.FileWebDownLoad;
import com.file.ReadWriteFileUtils;
import com.path.ResourceFileUtil;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.OutputStream;
import java.net.URLEncoder;
@RestController
@RequestMapping("/fileslice")
public class FIleSliceDownloadController {
private final String uploaddir="uploads"+ File.separator+"real"+File.separator;//實際文件目錄
// 獲取文件的大小
@GetMapping("/fIleSliceDownloadSize/{fileName}")
public Result getFIleSliceDownloadSize(@PathVariable String fileName){
String absoluteFilePath = ResourceFileUtil.getAbsoluteFilePathAndCreate(uploaddir)+File.separator+fileName;
File file= new File(absoluteFilePath);
if(file.exists()&&file.isFile()){
return Result.Ok(file.length(),Long.class);
}
return Result.Error();
}
/**
* 分段下載文件
* @param fileName 文件名稱
* @param begin 從文件什么位置開始讀取
* @param end 到什么位置結(jié)束
* @param last 是否是最后一次讀取
* @param response
*/
@GetMapping("/dwnloadsFIleSlice/{fileName}/{begin}/{end}/{last}")
public void dwnloadsFIleSlice(@PathVariable String fileName, @PathVariable long begin, @PathVariable long end, @PathVariable boolean last, HttpServletResponse response){
String absoluteFilePath = ResourceFileUtil.getAbsoluteFilePathAndCreate(uploaddir)+File.separator+fileName;
File file= new File(absoluteFilePath);
try(OutputStream toClient = new BufferedOutputStream(response.getOutputStream())) {
long readSize = end - begin;
//讀取文件的指定字節(jié)
byte[] bytes = new byte[(int)readSize];
ReadWriteFileUtils.randomAccessFileRead(file.getAbsolutePath(),(int)begin,bytes);
if(readSize<=file.length()||last){
bytes=ArrayByteUtil.getActualBytes(bytes); //去掉多余的
}
response.setContentType("application/octet-stream");
response.addHeader("Content-Length", String.valueOf(bytes.length));
response.setHeader("Content-Disposition", "attachment;filename*=UTF-8''" + URLEncoder.encode(fileName, "UTF-8"));
toClient.write(bytes);
} catch (Exception e) {
e.printStackTrace();
}
}
}
以上就是Java實現(xiàn)斷點下載功能的示例代碼的詳細內(nèi)容,更多關(guān)于Java斷點下載的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Spring Boot 中實現(xiàn)跨域的多種方式小結(jié)
Spring Boot提供了多種方式來實現(xiàn)跨域請求,開發(fā)者可以根據(jù)具體需求選擇適合的方法,在配置時,要確保不僅考慮安全性,還要兼顧應用的靈活性和性能,本文給大家介紹Spring Boot 中實現(xiàn)跨域的多種方式,感興趣的朋友一起看看吧2024-01-01
SpringBoot Import及自定義裝配實現(xiàn)方法解析
這篇文章主要介紹了SpringBoot Import及自定義裝配實現(xiàn)方法解析,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-08-08
java高并發(fā)InterruptedException異常引發(fā)思考
這篇文章主要為大家介紹了java高并發(fā)InterruptedException異常引發(fā)思考,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-08-08
java?LockSupport實現(xiàn)原理示例解析
這篇文章主要為大家介紹了java?LockSupport實現(xiàn)原理示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-01-01
Java對象在內(nèi)存中的布局是如何實現(xiàn)的?
Java對象在內(nèi)存中屬于oop-klass二分模型,即對象的實例數(shù)據(jù)和對象類型的元數(shù)據(jù)(字段定義、方法、常量池等元數(shù)據(jù))是分開存儲的.而由于JVM對對象內(nèi)相同寬度的字段分配在一起,所以只要指定了字段類型分配的順序,就可以計算出每種類型字段相對于當前對象的偏移起始位置2021-06-06

