實戰篇:斷點續傳?文件秒傳?手擼大文件上傳

      網友投稿 756 2025-04-04

      各位看官大家好,今天給大家分享的又是一篇實戰文章,希望大家能夠喜歡。


      開味菜

      最近接到一個新的需求,需要上傳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()關閉流的時候才真正的寫到文件。

      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 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"); } }

      回復break可獲取完整源碼呦!

      數據結構 網絡

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:wps文字如何快速填充表格序號(wps文檔怎么快速填充序號)
      下一篇:怎么設置段落默認?(怎樣設置段落)
      相關文章
      亚洲经典在线观看| 亚洲午夜久久久久妓女影院| 亚洲中文字幕无码日韩| 亚洲人成电影网站国产精品 | 亚洲中文字幕无码久久综合网| 五月天婷亚洲天综合网精品偷| 亚洲成在人线在线播放无码| 亚洲日本在线电影| 亚洲乱亚洲乱妇无码| 亚洲一卡2卡三卡4卡无卡下载| 亚洲人成电影网站久久| 久久亚洲精品专区蓝色区| 亚洲1区1区3区4区产品乱码芒果| 亚洲成aⅴ人片在线观| 亚洲综合一区二区精品导航| 久久久无码精品亚洲日韩按摩| 亚洲色图在线播放| 亚洲AV无码第一区二区三区| 亚洲综合一区二区国产精品| 亚洲视频免费观看| 亚洲午夜电影一区二区三区| 亚洲av成人综合网| 亚洲性色精品一区二区在线| 亚洲AV永久无码精品网站在线观看 | 久久精品国产亚洲AV无码娇色| 在线观看亚洲人成网站| 亚洲日韩乱码中文无码蜜桃臀| 亚洲国产精品专区| 亚洲一区二区三区高清在线观看 | 黑人大战亚洲人精品一区| 亚洲宅男天堂在线观看无病毒| 亚洲精品高清无码视频| 亚洲午夜久久久精品影院| 亚洲视频免费在线看| 亚洲已满18点击进入在线观看| 亚洲AV日韩综合一区| 亚洲欧洲国产成人综合在线观看 | 亚洲另类图片另类电影| 亚洲一线产品二线产品| 亚洲国产成人精品无码久久久久久综合| 亚洲午夜国产片在线观看|