web專案檔案上傳下載演變總結
背景:檔案上傳下載對於在網際網路開發中是一個比較常見的功能,本文將分別對上傳和下載進行闡述,只關心核心功能,其他邊緣功能不在敘述,每個功能採用演變的思想,給出幾種方案,當然每種方案都能完成需求,希望大家根據專案的需求以及上傳檔案大小挑選出適合自己的方案。
1 檔案上傳
以下所有上傳方案只針對單檔案上傳做優化,通過簡單的封裝和多執行緒的改寫,可以支援多執行緒上傳。另外在上傳過程中邊緣功能,例如檔案大小限制,格式判斷,檔案摘要,進度條等不在此次方案中贅述。
1> 檔案上傳精簡版
本方案是所有開發者首先想到的方案,也是不願意折騰的方案,適合小檔案上傳,具體流程為:
1 前端傳送檔案流 2 後端順序接受 3 操作檔案流儲存為檔案
前端表單
<form action="http://localhost:8081/upload" method="post" enctype="multipart/form-data" accept-charset="utf-8"> <input type="file" name="file" value="選擇檔案"/> <input id="submit_form" type="submit" value="提交"/> </form>
備註:accept-chaset主要是為了解決中文檔名稱亂碼問題。
後端程式碼如下:
@Override public ServiceResult upload(HttpServletRequest req) { // TODO Auto-generated method stub // TODO Auto-generated method stub ServiceResult result = null; try { /* * CommonsMultipartResolver resolver = new * CommonsMultipartResolver(req.getSession().getServletContext()); * resolver.setDefaultEncoding("utf-8"); */ MultipartHttpServletRequest params = ((MultipartHttpServletRequest) req); List<MultipartFile> files = params.getFiles("file"); // 多檔案的上傳 if (files != null && files.size() > 0) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); long time = System.currentTimeMillis(); String prefix = "logs" + File.separator; String fullPath = uploadPath + prefix; File file = new File(fullPath); if (!file.exists()) { file.mkdirs(); } // 某一個上傳錯誤,全部中斷 List<String> fileNames = new ArrayList<String>(); for (MultipartFile multipartFile : files) { if (multipartFile != null) { String fileName = multipartFile.getOriginalFilename(); multipartFile.transferTo(new File(fullPath + fileName)); fileNames.add(fileName); } } result = new ServiceResult<>(); result.setData(fileNames); } else { result = new ServiceResult(HttpResultEnum.CLIENT_UNABLE_OBTAIN); } } catch (Exception e) { e.printStackTrace(); result = new ServiceResult(HttpResultEnum.FAIL); } return result; }
備註:使用springmvc 上傳,支援多檔案上傳。
2> 分塊上傳
當檔案比較大,需要嚴格控制上傳的時間時,那麼我們就需要充分利用網路資源,壓榨伺服器效能。回想上一個方案中,上傳檔案作為一個整體被髮送到服務端,服務端也按照順序一個位元組一個位元組的讀取,按照目前的伺服器配置大多都是雙cpu,多核心,那麼我們可以同時讓其處理一個檔案的多個部分,從而縮短上傳時間。
分塊上傳需要前後端配合完成,對於分塊上傳前端的程式設計思路為:
1 確定分塊的大小 2 非同步的傳送資料塊(根據情況,不要併發太多,3-5個足以)
前端js分段程式碼
<script>
function Upload() {
var files = document.getElementById('myFile').files;
var file = files[0];
var totalSize = file.size;//檔案大小
var blockSize = 1024 * 1024 * 2;//塊大小
var count = Math.ceil(totalSize / blockSize);//總塊數
for(var i =0;i<count;i++){
//建立FormData物件
var formData = new FormData();
formData.append('fileName', file.name);//檔名
formData.append('total', blockCount);//總塊數
formData.append('index', index);//當前上傳的塊下標
var start = index * blockSize;
var end = Math.min(totalSize, start + blockSize);
var block = file.slice(start, end);
formData.set('data', block);
$.ajax({
url: 'localhost/upload',
type: 'post',
data: formData,
processData: false,
contentType: false,
success: function (res) {
}
});
}
}
</script>
備註:前端計算好每塊的md5值,用md5+index作為檔名,為斷點續傳做準備。
後端處理的步驟為:
1 處理每塊上傳的資料 2 把每塊上傳的資訊儲存到資料庫 3 每次上傳完成後,檢視當前所有分塊是否上傳完成,當上傳完成後進行合併。
程式碼如下:
MultipartHttpServletRequest params = ((MultipartHttpServletRequest) req);
//獲取所有前端資訊
List<MultipartFile> files = params.getFiles("file"); // 多檔案的上傳
String oleFileName = params.getParameter("oleFileName"); //檔名稱md5
String fileName = params.getParameter("fileName"); //檔名稱md5
String total = params.getParameter("total"); //總共有幾塊
String index = params.getParameter("index"); //當前是第幾塊
//儲存塊檔案
multipartFile.transferTo(new File(fullPath + fileName));
//把塊相關資訊插入資料庫
saveInfo(FileChunkForm);
//判斷是否已經上傳完成,當所有的都上傳完成進行合併
if(checkFinish(oleFileName)==Integer.parseInt(total)){
mergeFile(oleFileName);
}
3> 斷點續傳
此方案是一個優化的策略,可以和前兩個方案結合,按照字面意思理解斷點續傳,最重要的是要斷點在哪,所以就需要服務端做記錄,儲存上次客戶端上傳之前先獲取該檔案在服務端的狀態。 web前端可以選擇外掛類似WebUploader等完成斷點續傳,也可以採用分塊的思想自行編寫程式碼。
private void upload(long[] startPos, long[] endPos, File tmpfile) {
RandomAccessFile rantmpfile = null;
try {
if (tmpfile.exists()) {
rantmpfile = new RandomAccessFile(tmpfile, "rw");
for (int i = 0; i < threadNum; i++) {
rantmpfile.seek(8 * i + 8);
startPos[i] = rantmpfile.readLong();
rantmpfile.seek(8 * (i + 1000) + 16);
endPos[i] = rantmpfile.readLong();
}
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (rantmpfile != null) {
rantmpfile.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
備註:方案1中改造後端主要通過RandomAccessFile 完成斷點續傳,方案2中因為已經拆分為塊,續傳只需要控制到塊級別就行就行(已經完成的塊不在上傳,沒有完成的塊重頭開始上傳)。
2 檔案下載
未完待續