1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        SpringBoot超大文件實(shí)現(xiàn)秒傳!網(wǎng)友:這技術(shù)NB...

        共 11211字,需瀏覽 23分鐘

         ·

        2022-12-05 07:40

        前言

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

        017f6dd5e26475af4189300a863d851d.webp

        詳細(xì)教程

        秒傳

        1、什么是秒傳

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

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

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

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

        分片上傳

        1.什么是分片上傳

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

        2.分片上傳的場景

        1.大文件上傳

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

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

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

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

        2、應(yīng)用場景

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

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

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

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

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

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

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

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

        • 前端(客戶端)需要根據(jù)固定大小對文件進(jìn)行分片,請求后端(服務(wù)端)時要帶上分片序號和大小
        • 服務(wù)端創(chuàng)建conf文件用來記錄分塊位置,conf文件長度為總分片數(shù),每上傳一個分塊即向conf文件中寫入一個127,那么沒上傳的位置就是默認(rèn)的0,已上傳的就是Byte.MAX_VALUE 127(這步是實(shí)現(xiàn)斷點(diǎn)續(xù)傳和秒傳的核心步驟)
        • 服務(wù)器按照請求數(shù)據(jù)中給的分片序號和每片分塊大?。ǚ制笮∈枪潭ㄇ乙粯拥模┧愠鲩_始位置,與讀取到的文件片段數(shù)據(jù),寫入文件。另外,搜索公眾號Linux就該這樣學(xué)后臺回復(fù)“電子書”,獲取一份驚喜禮包。
        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,如果對RandomAccessFile不熟悉的朋友,可以查看如下鏈接:

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

        另一種是使用MappedByteBuffer,對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");??
        ??????//這個必須與前端設(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<Integer,?String>?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ù),每上傳一個分塊即向conf文件中寫入一個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?<?completeList.length?&&?isComplete?==?Byte.MAX_VALUE;?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)分片上傳的過程,需要前端和后端配合,比如前后端的上傳塊號的文件大小,前后端必須得要一致,否則上傳就會有問題。其次文件相關(guān)操作正常都是要搭建一個文件服務(wù)器的,比如使用fastdfs、hdfs等。

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

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

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

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

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


        文章來源:https://mp.weixin.qq.com/s/tic5dU8Yd86uVRW94_QxUw
            

        覺得本文對你有幫助?請分享給更多人

        關(guān)注「 全棧開發(fā)者社區(qū) 」加星標(biāo),提升全棧技能

        本公眾號會不定期給大家發(fā)福利,包括送書、學(xué)習(xí)資源等,敬請期待吧!

        如果感覺推送內(nèi)容不錯,不妨右下角點(diǎn)個 在看 轉(zhuǎn)發(fā)朋友圈或收藏,感謝支持。

        好文章,留言、點(diǎn)贊、 在看和分享一條龍

        瀏覽 82
        點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            久久免费少妇高潮99精品 | 亚洲精品无码视频 | 精品美女aⅴ成人啪啪高潮 | 大香蕉第一页 | 国产伦精品一区二区三区视频金莲 | 国产综合色在线视频 | 免费看污黄网站在线观看 | 亚洲日韩在线视频 | 美女被操视频在线 | 97操逼视频 |