文/陶剛 在Web應用程序中處理大文件下載的問題一直出了名的困難,因此對于大多數(shù)站點來說,如果用戶的下載被中斷了,它們只能說悲哀降臨到用戶的身上了。但是我們現(xiàn)在不必這樣了,因為你可以使自己的ASP.NET應用程序有能力支持可恢復(繼續(xù))的大文件下載。使用本文提供的方法的時候,你可以跟蹤下載的過程,這樣你就可以處理動態(tài)建立的文件--而且要達到這個目標根本不需要舊式的ISAPI動態(tài)鏈接庫和非受控的(unmanaged)C++代碼。 為客戶端提供從互聯(lián)網(wǎng)上下載文件的服務最容易了,對嗎?僅僅只需要把可下載的文件復制到你的Web應用程序目錄中,發(fā)布鏈接并讓IIS完成所有相關(guān)的工作。但是,文件服務不應該比脖子上的疼痛還要多(還要麻煩),你不希望整個世界都能訪問自己的數(shù)據(jù),你不希望服務器被數(shù)百個靜態(tài)文件塞滿了,你甚至于希望下載臨時文件--只有當客戶端開始下載后的空閑時間才建立這些文件。 不幸的是,使用IIS對下載請求的默認的響應是不可能達到這些效果的。因此在一般情況下,為了獲得對下載過程的控制權(quán),開發(fā)者需要鏈接到一個定制的.aspx頁面,在這個頁面中它們檢查用戶憑證(credential)、建立可以下載的文件并使用下面的代碼把該文件推送給客戶端: Response.WriteFile 而這就是出現(xiàn)真正麻煩的地方。 有什么問題? WriteFile方法看起來非常完美,它使文件的二進制數(shù)據(jù)流向客戶端。但是直到最近我們才知道,WriteFile方法是一個出名的內(nèi)存占用狂,它把整個文件載入服務器的RAM中來提供服務(實際上它甚至于會占用文件兩倍大小的空間)。對于大文件,這會引起服務內(nèi)存問題,并且可能重復ASP.NET過程。但是在2004年6月微軟發(fā)布了一個補丁解決了這個問題。這個補丁現(xiàn)在是.NET Framework 1.1補丁包(SP1)的一部分。 這個補丁引入了TransmitFile方法,它把一個磁盤文件讀入到較小的內(nèi)存緩沖區(qū)之后就開始傳輸該文件。盡管這個方案解決了內(nèi)存和循環(huán)的問題,但是它仍然不能令人滿意。你不能控制響應的生命周期。你無法知道下載是否正確地完成了,你沒有辦法知道下載是否被中斷了,并且(如果你建立了臨時文件)你也不知道是否應該、以及什么時候可以刪除這些文件。更糟的是,如果下載的確失敗了,TransmitFile方法又從客戶端下次嘗試的文件頭部開始下載。 其中一種可能的解決方案--實現(xiàn)后臺智能傳輸服務(BITS)對于多數(shù)站點來說是不可行的,因為這會毀掉維持客戶端瀏覽器和操作系統(tǒng)獨立性而作出的努力。 令人滿意的解決方案的基礎(chǔ)還是來自微軟用于解決WriteFile引起的內(nèi)存混亂問題的第一次嘗試(見知識庫文章812406)。那篇文章演示了智能的大塊數(shù)據(jù)下載過程,它從文件流中讀取數(shù)據(jù)。在服務器把字節(jié)塊發(fā)送給客戶端之前,它使用Response.IsClientConnected屬性檢查客戶端是否仍然保持著連接。如果仍然保持連接,它就繼續(xù)發(fā)送流字節(jié),否則就停止,以防止服務器發(fā)送不必要的數(shù)據(jù)。 HTTP協(xié)議和頭信息(Header)支持 HTTP協(xié)議支持可以用于處理被中斷下載的頭信息。使用少量的HTTP頭信息,你可以增強自己的下載過程,使它完全遵循HTTP協(xié)議規(guī)范。這個規(guī)范與ranges一起提供恢復被中斷的下載所需要的一切信息。 下面是它的工作方式。首先,如果服務器支持客戶端斷點續(xù)傳,它就在初始的響應中發(fā)送Accept-Ranges頭信息。服務器還發(fā)送一個實體標簽(entity tag)頭信息(ETag),它包含一個唯一的標識字符串。 下面的代碼顯示了IIS發(fā)送給客戶端的用于響應一個初始下載請求的一些頭信息,它向客戶端傳遞了被請求的文件的詳細信息。 HTTP/1.1 200 OK 在接收這些頭信息之后,如果下載被中斷了,IE瀏覽器在后來的下載請求中會把Etag值和Range頭信息發(fā)送回服務器。下面的代碼顯示了嘗試恢復被中斷下載時IE發(fā)送給服務器的一些頭信息。 GET http://192.168.100.100/download.zip HTTP/1.0 這些頭信息表明IE緩存了IIS提供的實體標簽,并在If-Range頭信息中把它發(fā)送回服務器了,這是確保下載從準確相同的文件恢復的一種途徑。不幸的是,并非所有的瀏覽器的工作方式都相同。客戶端發(fā)送的用于驗證文件的其它HTTP頭信息可能是If-Match、If-Unmodified-Since或者Unless-Modified-Since。很明顯,該規(guī)范對于客戶端軟件必須支持哪些頭信息,或者必須使用哪些頭信息沒有明確的規(guī)定。因此,有些客戶端根本就沒有使用頭信息,而IE只使用If-Range和Unless-Modified-Since。你最好用代碼檢查這些信息。采用這種方式的時候,你的應用程序可以在非常高的層次遵循HTTP規(guī)范,并可以使用多種瀏覽器。Range頭信息指明了被請求的字節(jié)范圍--在例子中它是服務器應該恢復文件流的起始點。 當IIS接收到恢復下載的請求類型時,它發(fā)回包含下面的頭信息的響應信息: HTTP/1.1 206 Partial Content
IE對于這些頭信息是很挑剔的。如果最初的響應沒有包含Etag頭信息,IE永遠不會嘗試恢復下載。我測試過的其它客戶端不使用ETag頭信息,它們簡單得依賴于文件名、請求范圍,并使用Last-Modified頭信息(如果它們試圖驗證該文件)。 深入了解HTTP協(xié)議 前面的部分中顯示的頭信息對于使恢復下載的解決方案運行來說是足夠的,但是它沒有完全覆蓋HTTP規(guī)范。 在單個請求中,Range頭信息可以詢問多個范圍,這種特性稱為"多部分范圍(multipart ranges)"。請不要與分段下載(segmented downloading)混淆,幾乎所有的下載工具都使用分段下載來提高下載速度。這些工具聲稱通過打開兩個或多個并發(fā)的連接(每個連接請求文件的不同范圍)提高了下載速度。 多部分范圍的想法并沒有開啟多個連接,但是它可以使客戶端軟件可以在單個請求/響應周期中請求某個文件的最前面的十個和最后面的十個字節(jié)。 誠實地說,我從來都沒有找到使用這種特性軟件片斷。但是我拒絕在代碼聲明中寫入"它并不是完全的HTTP兼容的"。略去這個特性必定會觸犯墨菲法則(Murphy's Law)。無論如何,多部分范圍還是被用于電子郵件傳輸中,把頭信息、普通文本和附件分開。 示例代碼 我們知道了客戶端和服務器如何交換頭信息以保證可恢復的下載,把這些知識與文件塊流的思想結(jié)合起來,你就可以給自己的ASP.NET應用程序增加可靠的下載管理能力了。 獲取下載過程的控制權(quán)的方法是從客戶端截取下載請求、讀取頭信息并適當?shù)仨憫T?NET之前,你必須編寫ISAPI(Internet服務器API)應用程序來實現(xiàn)這種功能,但是.NET框架組件提供了一個IHttpHandler接口,在類中實現(xiàn)的時候,它允許你僅僅使用.NET代碼就能夠截取和處理請求。這意味著你的應用程序?qū)τ谙螺d過程有完全控制權(quán)和響應性,再也不會涉及或使用IIS的自動化函數(shù)。 示例代碼在HttpHandler.vb文件中包含了一個自定義的HttpHandler類(ZIPHandler)。ZipHandler實現(xiàn)了IhttpHandler接口,并且處理對所有.zip文件的請求。 為了測試示例代碼,你需要在IIS中建立一個新的虛擬目錄,并把源文件復制到那兒。在該目錄中建立一個叫做download.zip的文件(請注意IIS和ASP.NET不能處理大于2GB的下載,因此要確保你的文件沒有超過該限制)。配置你的IIS虛擬目錄,通過aspnet_isapi.dll映射.zip擴展名。 HttpHandler類:ZIPHandler 在ASP.NET中映射了.zip擴展名之后,客戶端每次向服務器請求.zip文件的時候,IIS調(diào)用ZipHandler類的ProcessRequest方法(見下載代碼)。 ProcessRequest方法首先建立自定義的FileInformation類(見下載代碼)的一個實例,它封裝了下載的狀態(tài)(例如進行中、被中斷了等等)。示例把download.zip示例文件的路徑硬編碼到代碼中了。如果把這段代碼應用于你自己的應用程序,需要修改它來打開被請求的文件。
' 使用objRequest檢測請求了哪個文件,用該文件打開objFile。
接下來,程序使用描述的HTTP頭信息(如果請求提供了頭信息)執(zhí)幸幌盜械難櫓ぜ觳欏K衙恐旨觳槎擠庾霸諦⌒退接瀉校綣櫓こ曬Φ幕熬頭禱豑rue。如果某個驗證檢查失敗了,響應會立即終止,并發(fā)送適當?shù)腟tatusCode值。
If Not objRequest.HttpMethod.Equals(HTTP_METHOD_GET) Or Not
這些初步檢查的函數(shù)中的ParseRequestHeaderRange(見下載代碼)檢查客戶端是否請求了文件范圍(這意味著是一個局部下載)。如果被請求的范圍是無效的(無效范圍指超越文件大小或包含不合理數(shù)字的范圍數(shù)值),該方法把bIsRangeRequest設(shè)置為True。如果請求了范圍,CheckIfRange方法會驗證IfRange頭信息。 如果被請求的范圍是有效的,代碼會計算響應信息的大小。如果客戶端請求了多個范圍,響應信息大小的數(shù)值會包含多部分頭部信息長度的數(shù)值。 如果不能確定某個發(fā)送的頭部信息值,程序?qū)堰@個下載請求作為最初請求而不是部分下載來處理,從文件的頂部開始發(fā)送一個新的下載流。
If bIsRangeRequest AndAlso CheckIfRange(objRequest, objFile) Then If bMultipart Then
FileInformation輔助類 在ZIPHandler部分中你會發(fā)現(xiàn),F(xiàn)ileInformation是一個輔助類,它封裝了下載狀態(tài)信息(例如下載中、中斷等等)。 為了建立FileInformation的實例,你需要把被請求文件的路徑傳遞給該類的構(gòu)造函數(shù): Public Sub New(ByVal sPath As String) FileInformation使用System.IO.FileInfo對象來獲取文件的信息,這些信息是作為該對象的屬性暴露的(例如文件是否存在、文件全名、大小等等)。這個類還暴露了一個DownloadState枚舉,它描述了下載請求的多種狀態(tài): Enum DownloadState FileInformation還提供了EntityTag屬性值。示例代碼中的這個值是硬編碼的,這是由于示例代碼只使用了一個下載文件,并且該文件不會被改變,但是對于實際應用程序來說,你會提供多個文件,甚至于動態(tài)地建立文件,你的代碼必須為每個文件提供一個唯一的EntityTag值。此外,每次改變或修改該文件的時候,這個值也必須改變。這使客戶端軟件能夠驗證它們已經(jīng)下載的字節(jié)塊是否仍然是最新的。下面是示例代碼中返回硬編碼EntityTag值的部分: Public ReadOnly Property EntityTag() As String 一個簡單的和大致足夠安全的EntityTag可能由文件名和文件最后被修改的日期組成。無論使用什么方法,你都必須確保這個值是真的是唯一的,不會與其它文件的EntityTag混淆。我希望在自己的應用程序中按照客戶、顧客和郵編索引來動態(tài)地替被建立的文件命名,并把用作EntityTag的GUID存儲在數(shù)據(jù)庫中。 ZipFileHandler類讀取和設(shè)置公共的State屬性。在完成下載以后,它把State設(shè)置為fsDownloadFinished。這個時候你就可以刪除臨時文件了。這兒一般需要調(diào)用Save方法來維持狀態(tài)。 Public Property State() As DownloadState 在文件狀態(tài)發(fā)生改變的任何時候ZipFileHandler都應該調(diào)用Save方法,保存文件的狀態(tài),這樣在以后才能顯示給用戶。你還可以用它來保存你自己建立的EntityTag。請不要把文件的狀態(tài)和EntityTag值保存在Application、Session或Cache中--你必須跨越所有的這些這些對象的生命周期來保存信息。 Private Sub Save() 前面提到,示例代碼只處理一個已有的文件(download.zip),但是你可以進一步增強這個程序,根據(jù)需要建立被請求的文件。 測試示例代碼的時候,你的本地系統(tǒng)或LAN可能太快了,以至于無法中斷下載過程,因此我推薦你使用慢速LAN連接(在IIS中減少站點的帶寬是一種模擬的方法)或者把服務器放到互聯(lián)網(wǎng)上。 在客戶端上下載文件仍然很艱難。ISP操作的不對的或配置錯誤的Web緩沖服務器都可能使大文件下載過程失敗,包括下載狀況惡化或早期對話終結(jié)。如果文件大小超過了255MB,你就應該鼓勵顧客使用第三方下載管理軟件,盡管某些最新的瀏覽器內(nèi)建了基本的下載管理器。 如果你希望進一步擴展示例代碼,查閱一下HTTP規(guī)范是有益的。你可以為下載建立MD5校驗值,使用Content-MD5頭信息添加它們,提供一種驗證下載文件完整性的途徑。示例代碼除了GET和HEAD之外沒有涉及到其它的HTTP方法。 |
溫馨提示:喜歡本站的話,請收藏一下本站!