BigFileUpload
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsICN9YnJhRGOihDMjRWYzIDMkNDM4EjZjNTOhFGOxYTN2UmNyAjNkJmZ5EjN00TdmADM00zc-EDMyEzN4cTMvwVdvwVbvNmL05WZ052bjJXZzVnY1hGdpdmLzMnchRXY2F2Lc9CX6MHc0RHaiojIsJye.jpg)
目錄
- 背景介紹
- 項目介紹
- 使用說明
- 擷取代碼
- 需要知識點
- 啟動項目
- 項目示範
- 核心講解
- 功能分析
- 分塊上傳
- 秒傳功能
- 斷點續傳
- 總結
背景介紹
這個項目是在朋友的一次面試中,面試人提出了一個問題.
我有一個100M的檔案,然後我的寬帶隻有10M,我應該如何處理使用者上傳的檔案?
根據這個問題,我小試牛刀,寫了這個項目.
期間查閱了資料,借鑒了Fourwen的項目的前端架構和md寫法.
再次感謝.
項目介紹
項目采用如下:
- 上層: Java, JDK8, Tomcat8,
- 服務端: Jsp, 原生
- 前端: webuploader, bootstrap, jquery
來進行開發,
針對檔案的上傳,一般可以考慮的功能點有
斷點續傳 在斷網或者在暫停的情況下,能夠在上傳斷點中繼續上傳。
分塊上傳 也是斷點續傳的基礎之一,把大檔案通過前端分塊,然後背景在組在一起。
檔案秒傳 服務中已經有人上傳過檔案,其他人再上傳這個檔案直接記錄并放回成功。
其他功能 下面這些功能歸類到其他,是因為它們基本都是通過WebUploader來實作的,很簡單。
- 多線程上傳 多個線程上傳不同的塊檔案。
- 檔案進度顯示 顯示檔案的上傳完成情況。
使用說明
擷取代碼
- GitHub:https://github.com/ck-wizard/BigFileUpload
不會經常更新,下一步會做一個集合公司内部網址的項目.
需要知識點
- 項目使用nio來進行檔案的讀取和建立
- 使用原生web來開發,不使用任何架構
- 使用Apache提供的fileupload來實作上傳資料的擷取
- 使用Apache提供的codec來實作md5加密
- 并發的了解
啟動項目
…
項目示範
…
功能分析
分塊上傳可以說是我們整個項目的基礎,像斷點續傳、暫停這些都是需要用到分塊。
分塊這塊相對來說比較簡單。前端是采用了webuploader,分塊等基礎功能已經封裝起來,使用友善。
借助webUpload提供給我們的檔案API,前端就顯得異常簡單。
var uploader = WebUploader.create({
// swf檔案路徑
swf: '${ctx}/webuploader-0.1.5/Uploader.swf',
// 檔案接收服務端。
server: '${ctx}/upload.do',
//檔案上傳請求的參數表,每次發送都會發送此對象中的參數
formData: {
md5: ''
},
// 選擇檔案的按鈕。可選。
// 内部根據目前運作是建立,可能是input元素,也可能是flash.
pick: '#picker',
// 不壓縮image, 預設如果是jpeg,檔案上傳前會壓縮一把再上傳!
resize: false,
chunked: true, // 分塊
chunkSize: * * , // 位元組 1M分塊
threads: , //開啟線程
auto: false,
// 禁掉全局的拖拽功能。這樣不會出現圖檔拖進頁面的時候,把圖檔打開。
disableGlobalDnd: true,
fileNumLimit: ,
fileSizeLimit: * * , // 200 M
fileSingleSizeLimit: * * // 100 M
});
上傳的檔案會被發送到upload.do這個Controller,在裡面的邏輯如下:
- 判斷是檔案上傳請求,如果是繼續,否則退出
- 使用fileupload jar包解析request請求上傳的基礎資訊
- 使用FileUploadBean包裝上傳的基礎資訊.
- 拼裝父目錄,校驗是否存在
4.1 不存在就建立
4.2 存在就進入檢驗
4.2.1 檢查md5值是否比對, 應該建立資料庫,存儲檔案資訊才是更快 更好的解決辦法.
4.2.2 若比對直接傳回成功.
4.2.3 若不成功,删除源檔案再次讀取
- 寫入該分片資料到指定目錄
寫入規則如下:
// 0.讀取上傳檔案到數組
// 1.寫到本地
// 1.記錄分片數,檢查分片數
// 2.當對應的md5讀取數量達到對應的檔案後,合并檔案
// 3.删除臨時檔案
- 完成
功能分析
分塊上傳
分塊上傳可以說是我們整個項目的基礎,像斷點續傳、暫停這些都是需要用到分塊。
分塊這塊相對來說比較簡單。前端是采用了webuploader,分塊等基礎功能已經封裝起來,使用友善。
借助webUpload提供給我們的檔案API,前端就顯得異常簡單。
var uploader = WebUploader.create({
// swf檔案路徑
swf: '${ctx}/webuploader-0.1.5/Uploader.swf',
// 檔案接收服務端。
server: '${ctx}/upload.do',
//檔案上傳請求的參數表,每次發送都會發送此對象中的參數
formData: {
md5: ''
},
// 選擇檔案的按鈕。可選。
// 内部根據目前運作是建立,可能是input元素,也可能是flash.
pick: '#picker',
// 不壓縮image, 預設如果是jpeg,檔案上傳前會壓縮一把再上傳!
resize: false,
chunked: true, // 分塊
chunkSize: * * , // 位元組 1M分塊
threads: , //開啟線程
auto: false,
// 禁掉全局的拖拽功能。這樣不會出現圖檔拖進頁面的時候,把圖檔打開。
disableGlobalDnd: true,
fileNumLimit: ,
fileSizeLimit: * * , // 200 M
fileSingleSizeLimit: * * // 100 M
});
伺服器先建立一個md5檔案夾,然後按照上傳的檔案名進行一套規範命名,寫入到一個臨時檔案中.
然後記錄這個臨時檔案.
// 規範命名
String fileName = param.getName();
String uploadDirPath = finalDirPath + param.getMd5();
String tempFileName = fileName + "_" + param.getChunk() + "_tmp";
Path tmpDir = Paths.get(uploadDirPath);
// 寫入臨時檔案
Path path = Paths.get(uploadDirPath, tempFileName);
byte[] fileData = FileUtils.read(param.getFile(), );
Files.write(path, fileData, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
FileUtils.authorizationAll(path);
// 記錄
FileBean fileBean;
if(fileMap.containsKey(param.getMd5())) {
fileBean = fileMap.get(param.getMd5());
} else {
fileBean = new FileBean(param.getName(), param.getChunks(), param.getMd5());
fileMap.put(param.getMd5(), fileBean);
}
fileBean.setChunk(param.getChunk());
然後當檔案分片都上傳完成後,在把分片合并為一個檔案,并且删除所有臨時檔案.
if(fileBean.isLoadComplate()) {
// 合并檔案..
Path realFile = Paths.get(uploadDirPath, fileBean.getName());
realFile = Files.createFile(realFile);
// 設定權限
FileUtils.authorizationAll(realFile);
for(int i = ; i < fileBean.getChunks(); i++) {
// 擷取每個分片
tempFileName = fileName + "_" + i + "_tmp";
Path itemPath = Paths.get(uploadDirPath, tempFileName);
byte[] bytes = Files.readAllBytes(itemPath);
Files.write(realFile, bytes, StandardOpenOption.APPEND);
//寫完後删除掉臨時檔案.
Files.delete(itemPath);
}
logger.info("合并檔案{}成功", fileName);
}
秒傳功能
上傳檔案是若發現父目錄已經建立,并且目錄下有上傳的檔案名,那麼進行md5比較,若相同,直接傳回,若不相同,删除目錄檔案,重新上傳.
if (!Files.exists(tmpDir)) {
Files.createDirectory(tmpDir);
} else {
// 檔案夾已存在
// 1.檢查是否有檔案,有進入2, 沒有進3
Path localPath = Paths.get(uploadDirPath, fileName);
// 2.檢查md5值是否比對, 應該建立資料庫,存儲檔案資訊才是更快 更好的解決辦法.
// 2.1.若比對直接傳回成功.
// 2.2 若不成功,删除源檔案再次讀取
if(Files.exists(localPath)) {
String nowMd5 = DigestUtils.md5Hex(Files.newInputStream(localPath, StandardOpenOption.READ));
if(StringUtils.equals(param.getMd5(), nowMd5)) {
// 比較相等,那麼直接傳回成功.
logger.info("已檢測到重複檔案{},并且比較md5相等,已直接傳回", fileName);
return;
} else {
// 删除
logger.info("已經存在的檔案的md5不比對上傳上來的檔案的md5,删除後重新下載下傳");
Files.delete(localPath);
}
}
// 3. 直接寫入到具體目錄下.
}
斷點續傳
斷點續傳,就是在檔案上傳的過程中發生了中斷,人為因素(暫停)或者不可抗力(斷網或者網絡差)導緻了檔案上傳到一半失敗了。然後在環境恢複的時候,重新上傳該檔案,而不至于是從新開始上傳的。
檔案上傳時,擷取分片大小,同伺服器目錄存儲的分片大小進行比較,若一直,直接傳回成功.
//寫入該分片資料
Path path = Paths.get(uploadDirPath, tempFileName);
//檔案上傳時,擷取是否有分片,如果有直接傳回.
if(!Files.exists(path)) {
// 不存在
byte[] fileData = FileUtils.read(param.getFile(), );
try {
Files.write(path, fileData, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
} catch (IOException e) {
// 删除上傳的檔案
Files.delete(path);
throw e;
}
FileUtils.authorizationAll(path);
} else {
return;
}
總結
選擇使用原生是為了鍛煉自己不要忘記基礎,前前後後寫了3天,複習了不少檔案相關的操作,并且對lambda表達式和流
有了進一步了解,還是很滿足的.
在并發的情況下進行檔案上傳,在使用一個執行個體的成員變量進行存儲的時候,在方法上面使用synchronized或代碼段加synchronized
或Lock或使用AtomInteger去進行并發操作,都沒能達到正确統計的目的.最後使用ConcurrentHashMap才完成了正确的計數.
由此看來,多線程環境下,我還是個小菜鳥啊. 努力加油了.