<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 10036字,需瀏覽 21分鐘

           ·

          2021-08-24 16:18

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

          開味菜

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

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

          說(shuō)到這,大家先來(lái)了解一下這幾個(gè)概念:

          • 「文件分塊」:將大文件拆分成小文件,將小文件上傳\下載,最后再將小文件組裝成大文件;
          • 「斷點(diǎn)續(xù)傳」:在文件分塊的基礎(chǔ)上,將每個(gè)小文件采用單獨(dú)的線程進(jìn)行上傳\下載,如果碰到網(wǎng)絡(luò)故障,可以從已經(jīng)上傳\下載的部分開始繼續(xù)上傳\下載未完成的部分,而沒(méi)有必要從頭開始上傳\下載;
          • 「文件秒傳」:資源服務(wù)器中已經(jīng)存在該文件,其他人上傳時(shí)直接返回該文件的URI。

          RandomAccessFile

          平時(shí)我們都會(huì)使用FileInputStreamFileOutputStreamFileReader以及FileWriterIO流來(lái)讀取文件,今天我們來(lái)了解一下RandomAccessFile

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

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

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

          • r: 以只讀方式打開文件,如果執(zhí)行寫入操作會(huì)拋出IOException;
          • rw: 以讀、寫方式打開文件,如果文件不存在,則嘗試創(chuàng)建文件;
          • rws: 以讀、寫方式打開文件,要求對(duì)文件內(nèi)容或元數(shù)據(jù)的每次更新都同步寫入底層存儲(chǔ)設(shè)備;
          • rwd: 以讀、寫方式打開文件,要求對(duì)文件內(nèi)容的每次更新都同步寫入底層存儲(chǔ)設(shè)備;

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

          API

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

          ?

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

          ?

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

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

          4、「讀」方法

          5、「寫」方法

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

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

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

          ?

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

          ?

          主菜

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

          文件分塊

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

          為了防止上傳文件的分塊與其它文件混淆,采用文件的md5值來(lái)進(jìn)行區(qū)分,該值也可以用來(lái)校驗(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 == "文件已存在") {
                          //文件已存在不走后面分片了,直接返回文件地址到前臺(tái)頁(yè)面
                          success && success(res);
                      } else {
                          //文件不存在存在兩種情況,一種是返回data:null代表未上傳過(guò) 一種是data:[xx,xx] 還有哪幾片未上傳
                          if (!res.data.data) {
                              //還有幾片未上傳情況,斷點(diǎn)續(xù)傳
                              chunkArr = res.data.data;
                          }
                          readChunkMD5();
                      }
                  })
                  .catch((e) => {});
          });

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

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

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

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

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

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

          ?

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

          ?

          校驗(yàn)MD5值時(shí)會(huì)用到兩個(gè)值:

          • 文件上傳狀態(tài):只要該文件上傳過(guò)就不為空,如果完整上傳則為true,部分上傳返回false
          • 文件上傳地址:如果文件完整上傳,返回文件路徑;部分上傳返回臨時(shí)文件路徑。
          /**
           * 校驗(yàn)文件的MD5
           **/

          public Result checkFileMd5(String md5) throws IOException {
              //文件是否上傳狀態(tài):只要該文件上傳過(guò)該值一定存在
              Object processingObj = stringRedisTemplate.opsForHash().get(UploadConstants.FILE_UPLOAD_STATUS, md5);
              if (processingObj == null) {
                  return Result.ok("該文件沒(méi)有上傳過(guò)");
              }
              boolean processing = Boolean.parseBoolean(processingObj.toString());
              //完整文件上傳完成時(shí)為文件的路徑,如果未完成返回臨時(shí)文件路徑(臨時(shí)文件相當(dāng)于數(shù)組,為每個(gè)分塊分配一個(gè)值為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,"該文件上傳了一部分");
              }
          }

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

          分塊上傳、文件合并

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

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

           //讀操作和寫操作都是允許的
          RandomAccessFile tempRaf = new RandomAccessFile(tmpFile, "rw");
          //它返回的就是nio通信中的file的唯一channel
          FileChannel 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)算, 如果有部分沒(méi)有完成則 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");
              }
          }


          分享一下我寫的《10萬(wàn)字Springboot經(jīng)典學(xué)習(xí)筆記》中,點(diǎn)擊下面小卡片,進(jìn)入【Java禿頭哥】,回復(fù):筆記,即可免費(fèi)獲取。

          點(diǎn)贊是最大的支持 

          瀏覽 65
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  玖玖精品 | 天天色天天干天天日 | 天天日天天干天天操 | 国产成人无码A片免费看 | 波多野结衣中文字幕乱码 |