最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會員登陸 & 注冊

實(shí)戰(zhàn):斷點(diǎn)續(xù)傳?文件秒傳?手?jǐn)]大文件上傳

2023-03-20 17:28 作者:董嘉dongjia  | 我要投稿


最近接到一個新的需求,需要上傳2G左右的視頻文件,用測試環(huán)境的OSS試了一下,上傳需要十幾分鐘,再考慮到公司的資源問題,果斷放棄該方案。

一提到大文件上傳,我最先想到的就是各種網(wǎng)盤了,現(xiàn)在大家都喜歡將自己收藏的小電影上傳到網(wǎng)盤進(jìn)行保存。網(wǎng)盤一般都支持?jǐn)帱c(diǎn)續(xù)傳和文件秒傳功能,減少了網(wǎng)絡(luò)波動和網(wǎng)絡(luò)帶寬對文件的限制,大大提高了用戶體驗(yàn),讓人愛不釋手。

說到這,大家先來了解一下這幾個概念:

  • 文件分塊:將大文件拆分成小文件,將小文件上傳\下載,最后再將小文件組裝成大文件;

  • 斷點(diǎn)續(xù)傳:在文件分塊的基礎(chǔ)上,將每個小文件采用單獨(dú)的線程進(jìn)行上傳\下載,如果碰到網(wǎng)絡(luò)故障,可以從已經(jīng)上傳\下載的部分開始繼續(xù)上傳\下載未完成的部分,而沒有必要從頭開始上傳\下載;

  • 文件秒傳:資源服務(wù)器中已經(jīng)存在該文件,其他人上傳時直接返回該文件的URI。

RandomAccessFile

平時我們都會使用FileInputStream,F(xiàn)ileOutputStream,F(xiàn)ileReader以及FileWriter等IO流來讀取文件,今天我們來了解一下RandomAccessFile。

它是一個直接繼承Object的獨(dú)立的類,底層實(shí)現(xiàn)中它實(shí)現(xiàn)的是DataInput和DataOutput接口。該類支持隨機(jī)讀取文件,隨機(jī)訪問文件類似于文件系統(tǒng)中存儲的大字節(jié)數(shù)組。

它的實(shí)現(xiàn)基于文件指針(一種游標(biāo)或者指向隱含數(shù)組的索引),文件指針可以通過getFilePointer方法讀取,也可以通過seek方法設(shè)置。

輸入時從文件指針開始讀取字節(jié),并使文件指針超過讀取的字節(jié),如果寫入超過隱含數(shù)組當(dāng)前結(jié)尾的輸出操作會導(dǎo)致擴(kuò)展數(shù)組。該類有四種模式可供選擇:

  • r:以只讀方式打開文件,如果執(zhí)行寫入操作會拋出IOException;

  • rw:以讀、寫方式打開文件,如果文件不存在,則嘗試創(chuàng)建文件;

  • rws:以讀、寫方式打開文件,要求對文件內(nèi)容或元數(shù)據(jù)的每次更新都同步寫入底層存儲設(shè)備;

  • rwd:以讀、寫方式打開文件,要求對文件內(nèi)容的每次更新都同步寫入底層存儲設(shè)備;

在rw模式下,默認(rèn)是使用buffer的,只有cache滿的或者使用RandomAccessFile.close()關(guān)閉流的時候才真正的寫到文件。

API

1、void seek(long pos):設(shè)置下一次讀取或?qū)懭霑r的文件指針偏移量,通俗點(diǎn)說就是指定下次讀文件數(shù)據(jù)的位置。

偏移量可以設(shè)置在文件末尾之外,只有在偏移量設(shè)置超出文件末尾后,才能通過寫入更改文件長度;

2、native long getFilePointer():返回當(dāng)前文件的光標(biāo)位置;

3、native long length():返回當(dāng)前文件的長度;

4、方法


img

5、方法


img

6、readFully(byte[] b):這個方法的作用就是將文本中的內(nèi)容填滿這個緩沖區(qū)b。如果緩沖b不能被填滿,那么讀取流的過程將被阻塞,如果發(fā)現(xiàn)是流的結(jié)尾,那么會拋出異常;

7、FileChannel getChannel():返回與此文件關(guān)聯(lián)的唯一FileChannel對象;

8、int skipBytes(int n):試圖跳過n個字節(jié)的輸入,丟棄跳過的字節(jié);

RandomAccessFile的絕大多數(shù)功能,已經(jīng)被JDK1.4的NIO的內(nèi)存映射文件取代了,即把文件映射到內(nèi)存后再操作,省去了頻繁磁盤io。

主菜

總結(jié)經(jīng)驗(yàn),砥礪前行:之前的實(shí)戰(zhàn)文章中過多的粘貼了源碼,影響了各位小伙伴的閱讀感受。經(jīng)過大佬的點(diǎn)撥,以后將展示部分關(guān)鍵代碼,供各位賞析

文件分塊需要在前端進(jìn)行處理,可以利用強(qiáng)大的js庫或者現(xiàn)成的組件進(jìn)行分塊處理。需要確定分塊的大小和分塊的數(shù)量,然后為每一個分塊指定一個索引值。

為了防止上傳文件的分塊與其它文件混淆,采用文件的md5值來進(jìn)行區(qū)分,該值也可以用來校驗(yàn)服務(wù)器上是否存在該文件以及文件的上傳狀態(tài)。

  • 如果文件存在,直接返回文件地址;

  • 如果文件不存在,但是有上傳狀態(tài),即部分分塊上傳成功,則返回未上傳的分塊索引數(shù)組;

  • 如果文件不存在,且上傳狀態(tài)為空,則所有分塊均需要上傳。

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) { ? ? ? ? ? ? ? ? ? ?//還有幾片未上傳情況,斷點(diǎn)續(xù)傳 ? ? ? ? ? ? ? ? ? ?chunkArr = res.data.data; ? ? ? ? ? ? ? ?} ? ? ? ? ? ? ? ?readChunkMD5(); ? ? ? ? ? ?} ? ? ? ?}) ? ? ? ?.catch((e) => {}); });

在調(diào)用上傳接口前,通過slice方法來取出索引在文件中對應(yīng)位置的分塊。

const getChunkInfo = (file, currentChunk, chunkSize) => { ? ? ? //獲取對應(yīng)下標(biāo)下的文件片段 ? ? ? let start = currentChunk * chunkSize; ? ? ? let end = Math.min(file.size, start + chunkSize); ? ? ? //對文件分塊 ? ? ? let chunk = file.slice(start, end); ? ? ? return { start, end, chunk }; ? };

之后調(diào)用上傳接口完成上傳。

斷點(diǎn)續(xù)傳、文件秒傳

后端基于spring boot開發(fā),使用redis來存儲上傳文件的狀態(tài)和上傳文件的地址。

如果文件完整上傳,返回文件路徑;部分上傳則返回未上傳的分塊數(shù)組;如果未上傳過返回提示信息。

在上傳分塊時會產(chǎn)生兩個文件,一個是文件主體,一個是臨時文件。臨時文件可以看做是一個數(shù)組文件,為每一個分塊分配一個值為127的字節(jié)。

校驗(yàn)MD5值時會用到兩個值:

  • 文件上傳狀態(tài):只要該文件上傳過就不為空,如果完整上傳則為true,部分上傳返回false;

  • 文件上傳地址:如果文件完整上傳,返回文件路徑;部分上傳返回臨時文件路徑。

/** * 校驗(yàn)文件的MD5 **/public Result checkFileMd5(String md5) throws IOException { ? ?//文件是否上傳狀態(tài):只要該文件上傳過該值一定存在 ? ?Object processingObj = stringRedisTemplate.opsForHash().get(UploadConstants.FILE_UPLOAD_STATUS, md5); ? ?if (processingObj == null) { ? ? ? ?return Result.ok("該文件沒有上傳過"); ? ?} ? ?boolean processing = Boolean.parseBoolean(processingObj.toString()); ? ?//完整文件上傳完成時為文件的路徑,如果未完成返回臨時文件路徑(臨時文件相當(dāng)于數(shù)組,為每個分塊分配一個值為127的字節(jié)) ? ?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) { ? ? ? ? ? ? ? ?//用空格補(bǔ)齊 ? ? ? ? ? ? ? ?missChunkList.add(i); ? ? ? ? ? ?} ? ? ? ?} ? ? ? ?return Result.ok(missChunkList,"該文件上傳了一部分"); ? ?} }

說到這,你肯定會問:當(dāng)這個文件的所有分塊上傳完成之后,該怎么得到完整的文件呢?接下來我們就說一下分塊合并的問題。

分塊上傳、文件合并

上邊我們提到了利用文件的md5值來維護(hù)分塊和文件的關(guān)系,因此我們會將具有相同md5值的分塊進(jìn)行合并,由于每個分塊都有自己的索引值,所以我們會將分塊按索引像插入數(shù)組一樣分別插入文件中,形成完整的文件。

分塊上傳時,要和前端的分塊大小、分塊數(shù)量、當(dāng)前分塊索引等對應(yīng)好,以備文件合并時使用,此處我們采用的是磁盤映射的方式來合并文件。

//讀操作和寫操作都是允許的RandomAccessFile tempRaf = new RandomAccessFile(tmpFile, "rw");//它返回的就是nio通信中的file的唯一channelFileChannel fileChannel = tempRaf.getChannel();//寫入該分片數(shù)據(jù) ? 分片大小 * 第幾塊分片獲取偏移量long offset = CHUNK_SIZE * multipartFileDTO.getChunk();//分片文件大小byte[] fileData = multipartFileDTO.getFile().getBytes();//將文件的區(qū)域直接映射到內(nèi)存MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, offset, fileData.length); mappedByteBuffer.put(fileData);// 釋放FileMD5Util.freedMappedByteBuffer(mappedByteBuffer); fileChannel.close();

每當(dāng)完成一次分塊的上傳,還需要去檢查文件的上傳進(jìn)度,看文件是否上傳完成。

RandomAccessFile accessConfFile = new RandomAccessFile(confFile, "rw");//把該分段標(biāo)記為 true 表示完成accessConfFile.setLength(multipartFileDTO.getChunks()); accessConfFile.seek(multipartFileDTO.getChunk()); accessConfFile.write(Byte.MAX_VALUE);//completeList 檢查是否全部完成,如果數(shù)組里是否全部都是(全部分片都成功上傳)byte[] completeList = FileUtils.readFileToByteArray(confFile);byte isComplete = Byte.MAX_VALUE;for (int i = 0; i < completeList.length && isComplete == Byte.MAX_VALUE; i++) { ? ?//與運(yùn)算, 如果有部分沒有完成則 isComplete 不是 Byte.MAX_VALUE ? ?isComplete = (byte) (isComplete & completeList[i]); } accessConfFile.close();

然后更新文件的上傳進(jìn)度到Redis中。

//更新redis中的狀態(tài):如果是true的話證明是已經(jīng)該大文件全部上傳完成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"); ? ?} }


最后說一句(別白嫖,求關(guān)注)

每一篇文章都是精心輸出,如果這篇文章對你有所幫助,或者有所啟發(fā)的話,幫忙點(diǎn)贊、在看、轉(zhuǎn)發(fā)、收藏,你的支持就是我堅(jiān)持下去的最大動力!


實(shí)戰(zhàn):斷點(diǎn)續(xù)傳?文件秒傳?手?jǐn)]大文件上傳的評論 (共 條)

分享到微博請遵守國家法律
姜堰市| 元氏县| 彩票| 太和县| 合肥市| 新巴尔虎左旗| 区。| 宜州市| 华容县| 上饶县| 根河市| 阿勒泰市| 临高县| 汕尾市| 邳州市| 元江| 仁化县| 敦化市| 边坝县| 九龙坡区| 建平县| 汉中市| 胶南市| 阿图什市| 清苑县| 项城市| 博客| 大名县| 邵东县| 东城区| 隆安县| 沅陵县| 家居| 闽侯县| 岑溪市| 乌恰县| 康平县| 泰宁县| 平舆县| 苍溪县| 陇川县|