各位看官大家好,今天給大家分享的又是一篇實戰文章,希望大家能夠喜歡,
開味菜
最近接到一個新的需求,需要上傳2G
左右的視頻檔案,用測驗環境的OSS
試了一下,上傳需要十幾分鐘,再考慮到公司的資源問題,果斷放棄該方案,
一提到大檔案上傳,我最先想到的就是各種網盤了,現在大家都喜歡將自己收藏的「小電影」上傳到網盤進行保存,網盤一般都支持斷點續傳和檔案秒傳功能,減少了網路波動和網路帶寬對檔案的限制,大大提高了用戶體驗,讓人愛不釋手,
說到這,大家先來了解一下這幾個概念:
-
「檔案分塊」:將大檔案拆分成小檔案,將小檔案上傳\下載,最后再將小檔案組裝成大檔案; -
「斷點續傳」:在檔案分塊的基礎上,將每個小檔案采用單獨的執行緒進行上傳\下載,如果碰到網路故障,可以從已經上傳\下載的部分開始繼續上傳\下載未完成的部分,而沒有必要從頭開始上傳\下載; -
「檔案秒傳」:資源服務器中已經存在該檔案,其他人上傳時直接回傳該檔案的URI,
RandomAccessFile
平時我們都會使用FileInputStream
,FileOutputStream
,FileReader
以及FileWriter
等IO
流來讀取檔案,今天我們來了解一下RandomAccessFile
,
它是一個直接繼承Object
的獨立的類,底層實作中它實作的是DataInput
和DataOutput
介面,該類支持隨機讀取檔案,隨機訪問檔案類似于檔案系統中存盤的大位元組陣列,
它的實作基于「檔案指標」(一種游標或者指向隱含陣列的索引),檔案指標可以通過getFilePointer
方法讀取,也可以通過seek
方法設定,
輸入時從檔案指標開始讀取位元組,并使檔案指標超過讀取的位元組,如果寫入超過隱含陣列當前結尾的輸出操作會導致擴展陣列,該類有四種模式可供選擇:
-
r: 以只讀方式打開檔案,如果執行寫入操作會拋出 IOException
; -
rw: 以讀、寫方式打開檔案,如果檔案不存在,則嘗試創建檔案; -
rws: 以讀、寫方式打開檔案,要求對檔案內容或元資料的每次更新都同步寫入底層存盤設備; -
rwd: 以讀、寫方式打開檔案,要求對檔案內容的每次更新都同步寫入底層存盤設備;
在rw
模式下,默認是使用buffer
的,只有cache
滿的或者使用RandomAccessFile.close()
關閉流的時候才真正的寫到檔案,
API
1、void seek(long pos)
:設定下一次讀取或寫入時的檔案指標偏移量,通俗點說就是指定下次讀檔案資料的位置,
?偏移量可以設定在檔案末尾之外,只有在偏移量設定超出檔案末尾后,才能通過寫入更改檔案長度;
?
2、native long getFilePointer()
:回傳當前檔案的游標位置;
3、native long length()
:回傳當前檔案的長度;
4、「讀」方法
5、「寫」方法
6、readFully(byte[] b)
:這個方法的作用就是將文本中的內容填滿這個緩沖區b,如果緩沖b不能被填滿,那么讀取流的程序將被阻塞,如果發現是流的結尾,那么會拋出例外;
7、FileChannel getChannel()
:回傳與此檔案關聯的唯一FileChannel
物件;
8、int skipBytes(int n)
:試圖跳過n個位元組的輸入,丟棄跳過的位元組;
??
RandomAccessFile
的絕大多數功能,已經被JDK1.4
的NIO的「記憶體映射」檔案取代了,即把檔案映射到記憶體后再操作,省去了頻繁磁盤io
,
主菜
總結經驗,砥礪前行:之前的實戰文章中過多的粘貼了原始碼,影響了各位小伙伴的閱讀感受,經過大佬的點撥,以后將展示部分關鍵代碼,供各位賞析,原始碼可在「后臺」獲取,
檔案分塊
檔案分塊需要在前端進行處理,可以利用強大的js
庫或者現成的組件進行分塊處理,需要確定分塊的大小和分塊的數量,然后為每一個分塊指定一個索引值,
為了防止上傳檔案的分塊與其它檔案混淆,采用檔案的md5
值來進行區分,該值也可以用來校驗服務器上是否存在該檔案以及檔案的上傳狀態,
-
如果檔案存在,直接回傳檔案地址; -
如果檔案不存在,但是有上傳狀態,即部分分塊上傳成功,則回傳未上傳的分塊索引陣列; -
如果檔案不存在,且上傳狀態為空,則所有分塊均需要上傳,
fileRederInstance.readAsBinaryString(file);
fileRederInstance.addEventListener("load", (e) => {
let fileBolb = e.target.result;
fileMD5 = md5(fileBolb);
const formData = new FormData();
formData.append("md5", fileMD5);
axios
.post(http + "/fileUpload/checkFileMd5", formData)
.then((res) => {
if (res.data.message == "檔案已存在") {
//檔案已存在不走后面分片了,直接回傳檔案地址到前臺頁面
success && success(res);
} else {
//檔案不存在存在兩種情況,一種是回傳data:null代表未上傳過 一種是data:[xx,xx] 還有哪幾片未上傳
if (!res.data.data) {
//還有幾片未上傳情況,斷點續傳
chunkArr = res.data.data;
}
readChunkMD5();
}
})
.catch((e) => {});
});
在呼叫上傳介面前,通過slice
方法來取出索引在檔案中對應位置的分塊,
const getChunkInfo = (file, currentChunk, chunkSize) => {
//獲取對應下標下的檔案片段
let start = currentChunk * chunkSize;
let end = Math.min(file.size, start + chunkSize);
//對檔案分塊
let chunk = file.slice(start, end);
return { start, end, chunk };
};
之后呼叫上傳介面完成上傳,
斷點續傳、檔案秒傳
后端基于spring boot
開發,使用redis
來存盤上傳檔案的狀態和上傳檔案的地址,
如果檔案完整上傳,回傳檔案路徑;部分上傳則回傳未上傳的分塊陣列;如果未上傳過回傳提示資訊,
?在上傳分塊時會產生兩個檔案,一個是檔案主體,一個是臨時檔案,臨時檔案可以看做是一個陣列檔案,為每一個分塊分配一個值為127的位元組,
?
校驗MD5值時會用到兩個值:
-
檔案上傳狀態:只要該檔案上傳過就不為空,如果完整上傳則為 true
,部分上傳回傳false
; -
檔案上傳地址:如果檔案完整上傳,回傳檔案路徑;部分上傳回傳臨時檔案路徑,
/**
* 校驗檔案的MD5
**/
public Result checkFileMd5(String md5) throws IOException {
//檔案是否上傳狀態:只要該檔案上傳過該值一定存在
Object processingObj = stringRedisTemplate.opsForHash().get(UploadConstants.FILE_UPLOAD_STATUS, md5);
if (processingObj == null) {
return Result.ok("該檔案沒有上傳過");
}
boolean processing = Boolean.parseBoolean(processingObj.toString());
//完整檔案上傳完成時為檔案的路徑,如果未完成回傳臨時檔案路徑(臨時檔案相當于陣列,為每個分塊分配一個值為127的位元組)
String value = stringRedisTemplate.opsForValue().get(UploadConstants.FILE_MD5_KEY + md5);
//完整檔案上傳完成是true,未完成回傳false
if (processing) {
return Result.ok(value,"檔案已存在");
} else {
File confFile = new File(value);
byte[] completeList = FileUtils.readFileToByteArray(confFile);
List<Integer> missChunkList = new LinkedList<>();
for (int i = 0; i < completeList.length; i++) {
if (completeList[i] != Byte.MAX_VALUE) {
//用空格補齊
missChunkList.add(i);
}
}
return Result.ok(missChunkList,"該檔案上傳了一部分");
}
}
說到這,你肯定會問:當這個檔案的所有分塊上傳完成之后,該怎么得到完整的檔案呢?接下來我們就說一下分塊合并的問題,
分塊上傳、檔案合并
上邊我們提到了利用檔案的md5
值來維護分塊和檔案的關系,因此我們會將具有相同md5
值的分塊進行合并,由于每個分塊都有自己的索引值,所以我們會將分塊按索引像插入陣列一樣分別插入檔案中,形成完整的檔案,
分塊上傳時,要和前端的分塊大小、分塊數量、當前分塊索引等對應好,以備檔案合并時使用,此處我們采用的是「磁盤映射」的方式來合并檔案,
//讀操作和寫操作都是允許的
RandomAccessFile tempRaf = new RandomAccessFile(tmpFile, "rw");
//它回傳的就是nio通信中的file的唯一channel
FileChannel fileChannel = tempRaf.getChannel();
//寫入該分片資料 分片大小 * 第幾塊分片獲取偏移量
long offset = CHUNK_SIZE * multipartFileDTO.getChunk();
//分片檔案大小
byte[] fileData = multipartFileDTO.getFile().getBytes();
//將檔案的區域直接映射到記憶體
MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, offset, fileData.length);
mappedByteBuffer.put(fileData);
// 釋放
FileMD5Util.freedMappedByteBuffer(mappedByteBuffer);
fileChannel.close();
每當完成一次分塊的上傳,還需要去檢查檔案的上傳進度,看檔案是否上傳完成,
RandomAccessFile accessConfFile = new RandomAccessFile(confFile, "rw");
//把該分段標記為 true 表示完成
accessConfFile.setLength(multipartFileDTO.getChunks());
accessConfFile.seek(multipartFileDTO.getChunk());
accessConfFile.write(Byte.MAX_VALUE);
//completeList 檢查是否全部完成,如果陣列里是否全部都是(全部分片都成功上傳)
byte[] completeList = FileUtils.readFileToByteArray(confFile);
byte isComplete = Byte.MAX_VALUE;
for (int i = 0; i < completeList.length && isComplete == Byte.MAX_VALUE; i++) {
//與運算, 如果有部分沒有完成則 isComplete 不是 Byte.MAX_VALUE
isComplete = (byte) (isComplete & completeList[i]);
}
accessConfFile.close();
然后更新檔案的上傳進度到Redis
中,
//更新redis中的狀態:如果是true的話證明是已經該大檔案全部上傳完成
if (isComplete == Byte.MAX_VALUE) {
stringRedisTemplate.opsForHash().put(UploadConstants.FILE_UPLOAD_STATUS, multipartFileDTO.getMd5(), "true");
stringRedisTemplate.opsForValue().set(UploadConstants.FILE_MD5_KEY + multipartFileDTO.getMd5(), uploadDirPath + "/" + fileName);
} else {
if (!stringRedisTemplate.opsForHash().hasKey(UploadConstants.FILE_UPLOAD_STATUS, multipartFileDTO.getMd5())) {
stringRedisTemplate.opsForHash().put(UploadConstants.FILE_UPLOAD_STATUS, multipartFileDTO.getMd5(), "false");
}
if (!stringRedisTemplate.hasKey(UploadConstants.FILE_MD5_KEY + multipartFileDTO.getMd5())) {
stringRedisTemplate.opsForValue().set(UploadConstants.FILE_MD5_KEY + multipartFileDTO.getMd5(), uploadDirPath + "/" + fileName + ".conf");
}
}
作者|阿Q說代碼
本文來自博客園,作者:古道輕風,轉載請注明原文鏈接:https://www.cnblogs.com/88223100/p/How-to-realize-breakpoint-continuation-of-large-files.html
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/546599.html
標籤:其他