解析ASP.NET應用程序中上傳文件的方案

作者: Jeffrey Zhao  來源: 博客園  發布時間: 2010-08-08 20:47  閱讀: 1759 次  推薦: 1   原文鏈接   [收藏]  

  在Web程序中上傳文件是很常見的需求。利用HTTP協議上傳文件的方式非常有限,最常見的莫過于使用<input type="file" />元素進行上傳。這種上傳方式會將內容使用multipart/form-data方案進行編碼,并將內容POST到服務器端。使用multipart/form-data編碼方式與默認的application/x-url-encoded編碼方式相比,在大數據量情況下效率要高很多。

  使用<input type="file" />上傳文件最大的優勢在于編程方便,幾乎各種服務器端技術都對這種上傳方式做了良好的封裝,使得程序員能夠直觀地對客戶端上傳的文件進行處理。不過總體來說,這個協議并不適合做文件傳輸,解析數據流內容的代價相對較高,并且沒有一些例如斷點續傳的機制來輔助,導致在上傳大文件時經常會力不從心。

  有朋友認為使用<input type="file" />上傳文件最大的問題在于內存占用太高,由于需要將整個文件載入內存進行處理,導致如果用戶上傳文件太大,或者同時上傳的用戶太多,會造成服務器端內存耗盡。這個觀點其實是錯誤的。對于某些服務器端的技術,例如Spring Framework,或者早期ASP.NET 1.1時,為了供程序處理,都會將用戶上傳的內容完全載入內存,這的確會帶來問題。但是其實協議本身并沒有規定服務器端應該使用何種方式來處理上傳的文件。例如在現在的ASP.NET 2.0中就已經會在用戶上傳數據超過一定數量之后將其存在硬盤中的臨時文件中,而這點對于開發人員完全透明,也就是說,開發人員可以像以前一樣進行數據流的處理。

  ASP.NET 2.0啟用硬盤臨時文件的閾值(threshold)是可配置的:

<system.web>
  <httpRuntime
    maxRequestLength="Int32"
    requestLengthDiskThreshold="Int32" />
</system.web>

  maxRequestLength自不必說,剛接觸ASP.NET的朋友總會發現上傳文件不能超過4M,這就是因為maxRequestLength的大小默認為4096,這就限制著每個請求的大小不得超過4096KB。這么做的目的是為了保護應用程序不受惡意請求的危害。當請求超過maxRequestLength之后,ASP.NET處理程序將不會處理該請求。這里和ASP.NET拋出一個異常是不同的,這就是為什么如果用戶上傳文件太大,看到的并非是ASP.NET應用程序中指定的錯誤頁面(或者默認的),因為ASP.NET還沒有對這個請求進行處理。requestLengthDiskThreshold就是剛才所提到的閾值,其默認值為256,即一個請求內容超過256KB時就會啟用硬盤作為緩存。這個閾值理論上和客戶端是否是在上傳內容無關,只要客戶端發來的請求大于這個值即可。因此,在ASP.NET 2.0中服務器的內存不會因為客戶端的異常請求而耗盡。

  如果我們需要在ASP.NET(如果沒有特別說明,以下ASP.NET均指ASP.NET 2.0)應用中上傳文件,我們一般就會直接使用<asp:FileUpload />控件進行文件上傳。如果一個頁面中存在<asp:FileUpload />控件,那么頁面中form元素的enctype就會被自動改為multipart/form-data,而且我們可以在頁面PostBack之后通過<asp:FileUpload />控件的引用來獲得客戶端通過該控件所上傳得文件。不過,如果上傳文件的功能需要較為特別的需求——例如需要進度條提示,<asp:FileUpload />控件就無能為力了。

  確切地說,應該是<input type="file" />所能提供的支持非常有限,因此一些特殊需求我們不能實現——嚴格說來,應該是無法輕易地、直接地實現。這樣,在實現這些功能時,我們就會繞一個大大的彎。為了避免每次實現相同功能時都要費神費時地走一遍彎路,因此出現了各種上傳組件。上傳組件提供了封裝好的功能,使得我們在實現文件上傳功能時變得輕松了很多。例如幾乎所有的上傳組件都直接或間接地提供了進度提示的功能,有的提供了當前的百分比數值,有的則直接提供了一套UI;有的組件只提供了簡單的UI,有的卻提供了一整套上傳、刪除的管理界面。此外,有的組件還提供了防止客戶端惡意上傳的能力。

  關于ASP.NET下的上傳組件,最廣為流傳的方式莫過于在ASP.NET Pipeline的BeginRequest事件中截獲當前的HttpWorkerRequest對象,然后直接調用其ReadEntityBody等方法獲取客戶端傳遞過來的數據流,并加以分析和處理。在ASP.NET 1.1時期,這么做的目的是為了直接將數據寫入硬盤,以避免上傳內容消耗太多服務器內存,但是現在自然已經不會因為這個原因而這么做了。從客戶端發起請求到一定規模的數據傳輸完畢需要一段時間,那么從HttpWorkerRequest對象中讀取數據流自然需要一段時間,而在這段時間內,客戶端可以使用新的請求進行輪詢來獲得當前上傳的狀況。這就是獲得上傳進度的最傳統的做法。這個做法的原理很容易理解,但是寫出一個完整的組件其實很不容易,尤其是各種細節方面的問題會讓人感到防不勝防。此類組件中最成功且最著名的莫過于NeatUpload了。

  NeatUpload是一個開源組件,使用LGPL(Lesser General Public License)許可協議,也就是說它是“business-friendly”的。NeatUpload可以在ASP.NET和mono中使用,能夠將上傳的文件存在硬盤中或者Sql Server數據庫中。NeatUpload提供了兩個服務器控件:<NeatUpload:InputFile>和<NeatUpload:ProgressBar>。前者用于代替<asp:FileUpload />,可以通過它訪問到用戶通過特定上傳框上傳的內容;后者則是一個進度條顯示控件,負責使用彈出窗口或內聯的形式顯示上傳的進度。彈出窗口自不必說,而所謂的“內聯”方式其實只是在頁面中嵌入一個Iframe元素,然后通過不斷刷新iframe中的頁面來進行進度展示而已——可見它和彈出窗口顯示方式的區別僅僅在頁面所處的位置。當然,如果我們希望將其移植為AJAX形式也不難,只需開發一個頁面,繼承NeatUpload提供的ProgressPage類,并通過ProgressPage所提供的一些屬性(總字節數,已上傳字節數,已花時間,etc.)來獲得當前上傳的進度,最后直接使用Response.Write輸出JSON形式的數據即可。事實上原本在iframe(或新窗口)中的頁面,也是繼承了ProgressPage類,并且使用HTML的方式進行呈現而已,本質上并沒有太大區別。

  不過個人認為,其實NeatUpload的實用價值不高(這點稍后再述),它最大的意義還在于提供了一個完整的優秀的示例。NeatUpload設計精巧,注釋完整,是個不可多得學習案例。如果能夠將NeatUpload的代碼研究一遍,那么相信在編程能力和ASP.NET的理解上都會上一個新的臺階。此外,在NeatUpload站點上還能夠發現NeatHtml。NeatHtml是一個開源的Web組件,用于顯示不安全的內容(主要是用戶輸入內容,例如博客評論,論壇帖子等等),主要用于避免跨站腳本(XSS,Cross-Site Scripting)等安全問題。作為組件的作者,Dean還將NeatHtml所用到的技術總結為一篇Whitepaper,感興趣的朋友可以看一下,這是一份不可多得的技術資料。

  順便提一下,個人認為目前很多開發人員的編程能力還不夠,似乎很多人都過早地把精力放在了“設計”,或者某個特定的技術上,而忽略了最基礎的“編程能力”,也就是將一段思路轉化為代碼實現的能力。我發現,很多朋友在解決問題的時候,似乎都能很快得到解決方案并且敘述出來,但是真正要使用代碼來表現出來時卻顯得困難重重。其實在工作中,思路或解決方案可以通過討論而獲得,但是真正轉化為代碼的時候只能靠自己了。而且編程能力其實和所謂的“工作經驗”無關,我建議以“應屆畢業生”“自居”的朋友,可以定心地鍛煉一下自己的編程能力。

  與NeatUpload類似的開源組件還有Memba Velodoc XP Edition,它是Velodoc文件管理系統的核心。不過嚴格說來,這不僅僅是一個上傳組件,而是一套文件管理的解決方案,它包含:

  1. 一個兼容IIS 7集成管道模式的ASP.NET Http Module,支持大文件上傳使用(有趣的是,NeatUpload申明,IIS 7的一個Bug使它無法在IIS 7集成管道模式中使用)。
  2. 一個支持斷點續傳的ASP.NET Http Handler。
  3. 一系列ASP.NET服務器端控件,提供了文件上傳功能所需的UI,包括一個多文件上傳控件,一個ListView控件和一個進度條控件。
  4. 一個Web應用程序,可以替換FTP的交換文件方式,支持Email發送鏈接。它也是上面所提到的組件的使用示例。
  5. 一個Windows Service,用于定期清理舊文件。
  6. 一個測試項目、一個部署項目、以及一個安裝項目。
  7. 文檔。

  回到NeatUpload組件。說實話,我始終不喜歡這種進度獲取方式,因為我覺得通過一個額外的請求對服務器進行輪詢無疑是一個累贅。事實上,如果需要上傳大文件并且獲得上傳進度,目前最好的方式應該是使用RIA方式。最典型的RIA上傳方式就是利用Flash了。ActionScript 2.0中已經存在FileReference和FileReferenceList組件以支持單文件和多文件的上傳,有了這兩個組件,上傳的各種信息已經能夠完全在客戶端獲得,而上傳進度也自然能夠計算出來。FileReference和FileReferenceList組件非常容易使用,就連像我這樣對Flash一竅不通的人,也能在短時間內作出一個簡單的上傳功能。但是自從有了swfupload,世界就變得更美好了。

  嚴格說來,通過FileReference所得到的上傳進度是“客戶端發送數據的進度”,而像NeatUpload的做法得到的是“服務器端接受數據的進度”,兩者不可混為一談。

  swfupload也是個開源組件,顧名思義是使用Flash進行上傳。不過對于swfupload來說,Flash的作用主要是“控制”,而不是“展示”,這無疑給了開發人員更大的靈活性。swfupload的實現方式自然是利用了FileReference和FileReferenceList組件所提供的功能,通過Flash與JavaScript的交互能力,使得開發文件上傳功能變得非常優雅和容易。有了swfupload,開發人員可以使用JavaScript來實現各種顯示方式,開發像Flicker一樣酷酷的上傳界面也不再是非常困難的事情了。

  swfupload是個客戶端組件,它對于服務器端來說完全透明,也就是說,服務器端只需要使用對待普通form的方式來處理即可。例如在ASP.NET中我們可以使用Generic Handler來處理客戶端的文件上傳。如下,fileCollection變量即為客戶端Post至服務器端所有文件的集合,我們可以使用name或下標的方式來獲得其中的HttpPostedFile對象。:

public class UploadHandler : IHttpHandler
{
    public void ProcessRequest(HttpContext context)
    {
        HttpFileCollection fileColllection = context.Request.Files;
        ...
    }
 
    public bool IsReusable { ... }
}

  既然Flash提供了文件上傳功能,Silverlight作為微軟主推的RIA技術也不會缺了這項功能。這篇文章源自Silverlight 2.0的Quick Starts,展示了如何使用Silverlight 2.0開發文件上傳的功能,感興趣的朋友可以一讀。

  圍繞著ASP.NET中上傳文件這個話題也討論了不少了,還有什么沒有涉及到的嗎?個人認為其實至少還有一個非常重要問題是沒有討論過,那就是在處理上傳文件時占用ASP.NET處理線程的問題。眾所周知,ASP.NET處理請求時會用到線程池中的線程,當線程池中的線程被用完之后沒有被處理的請求只能排隊了。因此增大ASP.NET應用程序吞吐量的一個重要手段,就是為一些耗時的操作使用異步處理方式(事實上這一命題可以在大部分應用中成立)。例如一個數據庫查詢操作需要3秒鐘,如果不使用異步操作,處理線程就會被阻塞,直至查詢完成。如果使用異步方式來執行數據庫查詢,在這3秒鐘內線程就可以用戶處理其他請求,當異步操作結束之后,ASP.NET就會使用另一個線程來繼續處理這個請求。

  上傳大文件也是一個長時間占用處理線程的工作,而且遺憾的是,這無法使用異步操作來完成(通過異步操作來釋放處理線程需要操作系統的支持,因此只有少量功能可以使用異步操作)。如果一個文件上傳需要3分鐘時間,那么在這3分鐘內就會獨占一個處理線程,如果上傳文件的連接一多,就會大大影響應用程序的性能——就像遭受了某種方式的DOS攻擊一樣。因此,即使使用了像NeatUpload和swfupload這樣的組件,也無法解決上傳連接過多造成可用線程減少的問題。要解決這個問題并不容易,以下是兩種思路(歡迎大家就此問題進行討論):

  • 擴展IIS,使上傳文件或處理文件的過程不經ASP.NET處理,以減少ASP.NET應用程序線程的消耗。現在有了IIS 7,如果使用集成管道模式,應該也可以使用托管代碼進行擴展。
  • 使用額外的ASP.NET應用程序處理文件上傳,以節省上傳文件的線程對原ASP.NET應用程序線程的消耗。

  就先說到這里吧。

一個
1
0
 
 
 

文章列表

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()