在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檔案管理系統的核心。不過嚴格說來,這不僅僅是一個上傳組件,而是一套檔案管理的解決方案,它包含:
- 一個相容IIS 7整合式管線模式的ASP.NET Http Module,支援大檔案上傳使用(有趣的是,NeatUpload申明,IIS 7的一個Bug使它無法在IIS 7整合式管線模式中使用)。
- 一個支援斷點續傳的ASP.NET Http Handler。
- 一系列ASP.NET伺服器端控制項,提供了檔案上傳功能所需的UI,包括一個多檔案上傳控制項,一個ListView控制項和一個進度條控制項。
- 一個Web應用程式,可以替換FTP的分頁檔方式,支援Email發送連結。它也是上面所提到的組件的使用樣本。
- 一個Windows Service,用於定期清理舊檔案。
- 一個測試專案、一個部署項目、以及一個安裝項目。
- 文檔。
回到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,如果使用整合式管線模式,應該也可以使用Managed 程式碼進行擴充。
- 使用額外的ASP.NET應用程式處理檔案上傳,以節省上傳檔案的線程對原ASP.NET應用程式線程的消耗。