<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>

          大文件上傳:秒傳、斷點(diǎn)續(xù)傳、分片上傳

          共 6707字,需瀏覽 14分鐘

           ·

          2021-12-23 05:25

          來源:已賦值(作者-小度爺)

          前言

          文件上傳是一個(gè)老生常談的話題了,在文件相對(duì)比較小的情況下,可以直接把文件轉(zhuǎn)化為字節(jié)流上傳到服務(wù)器,但在文件比較大的情況下,用普通的方式進(jìn)行上傳,這可不是一個(gè)好的辦法,畢竟很少有人會(huì)忍受,當(dāng)文件上傳到一半中斷后,繼續(xù)上傳卻只能重頭開始上傳,這種讓人不爽的體驗(yàn)。那有沒有比較好的上傳體驗(yàn)?zāi)兀鸢赣械模褪窍逻呉榻B的幾種上傳方式

          詳細(xì)教程

          秒傳

          1、什么是秒傳

          通俗的說,你把要上傳的東西上傳,服務(wù)器會(huì)先做MD5校驗(yàn),如果服務(wù)器上有一樣的東西,它就直接給你個(gè)新地址,其實(shí)你下載的都是服務(wù)器上的同一個(gè)文件,想要不秒傳,其實(shí)只要讓MD5改變,就是對(duì)文件本身做一下修改(改名字不行),例如一個(gè)文本文件,你多加幾個(gè)字,MD5就變了,就不會(huì)秒傳了.

          2、本文實(shí)現(xiàn)的秒傳核心邏輯

          a、利用redis的set方法存放文件上傳狀態(tài),其中key為文件上傳的md5,value為是否上傳完成的標(biāo)志位,

          b、當(dāng)標(biāo)志位true為上傳已經(jīng)完成,此時(shí)如果有相同文件上傳,則進(jìn)入秒傳邏輯。如果標(biāo)志位為false,則說明還沒上傳完成,此時(shí)需要在調(diào)用set的方法,保存塊號(hào)文件記錄的路徑,其中key為上傳文件md5加一個(gè)固定前綴,value為塊號(hào)文件記錄路徑

          分片上傳

          1.什么是分片上傳

          分片上傳,就是將所要上傳的文件,按照一定的大小,將整個(gè)文件分隔成多個(gè)數(shù)據(jù)塊(我們稱之為Part)來進(jìn)行分別上傳,上傳完之后再由服務(wù)端對(duì)所有上傳的文件進(jìn)行匯總整合成原始的文件。

          2.分片上傳的場(chǎng)景

          1.大文件上傳

          2.網(wǎng)絡(luò)環(huán)境環(huán)境不好,存在需要重傳風(fēng)險(xiǎn)的場(chǎng)景

          斷點(diǎn)續(xù)傳

          1、什么是斷點(diǎn)續(xù)傳

          斷點(diǎn)續(xù)傳是在下載或上傳時(shí),將下載或上傳任務(wù)(一個(gè)文件或一個(gè)壓縮包)人為的劃分為幾個(gè)部分,每一個(gè)部分采用一個(gè)線程進(jìn)行上傳或下載,如果碰到網(wǎng)絡(luò)故障,可以從已經(jīng)上傳或下載的部分開始繼續(xù)上傳或者下載未完成的部分,而沒有必要從頭開始上傳或者下載。本文的斷點(diǎn)續(xù)傳主要是針對(duì)斷點(diǎn)上傳場(chǎng)景。

          2、應(yīng)用場(chǎng)景

          斷點(diǎn)續(xù)傳可以看成是分片上傳的一個(gè)衍生,因此可以使用分片上傳的場(chǎng)景,都可以使用斷點(diǎn)續(xù)傳。

          3、實(shí)現(xiàn)斷點(diǎn)續(xù)傳的核心邏輯

          在分片上傳的過程中,如果因?yàn)橄到y(tǒng)崩潰或者網(wǎng)絡(luò)中斷等異常因素導(dǎo)致上傳中斷,這時(shí)候客戶端需要記錄上傳的進(jìn)度。在之后支持再次上傳時(shí),可以繼續(xù)從上次上傳中斷的地方進(jìn)行繼續(xù)上傳。

          為了避免客戶端在上傳之后的進(jìn)度數(shù)據(jù)被刪除而導(dǎo)致重新開始從頭上傳的問題,服務(wù)端也可以提供相應(yīng)的接口便于客戶端對(duì)已經(jīng)上傳的分片數(shù)據(jù)進(jìn)行查詢,從而使客戶端知道已經(jīng)上傳的分片數(shù)據(jù),從而從下一個(gè)分片數(shù)據(jù)開始繼續(xù)上傳。

          4、實(shí)現(xiàn)流程步驟

          a、方案一,常規(guī)步驟

          • 將需要上傳的文件按照一定的分割規(guī)則,分割成相同大小的數(shù)據(jù)塊;
          • 初始化一個(gè)分片上傳任務(wù),返回本次分片上傳唯一標(biāo)識(shí);
          • 按照一定的策略(串行或并行)發(fā)送各個(gè)分片數(shù)據(jù)塊;
          • 發(fā)送完成后,服務(wù)端根據(jù)判斷數(shù)據(jù)上傳是否完整,如果完整,則進(jìn)行數(shù)據(jù)塊合成得到原始文件。

          b、方案二、本文實(shí)現(xiàn)的步驟

          • 前端(客戶端)需要根據(jù)固定大小對(duì)文件進(jìn)行分片,請(qǐng)求后端(服務(wù)端)時(shí)要帶上分片序號(hào)和大小
          • 服務(wù)端創(chuàng)建conf文件用來記錄分塊位置,conf文件長度為總分片數(shù),每上傳一個(gè)分塊即向conf文件中寫入一個(gè)127,那么沒上傳的位置就是默認(rèn)的0,已上傳的就是Byte.MAX_VALUE 127(這步是實(shí)現(xiàn)斷點(diǎn)續(xù)傳和秒傳的核心步驟)
          • 服務(wù)器按照請(qǐng)求數(shù)據(jù)中給的分片序號(hào)和每片分塊大小(分片大小是固定且一樣的)算出開始位置,與讀取到的文件片段數(shù)據(jù),寫入文件。
          5、分片上傳/斷點(diǎn)上傳代碼實(shí)現(xiàn)

          a、前端采用百度提供的webuploader的插件,進(jìn)行分片。因本文主要介紹服務(wù)端代碼實(shí)現(xiàn),webuploader如何進(jìn)行分片,具體實(shí)現(xiàn)可以查看如下鏈接:

          http://fex.baidu.com/webuploader/getting-started.html

          b、后端用兩種方式實(shí)現(xiàn)文件寫入,一種是用RandomAccessFile,如果對(duì)RandomAccessFile不熟悉的朋友,可以查看如下鏈接:

          https://blog.csdn.net/dimudan2015/article/details/81910690

          另一種是使用MappedByteBuffer,對(duì)MappedByteBuffer不熟悉的朋友,可以查看如下鏈接進(jìn)行了解:

          https://www.jianshu.com/p/f90866dcbffc

          后端進(jìn)行寫入操作的核心代碼

          a、RandomAccessFile實(shí)現(xiàn)方式

          @UploadMode(mode?=?UploadModeEnum.RANDOM_ACCESS)??
          @Slf4j??
          public?class?RandomAccessUploadStrategy?extends?SliceUploadTemplate?{??
          ??
          ??@Autowired??
          ??private?FilePathUtil?filePathUtil;??
          ??
          ??@Value("${upload.chunkSize}")??
          ??private?long?defaultChunkSize;??
          ??
          ??@Override??
          ??public?boolean?upload(FileUploadRequestDTO?param)?{??
          ????RandomAccessFile?accessTmpFile?=?null;??
          ????try?{??
          ??????String?uploadDirPath?=?filePathUtil.getPath(param);??
          ??????File?tmpFile?=?super.createTmpFile(param);??
          ??????accessTmpFile?=?new?RandomAccessFile(tmpFile,?"rw");??
          ??????//這個(gè)必須與前端設(shè)定的值一致??
          ??????long?chunkSize?=?Objects.isNull(param.getChunkSize())???defaultChunkSize?*?1024?*?1024??
          ??????????:?param.getChunkSize();??
          ??????long?offset?=?chunkSize?*?param.getChunk();??
          ??????//定位到該分片的偏移量??
          ??????accessTmpFile.seek(offset);??
          ??????//寫入該分片數(shù)據(jù)??
          ??????accessTmpFile.write(param.getFile().getBytes());??
          ??????boolean?isOk?=?super.checkAndSetUploadProgress(param,?uploadDirPath);??
          ??????return?isOk;??
          ????}?catch?(IOException?e)?{??
          ??????log.error(e.getMessage(),?e);??
          ????}?finally?{??
          ??????FileUtil.close(accessTmpFile);??
          ????}??
          ???return?false;??
          ??}??
          ??
          }??

          b、MappedByteBuffer實(shí)現(xiàn)方式

          @UploadMode(mode?=?UploadModeEnum.MAPPED_BYTEBUFFER)??
          @Slf4j??
          public?class?MappedByteBufferUploadStrategy?extends?SliceUploadTemplate?{??
          ??
          ??@Autowired??
          ??private?FilePathUtil?filePathUtil;??
          ??
          ??@Value("${upload.chunkSize}")??
          ??private?long?defaultChunkSize;??
          ??
          ??@Override??
          ??public?boolean?upload(FileUploadRequestDTO?param)?{??
          ??
          ????RandomAccessFile?tempRaf?=?null;??
          ????FileChannel?fileChannel?=?null;??
          ????MappedByteBuffer?mappedByteBuffer?=?null;??
          ????try?{??
          ??????String?uploadDirPath?=?filePathUtil.getPath(param);??
          ??????File?tmpFile?=?super.createTmpFile(param);??
          ??????tempRaf?=?new?RandomAccessFile(tmpFile,?"rw");??
          ??????fileChannel?=?tempRaf.getChannel();??
          ??
          ??????long?chunkSize?=?Objects.isNull(param.getChunkSize())???defaultChunkSize?*?1024?*?1024??
          ??????????:?param.getChunkSize();??
          ??????//寫入該分片數(shù)據(jù)??
          ??????long?offset?=?chunkSize?*?param.getChunk();??
          ??????byte[]?fileData?=?param.getFile().getBytes();??
          ??????mappedByteBuffer?=?fileChannel??
          .map(FileChannel.MapMode.READ_WRITE,?offset,?fileData.length);??
          ??????mappedByteBuffer.put(fileData);??
          ??????boolean?isOk?=?super.checkAndSetUploadProgress(param,?uploadDirPath);??
          ??????return?isOk;??
          ??
          ????}?catch?(IOException?e)?{??
          ??????log.error(e.getMessage(),?e);??
          ????}?finally?{??
          ??
          ??????FileUtil.freedMappedByteBuffer(mappedByteBuffer);??
          ??????FileUtil.close(fileChannel);??
          ??????FileUtil.close(tempRaf);??
          ??
          ????}??
          ??
          ????return?false;??
          ??}??
          ??
          }??

          c、文件操作核心模板類代碼

          @Slf4j??
          public?abstract?class?SliceUploadTemplate?implements?SliceUploadStrategy?{??
          ??
          ??public?abstract?boolean?upload(FileUploadRequestDTO?param);??
          ??
          ??protected?File?createTmpFile(FileUploadRequestDTO?param)?{??
          ??
          ????FilePathUtil?filePathUtil?=?SpringContextHolder.getBean(FilePathUtil.class);??
          ????param.setPath(FileUtil.withoutHeadAndTailDiagonal(param.getPath()));??
          ????String?fileName?=?param.getFile().getOriginalFilename();??
          ????String?uploadDirPath?=?filePathUtil.getPath(param);??
          ????String?tempFileName?=?fileName?+?"_tmp";??
          ????File?tmpDir?=?new?File(uploadDirPath);??
          ????File?tmpFile?=?new?File(uploadDirPath,?tempFileName);??
          ????if?(!tmpDir.exists())?{??
          ??????tmpDir.mkdirs();??
          ????}??
          ????return?tmpFile;??
          ??}??
          ??
          ??@Override??
          ??public?FileUploadDTO?sliceUpload(FileUploadRequestDTO?param)?{??
          ??
          ????boolean?isOk?=?this.upload(param);??
          ????if?(isOk)?{??
          ??????File?tmpFile?=?this.createTmpFile(param);??
          ??????FileUploadDTO?fileUploadDTO?=?this.saveAndFileUploadDTO(param.getFile().getOriginalFilename(),?tmpFile);??
          ??????return?fileUploadDTO;??
          ????}??
          ????String?md5?=?FileMD5Util.getFileMD5(param.getFile());??
          ??
          ????Map?map?=?new?HashMap<>();??
          ????map.put(param.getChunk(),?md5);??
          ????return?FileUploadDTO.builder().chunkMd5Info(map).build();??
          ??}??
          ??
          ??/**??
          ???*?檢查并修改文件上傳進(jìn)度??
          ???*/
          ??
          ??public?boolean?checkAndSetUploadProgress(FileUploadRequestDTO?param,?String?uploadDirPath)?{??
          ??
          ????String?fileName?=?param.getFile().getOriginalFilename();??
          ????File?confFile?=?new?File(uploadDirPath,?fileName?+?".conf");??
          ????byte?isComplete?=?0;??
          ????RandomAccessFile?accessConfFile?=?null;??
          ????try?{??
          ??????accessConfFile?=?new?RandomAccessFile(confFile,?"rw");??
          ??????//把該分段標(biāo)記為?true?表示完成??
          ??????System.out.println("set?part?"?+?param.getChunk()?+?"?complete");??
          ??????//創(chuàng)建conf文件文件長度為總分片數(shù),每上傳一個(gè)分塊即向conf文件中寫入一個(gè)127,那么沒上傳的位置就是默認(rèn)0,已上傳的就是Byte.MAX_VALUE?127??
          ??????accessConfFile.setLength(param.getChunks());??
          ??????accessConfFile.seek(param.getChunk());??
          ??????accessConfFile.write(Byte.MAX_VALUE);??
          ??
          ??????//completeList?檢查是否全部完成,如果數(shù)組里是否全部都是127(全部分片都成功上傳)??
          ??????byte[]?completeList?=?FileUtils.readFileToByteArray(confFile);??
          ??????isComplete?=?Byte.MAX_VALUE;??
          ??????for?(int?i?=?0;?i?????????//與運(yùn)算,?如果有部分沒有完成則?isComplete?不是?Byte.MAX_VALUE??
          ????????isComplete?=?(byte)?(isComplete?&?completeList[i]);??
          ????????System.out.println("check?part?"?+?i?+?"?complete?:"?+?completeList[i]);??
          ??????}??
          ??
          ????}?catch?(IOException?e)?{??
          ??????log.error(e.getMessage(),?e);??
          ????}?finally?{??
          ??????FileUtil.close(accessConfFile);??
          ????}??
          ?boolean?isOk?=?setUploadProgress2Redis(param,?uploadDirPath,?fileName,?confFile,?isComplete);??
          ????return?isOk;??
          ??}??
          ??
          ??/**??
          ???*?把上傳進(jìn)度信息存進(jìn)redis??
          ???*/
          ??
          ??private?boolean?setUploadProgress2Redis(FileUploadRequestDTO?param,?String?uploadDirPath,??
          ??????String?fileName,?File?confFile,?byte?isComplete)
          ?
          {??
          ??
          ????RedisUtil?redisUtil?=?SpringContextHolder.getBean(RedisUtil.class);??
          ????if?(isComplete?==?Byte.MAX_VALUE)?{??
          ??????redisUtil.hset(FileConstant.FILE_UPLOAD_STATUS,?param.getMd5(),?"true");??
          ??????redisUtil.del(FileConstant.FILE_MD5_KEY?+?param.getMd5());??
          ??????confFile.delete();??
          ??????return?true;??
          ????}?else?{??
          ??????if?(!redisUtil.hHasKey(FileConstant.FILE_UPLOAD_STATUS,?param.getMd5()))?{??
          ????????redisUtil.hset(FileConstant.FILE_UPLOAD_STATUS,?param.getMd5(),?"false");??
          ????????redisUtil.set(FileConstant.FILE_MD5_KEY?+?param.getMd5(),??
          ????????????uploadDirPath?+?FileConstant.FILE_SEPARATORCHAR?+?fileName?+?".conf");??
          ??????}??
          ??
          ??????return?false;??
          ????}??
          ??}??
          /**??
          ???*?保存文件操作??
          ???*/
          ??
          ??public?FileUploadDTO?saveAndFileUploadDTO(String?fileName,?File?tmpFile)?{??
          ??
          ????FileUploadDTO?fileUploadDTO?=?null;??
          ??
          ????try?{??
          ??
          ??????fileUploadDTO?=?renameFile(tmpFile,?fileName);??
          ??????if?(fileUploadDTO.isUploadComplete())?{??
          ????????System.out??
          ????????????.println("upload?complete?!!"?+?fileUploadDTO.isUploadComplete()?+?"?name="?+?fileName);??
          ????????//TODO?保存文件信息到數(shù)據(jù)庫??
          ??
          ??????}??
          ??
          ????}?catch?(Exception?e)?{??
          ??????log.error(e.getMessage(),?e);??
          ????}?finally?{??
          ??
          ????}??
          ????return?fileUploadDTO;??
          ??}??
          /**??
          ???*?文件重命名??
          ???*??
          ???*?@param?toBeRenamed?將要修改名字的文件??
          ???*?@param?toFileNewName?新的名字??
          ???*/
          ??
          ??private?FileUploadDTO?renameFile(File?toBeRenamed,?String?toFileNewName)?{??
          ????//檢查要重命名的文件是否存在,是否是文件??
          ????FileUploadDTO?fileUploadDTO?=?new?FileUploadDTO();??
          ????if?(!toBeRenamed.exists()?||?toBeRenamed.isDirectory())?{??
          ??????log.info("File?does?not?exist:?{}",?toBeRenamed.getName());??
          ??????fileUploadDTO.setUploadComplete(false);??
          ??????return?fileUploadDTO;??
          ????}??
          ????String?ext?=?FileUtil.getExtension(toFileNewName);??
          ????String?p?=?toBeRenamed.getParent();??
          ????String?filePath?=?p?+?FileConstant.FILE_SEPARATORCHAR?+?toFileNewName;??
          ????File?newFile?=?new?File(filePath);??
          ????//修改文件名??
          ????boolean?uploadFlag?=?toBeRenamed.renameTo(newFile);??
          ??
          ????fileUploadDTO.setMtime(DateUtil.getCurrentTimeStamp());??
          ????fileUploadDTO.setUploadComplete(uploadFlag);??
          ????fileUploadDTO.setPath(filePath);??
          ????fileUploadDTO.setSize(newFile.length());??
          ????fileUploadDTO.setFileExt(ext);??
          ????fileUploadDTO.setFileId(toFileNewName);??
          ??
          ????return?fileUploadDTO;??
          ??}??
          }??

          總結(jié)

          在實(shí)現(xiàn)分片上傳的過程,需要前端和后端配合,比如前后端的上傳塊號(hào)的文件大小,前后端必須得要一致,否則上傳就會(huì)有問題。其次文件相關(guān)操作正常都是要搭建一個(gè)文件服務(wù)器的,比如使用fastdfs、hdfs等。

          本示例代碼在電腦配置為4核內(nèi)存8G情況下,上傳24G大小的文件,上傳時(shí)間需要30多分鐘,主要時(shí)間耗費(fèi)在前端的md5值計(jì)算,后端寫入的速度還是比較快。如果項(xiàng)目組覺得自建文件服務(wù)器太花費(fèi)時(shí)間,且項(xiàng)目的需求僅僅只是上傳下載,那么推薦使用阿里的oss服務(wù)器,其介紹可以查看官網(wǎng):

          https://help.aliyun.com/product/31815.html

          阿里的oss它本質(zhì)是一個(gè)對(duì)象存儲(chǔ)服務(wù)器,而非文件服務(wù)器,因此如果有涉及到大量刪除或者修改文件的需求,oss可能就不是一個(gè)好的選擇。

          文末提供一個(gè)oss表單上傳的鏈接demo,通過oss表單上傳,可以直接從前端把文件上傳到oss服務(wù)器,把上傳的壓力都推給oss服務(wù)器:

          https://www.cnblogs.com/ossteam/p/4942227.html


          程序汪資料鏈接

          程序汪接的7個(gè)私活都在這里,經(jīng)驗(yàn)整理

          Java項(xiàng)目分享 最新整理全集,找項(xiàng)目不累啦 06版

          堪稱神級(jí)的Spring Boot手冊(cè),從基礎(chǔ)入門到實(shí)戰(zhàn)進(jìn)階

          臥槽!字節(jié)跳動(dòng)《算法中文手冊(cè)》火了,完整版 PDF 開放下載!

          臥槽!阿里大佬總結(jié)的《圖解Java》火了,完整版PDF開放下載!

          字節(jié)跳動(dòng)總結(jié)的設(shè)計(jì)模式 PDF 火了,完整版開放下載!


          歡迎添加程序汪個(gè)人微信 itwang005? 進(jìn)粉絲群或圍觀朋友圈

          瀏覽 48
          點(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>
                  日本美女操B | 久久午夜无码鲁丝片午夜精 | 成人91av | 北条麻妃高清无码视频 | 无码人妻一区二区三区在线神菜美 |