最近公司一同事咨詢了一個MVC項目下上傳大文件時遇到的問題,問題描述如下:
MVC項目中,當上傳比較大的文件時,速度非常慢,小文件基本沒有影響。
原因分析:
如果是用傳統的form表單去提交的話,會將整個文件一次性的加載到內存中然后再做保存,這個過程是相當慢的,特別是大文件,且上傳文件容易受到各種因素的影響而導致上傳失敗,比如臨時的網絡故障等。
如何解決?
最直接的概念就是異步以及斷點續傳。
為什么要異步
- 如果一個表單提交的元素中有文件上傳的需求,如最終因為文件上傳失敗而影響整個表單數據的提交,這個體驗性是非常差的。
- 如果上傳文件時間特別長,容易使應用程序長時間失去響應,給用戶一個錯覺,最好的方法是先讓用戶選擇文件,此時點擊上傳,后台進行文件的異步上傳,此時用戶還可以繼續去填寫其它的表單元素,等用戶填寫完其它表單元素,文件有可能已經上傳完成了,再提交表單,就只處理數據而不再上傳文件了。增強了用戶體驗性。
為什么要斷點續傳
在處理大文件時,無法忍受因為一時的網絡原因導致上傳失敗,從而重新再上傳的煩惱。好的方法是將一個大文件分成N個小塊來進行上傳,即使第一次失敗了,之前上傳的那部分由於得到了保留,再次點擊上傳時,以前已經傳輸成功的部分就不會再次被重新寫入文件。注意,第二次上傳時,文件還是從0開始傳輸到服務器,而不能根據服務器上的文件選擇性的傳輸片斷,這部分不太好節省,有興趣的可以研究下。
如何實現異步上傳
這里可以利用jQuery的相應插件來完成,它的主要功能是將文件分割成N多個小塊來批量上傳,參考網址:https://github.com/blueimp/jQuery-File-Upload
如何實現斷點續傳
其實這個也非常簡單,在Http頭信息中有一個Conten-Range的屬性,它會說明此次傳遞的文件內容的片斷范圍,我們只需要在后台解析這個范圍稍加處理就可以實現。之所這么簡單,是因為有了上面的jQuery 上傳文件的插件,它負責將一個大文件分成N多小塊進行傳輸,這就有了請求頭中的Content-Range。
我主要參考了http://weblogs.asp.net/bryansampica/archive/2013/01/15/AsyncMVCFileUpload.aspx ,但它沒有完成對文件的保存功能,我這里加了斷點續傳的邏輯。
組件對於瀏覽器的要求
看到大家對於js分割文件上傳產生了不解,其實這個jQuery組件對瀏覽器是有要求的,利用了一些全新的api,上面文章中有提到瀏覽器的要求,對於只能使用低版本瀏覽器的項目來講可能目前還有些問題,但異步上傳是沒問題的。
Here it is running in Chrome: (Note: In Chrome, Firefox, and Safari you'll get a multiple file selection, in IE 9 and under, you wont. IE 10 you will)
效果圖:
可支持同時上傳多個文件
上傳成功后的效果
實現關鍵步驟:
- 引入jQuery上傳文件組件
- 初始化上傳組件
主要是控制進展條的顯示以及組件的部分參數,比如文件塊大小等等。
$(function () { $('#fileupload').fileupload({ dataType: "json", url: "/api/upload", limitConcurrentUploads: 1, sequentialUploads: true, progressInterval: 100, maxChunkSize: 10000, add: function (e, data) { $('#filelistholder').removeClass('hide'); data.context = $('<div />').text(data.files[0].name).appendTo('#filelistholder'); $('</div><div class="progress"><div class="bar" style="width:0%"></div></div>').appendTo(data.context); $('#btnUploadAll').click(function () { data.submit(); }); }, done: function (e, data) { data.context.text(data.files[0].name + '... Completed'); $('</div><div class="progress"><div class="bar" style="width:100%"></div></div>').appendTo(data.context); }, progressall: function (e, data) { var progress = parseInt(data.loaded / data.total * 100, 10); $('#overallbar').css('width', progress + '%'); }, progress: function (e, data) { var progress = parseInt(data.loaded / data.total * 100, 10); data.context.find('.bar').css('width', progress + '%'); } }); });
3. 完成斷點續傳后台邏輯
- 首先通過api來配合jQuery上傳組件對於上傳進展條的一個監控 。
[HttpGet] [HttpPost] public HttpResponseMessage Upload() { // Get a reference to the file that our jQuery sent. Even with multiple files, they will all be their own request and be the 0 index HttpPostedFile file = HttpContext.Current.Request.Files[0]; // do something with the file in this space // {....} // end of file doing this.SaveAs(HttpContext.Current.Server.MapPath("~/Images/") + file.FileName, file); // Now we need to wire up a response so that the calling script understands what happened HttpContext.Current.Response.ContentType = "text/plain"; var serializer = new System.Web.Script.Serialization.JavaScriptSerializer(); var result = new { name = file.FileName }; HttpContext.Current.Response.Write(serializer.Serialize(result)); HttpContext.Current.Response.StatusCode = 200; // For compatibility with IE's "done" event we need to return a result as well as setting the context.response return new HttpResponseMessage(HttpStatusCode.OK); }
- 實現斷點續傳邏輯
這其中主要是通過解析Http請求頭中的Content-Range屬性來獲知此次處理的文件片斷,后續就是基本的文件操作了,沒什么可說的。
private void SaveAs(string saveFilePath, HttpPostedFile file) { long lStartPos = 0; int startPosition = 0; int endPosition = 0; var contentRange=HttpContext.Current.Request.Headers["Content-Range"]; //bytes 10000-19999/1157632 if (!string.IsNullOrEmpty(contentRange)) { contentRange = contentRange.Replace("bytes", "").Trim(); contentRange = contentRange.Substring(0, contentRange.IndexOf("/")); string[] ranges = contentRange.Split('-'); startPosition = int.Parse(ranges[0]); endPosition = int.Parse(ranges[1]); } System.IO.FileStream fs; if (System.IO.File.Exists(saveFilePath)) { fs = System.IO.File.OpenWrite(saveFilePath); lStartPos = fs.Length; } else { fs = new System.IO.FileStream(saveFilePath, System.IO.FileMode.Create); lStartPos = 0; } if (lStartPos > endPosition) { fs.Close(); return; } else if (lStartPos < startPosition) { lStartPos = startPosition; } else if (lStartPos > startPosition && lStartPos < endPosition) { lStartPos = startPosition; } fs.Seek(lStartPos, System.IO.SeekOrigin.Current); byte[] nbytes = new byte[512]; int nReadSize = 0; nReadSize = file.InputStream.Read(nbytes, 0, 512); while (nReadSize > 0) { fs.Write(nbytes, 0, nReadSize); nReadSize = file.InputStream.Read(nbytes, 0, 512); } fs.Close(); }
總結:
jQuery上傳組件將大文件分割成小文件上傳,正好解決了.net上傳文件大小問題,只要將塊大小配置好即可。利用Http頭信息的Content-Range來實現斷點續傳,即解決了性能問題也解決了用戶體驗。
注:
這只是我個人測試的代碼,如覺的不妥,可自行修改。