文件上傳在web應用中是比較常見的功能,前段時間做了一個多文件、大文件、多線程文件上傳的功能,使用效果還不錯,總結分享下。
一、 功能性需求與非功能性需求
要求操作便利,一次選擇多個文件進行上傳;
支持大文件上傳(1G),同時需要保證上傳期間用戶電腦不出現卡死等體驗;
交互友好,能夠及時反饋上傳的進度;
服務端的安全性,不因上傳文件功能導致JVM內存溢出影響其他功能使用;
最大限度利用網絡上行帶寬,提高上傳速度;
二、 設計分析
對於大文件的處理,無論是用戶端還是服務端,如果一次性進行讀取發送、接收都是不可取,很容易導致內存問題。所以對於大文件上傳,采用切塊分段上傳
從上傳的效率來看,利用多線程並發上傳能夠達到最大效率。
對於大文件切塊、多線程上傳,需要考慮服務端合並文件的時間點;
三、解決方案:
在HTML5之前的標准是無法支持上面的功能,因此我們需要把功能實現居於H5提供的新特性上面:
1. H5新標准對file標簽進行了增強,支持同時選擇多個文件
<input type="file" multiple=true onchange="doSomething(this.files)"/>
1
注意multiple屬性,設置為true;
onchange:一般是選擇文件確定后的響應事件
this.files:文件對象集合
2. File對象
H5提供的類似java的RandomAccessFile的文件操作對象,其中silce方法允許程序指定文件的起止字節進行讀取。利用這個對象,實現對大文件的切分;
3.
這個對象大家應該很熟悉了,屬於web2.0的標准,我們最常用的ajax請求底層就是居於此對象。本質上是一個線程對象,因此我們通過創建一定數量的對象,實現多線程並行操作;
4. FormData對象
H5新增對象,可以理解為一個key-value的map,通過把文件的二進制流和業務參數封裝到此對象,再交由對象發送到服務端,服務端可以通過普通的request.getParamter方法獲取這些參數;
5. progress標簽
H5新增的標簽,在頁面顯示一個進度條:
value:當前進度條的值
max:最大值
利用這個標簽,結合的回調來反饋目前上傳的進度
四、客戶端代碼示例
HTML代碼:
<input type="file" multiple=true onchange="showFileList(this.files)"/>
<input id="uploadBtn" type="button" value="上傳" onclick="doUpload()"/>
1
2
java腳本:
var quence = new Array();//待上傳的文件隊列,包含切塊的文件
/**
* 用戶選擇文件之后的響應函數,將文件信息展示在頁面,同時對大文件的切塊大小、塊的起止進行計算、入列等
*/
function showFileList(files) {
if(!files) {
return;
}
var chunkSize = 5 * 1024 * 1024; //切塊的閥值:5M
$(files).each(function(idx,e){
//展示文件列表,略......
if(e.size > chunkSize) {//文件大於閥值,進行切塊
//切塊發送
var chunks = Math.max(Math.floor(fileSize / chunkSize), 1)+1;//分割塊數
for(var i=0 ; i<chunks; i++) {
var startIdx = i*chunkSize;//塊的起始位置
var endIdx = startIdx+chunkSize;//塊的結束位置
if(endIdx > fileSize) {
endIdx = fileSize;
}
var lastChunk = false;
if(i == (chunks-1)) {
lastChunk = true;
}
//封裝成一個task,入列
var task = {
file:e,
uuid:uuid,//避免文件的重名導致服務端無法定位文件,需要給每個文件生產一個UUID
chunked:true,
startIdx:startIdx,
endIdx:endIdx,
currChunk:i,
totalChunk:chunks
}
quence.push(task);
}
} else {//文件小於閥值
var task = {
file:e,
uuid:uuid,
chunked:false
}
quence.push(task);
}
});
}
/**
* 上傳器,綁定一個對象,處理分配給其的上傳任務
**/
function Uploader(name) {
this.url=""; //服務端處理url
this.req = new ();
this.tasks; //任務隊列
this.taskIdx = 0; //當前處理的tasks的下標
this.name=name;
this.status=0; //狀態,0:初始;1:所有任務成功;2:異常
//上傳 動作
this.upload = function(uploader) {
this.req.responseType = "json";
//注冊load事件(即一次異步請求收到服務端的響應)
this.req.addEventListener("load", function(){
//更新對應的進度條
progressUpdate(this.response.uuid, this.response.fileSize);
//從任務隊列中取一個再次發送
var task = uploader.tasks[uploader.taskIdx];
if(task) {
console.log(uploader.name + ":當前執行的任務編號:" +uploader.taskIdx);
this.open("POST", uploader.url);
this.send(uploader.buildFormData(task));
uploader.taskIdx++;
} else {
console.log("處理完畢");
uploader.status=1;
}
});
//處理第一個
var task = this.tasks[this.taskIdx];
if(task) {
console.log(uploader.name + ":當前執行的任務編號:" +this.taskIdx);
this.req.open("POST", this.url);
this.req.send(this.buildFormData(task));
this.taskIdx++;
} else {
uploader.status=1;
}
}
//提交任務
this.submit = function(tasks) {
this.tasks = tasks;
}
//構造表單數據
this.buildFormData = function(task) {
var file = task.file;
var formData = new FormData();
formData.append("fileName", file.name);
formData.append("fileSize", file.size);
formData.append("uuid", task.uuid);
var chunked = task.chunked;
if(chunked) {//分塊
formData.append("chunked", task.chunked);
formData.append("data", file.slice(task.startIdx, task.endIdx));//截取文件塊
formData.append("currChunk", task.currChunk);
formData.append("totalChunk", task.totalChunk);
} else {
formData.append("data", file);
}
return formData;
}
}
/**
*用戶點擊“上傳”按鈕
*/
function doUpload() {
//創建4個Uploader上傳器(4條線程)
var uploader0 = new Uploader("uploader0");
var task0 = new Array();
var uploader1 = new Uploader("uploader1");
var task1 = new Array();
var uploader2 = new Uploader("uploader2");
var task2 = new Array();
var uploader3 = new Uploader("uploader3");
var task3 = new Array();
//將文件列表取模hash,分配給4個上傳器
for(var i=0 ; i<quence.length; i++) {
if(i%4==0) {
task0.push(quence[i]);
} else if(i%4==1) {
task1.push(quence[i]);
} else if(i%4==2) {
task2.push(quence[i]);
} else if(i%4==3) {
task3.push(quence[i]);
}
}
/提交任務,啟動線程上傳
uploader0.submit(task0);
uploader0.upload(uploader0);
uploader1.submit(task1);
uploader1.upload(uploader1);
uploader2.submit(task2);
uploader2.upload(uploader2);
uploader3.submit(task3);
uploader3.upload(uploader3);
//注冊一個定時任務,每2秒監控文件是否都上傳完畢
uploadCompleteMonitor = setInterval("uploadComplete()",2000);
}
五、服務端處理:
服務端處理邏輯相對比較傳統,利用輸入輸出流、NIO等把文件寫到磁盤即可。
這里需要特別考慮的是關於被切塊文件的合並。前端在上傳的時候,文件塊是無序到達服務端,因此我們在每次接收到一個文件塊的時候需要判斷被切塊的文件是否都傳輸完畢並進行合並,思路如下:
回到前端,我們在構造被切塊的文件formData的數據結構:
formData.append("fileName", file.name);
formData.append("fileSize", file.size);
formData.append("uuid", task.uuid);
formData.append("chunked", task.chunked);
formData.append("data", file.slice(task.startIdx, task.endIdx));//截取文件塊
formData.append("currChunk", task.currChunk);
formData.append("totalChunk", task.totalChunk);
fileName:文件的原始名字
fileSize:文件的大小,KB
uuid:文件的uuid
chunked:true,標識是分段上傳的文件塊
data:文件二進制流
currChunk:當前上傳的塊編號
totalChunk:總塊數
服務端以文件的UUID為key,維護一個chunk計數器,每接收到一塊就找到對應的uuid執行計數器+1,同時考慮到並發情況,需采用同步關鍵字,避免出現邏輯錯誤。當計數器等於totalChunk的時候,進行文件合並
前端效果:

文件上傳存儲目錄:D:\wamp64\www\up6\db\upload\2019\04\19\920144c756af424ca59136be71cf9209

文件上傳完成后,被完整的存放在了目錄中。
DEMO下載地址:https://dwz.cn/fgXtRtnu