在網頁中直接上傳大文件一直是個比較頭疼的問題,主要面臨的問題一般包括兩類:一是上傳時間長中途一旦出錯會導致前功盡棄;二是服務端配置復雜,要考慮接收超大表單和超時問題,如果是托管主機沒準還改不了配置,默認只能接收小于4mb的附件。
比較理想的方案是能夠把大文件分片,一片一片的傳到服務端,再由服務端合并。這么做的好處在于一旦上傳失敗只是損失一個分片而已,不用整個文件重傳,而且每個分片的大小可以控制在4mb以內,服務端不用做任何設置就可適應。
常用的解決方案是ria,以flex為例,通常是利用filereference.load方法加載文件得到bytearray,然后分片構造表單(flash的高版本不允許直接訪問文件)。不過這個load方法只能加載較小的文件,大約不超過300mb,因此適用性不是很強。
好在現在有了html5,我們可以直接構造分片了,這是一個非常喜人的進步,只可惜目前適用面不廣(ie啊ie,真是恨你恨得牙癢癢)。
言歸正傳,來看一個demo吧,基于asp.net mvc3,只是示例,很多問題做了簡化處理。
主要是客戶端,新特性都體現在這里:
<%@ page language=c# inherits=system.web.mvc.viewpage<dynamic> %>
<!doctype html>
<html lang=zh-cn>
<head>
<meta charset=utf-8>
<title>html5大文件分片上傳示例</title>
<script src=../scripts/jquery-1.11.1.min.js></script>
<script>
var page = {
init: function(){
$(#upload).click($.proxy(this.upload, this));
},
upload: function(){
var file = $(#file)[0].files[0], //文件對象
name = file.name, //文件名
size = file.size, //總大小
succeed = 0;
var shardsize = 2 * 1024 * 1024, //以2mb為一個分片
shardcount = math.ceil(size / shardsize); //總片數
for(var i = 0;i < shardcount;++i){
//計算每一片的起始與結束位置
var start = i * shardsize,
end = math.min(size, start + shardsize);
//構造一個表單,formdata是html5新增的
var form = new formdata();
form.append(data, file.slice(start,end)); //slice方法用于切出文件的一部分
form.append(name, name);
form.append(total, shardcount); //總片數
form.append(index, i + 1); //當前是第幾片
//ajax提交
$.ajax({
url: ../file/upload,
type: post,
data: form,
async: true, //異步
processdata: false, //很重要,告訴jquery不要對form進行處理
contenttype: false, //很重要,指定為false才能形成正確的content-type
success: function(){
++succeed;
$(#output).text(succeed + / + shardcount);
}
});
}
}
};
$(function(){
page.init();
});
</script>
</head>
<body>
<input type=file id=file />
<button id=upload>上傳</button>
<span id=output style=font-size:12px>等待</span>
</body>
</html>
這里的slice方法和formdata都是html5之前不存在的。通過這樣的方法,我們的表單構造出來是這樣的,抓包看看:
利用html5分片上傳超大文件
可以看到構造出來的content-type是multipart/form-data,也就是符合rfc標準的那個最傳統(tǒng)的文件上傳表單。另外我們同時傳輸的name、total等屬性也都在表單里。
然后是服務端,沒什么新鮮的,完全是在接收一個普通的文件:
[httppost]
public actionresult upload()
{
//從request中取參數,注意上傳的文件在requst.files中
string name = request[name];
int total = convert.toint32(request[total]);
int index = convert.toint32(request[index]);
var data = request.files[data];
//保存一個分片到磁盤上
string dir = server.mappath(~/upload);
string file = path.combine(dir, name + _ + index);
data.saveas(file);
//如果已經是最后一個分片,組合
//當然你也可以用其它方法比如接收每個分片時直接寫到最終文件的相應位置上,但要控制好并發(fā)防止文件鎖沖突
if(index == total)
{
file = path.combine(dir, name);
var fs = new filestream(file, filemode.create);
for(int i = 1;i <= total;++i)
{
string part = path.combine(dir, name + _ + i);
var bytes = system.io.file.readallbytes(part);
fs.write(bytes, 0, bytes.length);
bytes = null;
system.io.file.delete(part);
}
fs.close();
}
//返回是否成功,此處做了簡化處理
return json(new { error = 0 });
}
上面的demo很多問題是簡化處理的,比如沒做什么異常處理,客戶端也沒有判斷服務端是否出錯重試一類的,各位可以自己完善。
在上面的基礎上,我們可以做很多功能上的擴展,比如我們可以控制所有分片是順序上傳還是并發(fā)上傳,以適用不同應用。再比如我們可以在整體文件上傳前以及分片上傳前都先計算一下相應的hash,發(fā)個請求詢問服務器文件是否已存在,如果存在就不要重復上傳了,這樣就實現了“極速上傳”以及“斷點續(xù)傳”。