【趙勳】在ASP.NET應用程式中上傳檔案

iDotNetSpace發表於2008-06-11
在Web程式中上傳檔案是很常見的需求。利用HTTP協議上傳檔案的方式非常有限,最常見的莫過於使用元素進行上傳。這種上傳方式會將內容使用multipart/form-data方案進行編碼,並將內容POST到伺服器端。使用multipart/form-data編碼方式與預設的application/x-url-encoded編碼方式相比,在大資料量情況下效率要高很多。

  使用上傳檔案最大的優勢在於程式設計方便,幾乎各種伺服器端技術都對這種上傳方式做了良好的封裝,使得程式設計師能夠直觀地對客戶端上傳的檔案進行處理。不過總體來說,這個協議並不適合做檔案傳輸,解析資料流內容的代價相對較高,並且沒有一些例如斷點續傳的機制來輔助,導致在上傳大檔案時經常會力不從心。

  有朋友認為使用上傳檔案最大的問題在於記憶體佔用太高,由於需要將整個檔案載入記憶體進行處理,導致如果使用者上傳檔案太大,或者同時上傳的使用者太多,會造成伺服器端記憶體耗盡。這個觀點其實是錯誤的。對於某些伺服器端的技術,例如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)應用中上傳檔案,我們一般就會直接使用控制元件進行檔案上傳。如果一個頁面中存在控制元件,那麼頁面中form元素的enctype就會被自動改為multipart/form-data,而且我們可以在頁面PostBack之後通過控制元件的引用來獲得客戶端通過該控制元件所上傳得檔案。不過,如果上傳檔案的功能需要較為特別的需求——例如需要進度條提示,控制元件就無能為力了。

  確切地說,應該是所能提供的支援非常有限,因此一些特殊需求我們不能實現——嚴格說來,應該是無法輕易地、直接地實現。這樣,在實現這些功能時,我們就會繞一個大大的彎。為了避免每次實現相同功能時都要費神費時地走一遍彎路,因此出現了各種上傳元件。上傳元件提供了封裝好的功能,使得我們在實現檔案上傳功能時變得輕鬆了很多。例如幾乎所有的上傳元件都直接或間接地提供了進度提示的功能,有的提供了當前的百分比數值,有的則直接提供了一套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提供了兩個伺服器控制元件:。前者用於代替,可以通過它訪問到使用者通過特定上傳框上傳的內容;後者則是一個進度條顯示控制元件,負責使用彈出視窗或內聯的形式顯示上傳的進度。彈出視窗自不必說,而所謂的“內聯”方式其實只是在頁面中嵌入一個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應用程式執行緒的消耗。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/12639172/viewspace-343342/,如需轉載,請註明出處,否則將追究法律責任。

相關文章