在Web應用程序中處理大文件下載的問題一直出了名的困難因此對於大多數站點來說如果用戶的下載被中斷了它們只能說悲哀降臨到用戶的身上了但是我們現在不必這樣了因為你可以使自己的ASPNET應用程序有能力支持可恢復(繼續)的大文件下載使用本文提供的方法的時候你可以跟蹤下載的過程這樣你就可以處理動態建立的文件——而且要達到這個目標根本不需要舊式的ISAPI動態鏈接庫和非受控的(unmanaged)C++代碼
為客戶端提供從互聯網上下載文件的服務最容易了對嗎?僅僅只需要把可下載的文件復制到你的Web應用程序目錄中發布鏈接並讓IIS完成所有相關的工作但是文件服務不應該比脖子上的疼痛還要多(還要麻煩)你不希望整個世界都能訪問自己的數據你不希望服務器被數百個靜態文件塞滿了你甚至於希望下載臨時文件——只有當客戶端開始下載後的空閒時間才建立這些文件
不幸的是使用IIS對下載請求的默認的響應是不可能達到這些效果的因此在一般情況下為了獲得對下載過程的控制權開發者需要鏈接到一個定制的aspx頁面在這個頁面中它們檢查用戶憑證(credential)建立可以下載的文件並使用下面的代碼把該文件推送給客戶端
ResponseWriteFile
ResponseEnd()
而這就是出現真正麻煩的地方
有什麼問題?
WriteFile方法看起來非常完美它使文件的二進制數據流向客戶端但是直到最近我們才知道WriteFile方法是一個出名的內存占用狂它把整個文件載入服務器的RAM中來提供服務(實際上它甚至於會占用文件兩倍大小的空間)對於大文件這會引起服務內存問題並且可能重復ASPNET過程但是在年月微軟發布了一個補丁解決了這個問題這個補丁現在是NET Framework 補丁包(SP)的一部分
這個補丁引入了TransmitFile方法它把一個磁盤文件讀入到較小的內存緩沖區之後就開始傳輸該文件盡管這個方案解決了內存和循環的問題但是它仍然不能令人滿意你不能控制響應的生命周期你無法知道下載是否正確地完成了你沒有辦法知道下載是否被中斷了並且(如果你建立了臨時文件)你也不知道是否應該以及什麼時候可以刪除這些文件更糟的是如果下載的確失敗了TransmitFile方法又從客戶端下次嘗試的文件頭部開始下載
其中一種可能的解決方案——實現後台智能傳輸服務(BITS)對於多數站點來說是不可行的因為這會毀掉維持客戶端浏覽器和操作系統獨立性而作出的努力
令人滿意的解決方案的基礎還是來自微軟用於解決WriteFile引起的內存混亂問題的第一次嘗試(見知識庫文章)那篇文章演示了智能的大塊數據下載過程它從文件流中讀取數據在服務器把字節塊發送給客戶端之前它使用ResponseIsClientConnected屬性檢查客戶端是否仍然保持著連接如果仍然保持連接它就繼續發送流字節否則就停止以防止服務器發送不必要的數據
這就是我們采用的方法特別是在下載臨時文件的時候在IsClientConnected返回False的情況下你就知道下載過程被中斷了你應該保存文件反之當這個過程成功完成的時候你就刪除臨時文件此外為了恢復中斷了的下載你需要做的工作是從上次下載嘗試過程中客戶端連接失敗的文件點開始下載
HTTP協議和頭信息(Header)支持
HTTP協議支持可以用於處理被中斷下載的頭信息使用少量的HTTP頭信息你可以增強自己的下載過程使它完全遵循HTTP協議規范這個規范與ranges一起提供恢復被中斷的下載所需要的一切信息
下面是它的工作方式首先如果服務器支持客戶端斷點續傳它就在初始的響應中發送AcceptRanges頭信息服務器還發送一個實體標簽(entity tag)頭信息(ETag)它包含一個唯一的標識字符串
下面的代碼顯示了IIS發送給客戶端的用於響應一個初始下載請求的一些頭信息它向客戶端傳遞了被請求的文件的詳細信息
HTTP/ OK
Connection: close
Date: Tue Oct :: GMT
AcceptRanges: bytes
LastModified: Sun Sep :: GMT
ETag: febbcfdc:
CacheControl: private
ContentType: application/xzipcompressed
ContentLength:
在接收這些頭信息之後
如果下載被中斷了
IE浏覽器在後來的下載請求中會把Etag值和Range頭信息發送回服務器
下面的代碼顯示了嘗試恢復被中斷下載時IE發送給服務器的一些頭信息
GET HTTP/
Range: bytes=
Unless
Modified
Since: Sun
Sep
:
:
GMT
If
Range:
febb
cfd
c
:
這些頭信息表明IE緩存了IIS提供的實體標簽並在IfRange頭信息中把它發送回服務器了這是確保下載從准確相同的文件恢復的一種途徑不幸的是並非所有的浏覽器的工作方式都相同客戶端發送的用於驗證文件的其它HTTP頭信息可能是IfMatchIfUnmodifiedSince或者UnlessModifiedSince很明顯該規范對於客戶端軟件必須支持哪些頭信息或者必須使用哪些頭信息沒有明確的規定因此有些客戶端根本就沒有使用頭信息而IE只使用IfRange和UnlessModifiedSince你最好用代碼檢查這些信息采用這種方式的時候你的應用程序可以在非常高的層次遵循HTTP規范並可以使用多種浏覽器Range頭信息指明了被請求的字節范圍——在例子中它是服務器應該恢復文件流的起始點
當IIS接收到恢復下載的請求類型時它發回包含下面的頭信息的響應信息
HTTP/ Partial Content
ContentRange: bytes /
AcceptRanges: bytes
LastModified: Sun Sep :: GMT
ETag: febbcfdc:
CacheControl: private
ContentType: application/xzipcompressed
ContentLength:
請注意上面的代碼與最初的下載請求的HTTP響應有點差別——恢復下載的請求是而最初下載的請求是這表明通過線路傳遞進來的內容是部分文件這一次ContentRange頭信息指出了被傳遞字節的精確數量和位置
IE對於這些頭信息是很挑剔的如果最初的響應沒有包含Etag頭信息IE永遠不會嘗試恢復下載我測試過的其它客戶端不使用ETag頭信息它們簡單得依賴於文件名請求范圍並使用LastModified頭信息(如果它們試圖驗證該文件)
深入了解HTTP協議
前面的部分中顯示的頭信息對於使恢復下載的解決方案運行來說是足夠的但是它沒有完全覆蓋HTTP規范
在單個請求中Range頭信息可以詢問多個范圍這種特性稱為多部分范圍(multipart ranges)請不要與分段下載(segmented downloading)混淆幾乎所有的下載工具都使用分段下載來提高下載速度這些工具聲稱通過打開兩個或多個並發的連接(每個連接請求文件的不同范圍)提高了下載速度
多部分范圍的想法並沒有開啟多個連接但是它可以使客戶端軟件可以在單個請求/響應周期中請求某個文件的最前面的十個和最後面的十個字節
誠實地說我從來都沒有找到使用這種特性軟件片斷但是我拒絕在代碼聲明中寫入它並不是完全的HTTP兼容的略去這個特性必定會觸犯墨菲法則(Murphys Law)無論如何多部分范圍還是被用於電子郵件傳輸中把頭信息普通文本和附件分開
示例代碼
我們知道了客戶端和服務器如何交換頭信息以保證可恢復的下載把這些知識與文件塊流的思想結合起來你就可以給自己的ASPNET應用程序增加可靠的下載管理能力了
獲取下載過程的控制權的方法是從客戶端截取下載請求讀取頭信息並適當地響應在NET之前你必須編寫ISAPI(Internet服務器API)應用程序來實現這種功能但是NET框架組件提供了一個IHttpHandler接口在類中實現的時候它允許你僅僅使用NET代碼就能夠截取和處理請求這意味著你的應用程序對於下載過程有完全控制權和響應性再也不會涉及或使用IIS的自動化函數
示例代碼在HttpHandlervb文件中包含了一個自定義的HttpHandler類(ZIPHandler)ZipHandler實現了IhttpHandler接口並且處理對所有zip文件的請求
為了測試示例代碼你需要在IIS中建立一個新的虛擬目錄並把源文件復制到那兒在該目錄中建立一個叫做downloadzip的文件(請注意IIS和ASPNET不能處理大於GB的下載因此要確保你的文件沒有超過該限制)配置你的IIS虛擬目錄通過aspnet_isapidll映射zip擴展名
HttpHandler類ZIPHandler
在ASPNET中映射了zip擴展名之後客戶端每次向服務器請求zip文件的時候IIS調用ZipHandler類的ProcessRequest方法(見下載代碼)
ProcessRequest方法首先建立自定義的FileInformation類(見下載代碼)的一個實例它封裝了下載的狀態(例如進行中被中斷了等等)示例把downloadzip示例文件的路徑硬編碼到代碼中了如果把這段代碼應用於你自己的應用程序需要修改它來打開被請求的文件
使用objRequest檢測請求了哪個文件用該文件打開objFile
例如objFile = New DownloadFileInformation(<完整文件名>)
objFile = New DownloadFileInformation( _
objContextServerMapPath(~/downloadzip))
接下來
程序使用描述的HTTP頭信息(如果請求提供了頭信息)執行一系列的驗證檢查
它把每種檢查都封裝在小型私有函數中
如果驗證成功的話就返回True
如果某個驗證檢查失敗了
響應會立即終止
並發送適當的StatusCode值
If Not objRequest
HttpMethod
Equals(HTTP_METHOD_GET) Or Not
objRequest
HttpMethod
Equals(HTTP_METHOD_HEAD) Then
目前只支持GET和HEAD方法
objResponse
StatusCode =
沒有執行
ElseIf Not objFile
Exists Then
無法找到被請求的文件
objResponse
StatusCode =
沒有找到
ElseIf objFile
Length > Int
MaxValue Then
文件太大了
objResponse
StatusCode =
請求實體太大
ElseIf Not ParseRequestHeaderRange(objRequest
alRequestedRangesBegin
alRequestedRangesend
_
objFile
Length
bIsRangeRequest) Then
Range請求中包含無用的實體
objResponse
StatusCode =
無用的請求
ElseIf Not CheckIfModifiedSince(objRequest
objFile) Then
實體沒有被修改過
objResponse
StatusCode =
沒有被修改過
ElseIf Not CheckIfUnmodifiedSince(objRequest
objFile) Then
實體在上次被請求的日期之後被修改過
objResponse
StatusCode =
預處理失敗
ElseIf Not CheckIfMatch(objRequest
objFile) Then
實體與請求不匹配
objResponse
StatusCode =
預處理失敗
ElseIf Not CheckIfNoneMatch(objRequest
objResponse
objFile) Then
實體的確與none
match請求匹配
響應代碼位於CheckIfNoneMatch函數中
Else
初步檢查成功
這些初步檢查的函數中的ParseRequestHeaderRange(見下載代碼)檢查客戶端是否請求了文件范圍(這意味著是一個局部下載)如果被請求的范圍是無效的(無效范圍指超越文件大小或包含不合理數字的范圍數值)該方法把bIsRangeRequest設置為True如果請求了范圍CheckIfRange方法會驗證IfRange頭信息
如果被請求的范圍是有效的代碼會計算響應信息的大小如果客戶端請求了多個范圍響應信息大小的數值會包含多部分頭部信息長度的數值
如果不能確定某個發送的頭部信息值程序將把這個下載請求作為最初請求而不是部分下載來處理從文件的頂部開始發送一個新的下載流
If bIsRangeRequest AndAlso CheckIfRange(objRequest objFile) Then
這是范圍請求
如果Range數組包含多個實體它還是一個多部分范圍請求
bMultipart = CBool(alRequestedRangesBeginGetUpperBound()>)
進入每個范圍來獲取整個響應長度
For iLoop = alRequestedRangesBeginGetLowerBound() To alRequestedRangesBeginGetUpperBound()
內容的長度(這個范圍的)
iResponseContentLength += ConvertToInt(alRequestedRangesend( _
iLoop) alRequestedRangesBegin(iLoop)) +
If bMultipart Then
如果是多部分范圍請求計算出將發送的中間頭信息的長度
iResponseContentLength += MULTIPART_BOUNDARYLength
iResponseContentLength += objFileContentTypeLength
iResponseContentLength += alRequestedRangesBegin(iLoop)ToStringLength
iResponseContentLength += alRequestedRangesend(iLoop)ToStringLength
iResponseContentLength += objFileLengthToStringLength
是多部分下載中換行和其它必要的字符的長度
iResponseContentLength +=
End If
Next iLoop
If bMultipart Then
如果是多部分范圍請求
我們還必須計算出將發送的最後一個中間頭信息的長度
iResponseContentLength +=MULTIPART_BOUNDARYLength
是破折號和換行符的長度
iResponseContentLength +=
Else
不是多部分下載因此我們必須說明初始HTTP頭信息的響應范圍
objResponseAppendHeader( HTTP_HEADER_CONTENT_RANGE bytes & _
alRequestedRangesBegin()ToString & & _
alRequestedRangesend()ToString & / & _
objFileLengthToString)
End If
范圍響應
objResponseStatusCode = 局部響應
Else
這不是范圍請求或者被請求的范圍實體ID與當前的實體ID不匹配
因此開始新的下載
指明文件完成部分的大小等於內容的長度
iResponseContentLength =ConvertToInt(objFileLength)
返回正常的OK狀態
objResponseStatusCode =
End If
接下來服務器必須發送幾個重要的響應頭信息例如內容長度Etag和文件的內容類型
把內容長度寫入響應
objResponseAppendHeader( HTTP_HEADER_CONTENT_LENGTHiResponseContentLengthToString)
把最後修改日期寫入響應
objResponseAppendHeader( HTTP_HEADER_LAST_MODIFIEDobjFileLastWriteTimeUTCToString(r))
告訴客戶端軟件我們接受了范圍請求
objResponseAppendHeader( HTTP_HEADER_ACCEPT_RANGESHTTP_HEADER_ACCEPT_RANGES_BYTES)
把文件的實體標簽寫入響應(用引號括起來)
objResponseAppendHeader(HTTP_HEADER_ENTITY_TAG & objFileEntityTag & )
把內容類型寫入響應
If bMultipart Then
多部分消息有這種特殊的類型
在例子中文件實際的mime類型在以後才寫入響應
objResponseContentType = MULTIPART_CONTENTTYPE
Else
單個部分消息擁有的文件內容類型
objResponseContentType = objFileContentType
End If
下載所需要的一切都准備好了可以開始下載文件了你將使用FileStream對象從文件中讀取字節塊把FileInformation實例objFile的State屬性設置為fsDownloadInProgress只要客戶端保持連接服務器就從文件中讀取字節塊並發送給客戶端對於多部分下載這段代碼會發送特定的頭信息如果客戶端中斷連接服務器就把文件狀態設置為fsDownloadBroken如果服務器完成了被請求范圍的發送過程它會把狀態設置為fsDownloadFinished(見下載代碼)
FileInformation輔助類
在ZIPHandler部分中你會發現FileInformation是一個輔助類它封裝了下載狀態信息(例如下載中中斷等等)
為了建立FileInformation的實例你需要把被請求文件的路徑傳遞給該類的構造函數
Public Sub New(ByVal sPath As String)
m_objFile = New SystemIOFileInfo(sPath)
End Sub
FileInformation使用SystemIOFileInfo對象來獲取文件的信息這些信息是作為該對象的屬性暴露的(例如文件是否存在文件全名大小等等)這個類還暴露了一個DownloadState枚舉它描述了下載請求的多種狀態
<Flags()> Enum DownloadState
Clear
沒有下載過程
文件可能在維護
fsClear =
Locked
動態建立的文件不能被更改
fsLocked =
In Progress
文件被鎖定了
下載過程正在進行
fsDownloadInProgress =
Broken
文件被鎖定了
下載過程正在進行
但是被取消了
fsDownloadBroken =
Finished
文件被鎖定了
下載過程完成了
fsDownloadFinished =
End Enum FileInformation還提供了EntityTag屬性值
示例代碼中的這個值是硬編碼的
這是由於示例代碼只使用了一個下載文件
並且該文件不會被改變
但是對於實際應用程序來說
你會提供多個文件
甚至於動態地建立文件
你的代碼必須為每個文件提供一個唯一的EntityTag值
此外
每次改變或修改該文件的時候
這個值也必須改變
這使客戶端軟件能夠驗證它們已經下載的字節塊是否仍然是最新的
下面是示例代碼中返回硬編碼EntityTag值的部分
Public ReadOnly Property EntityTag() As String
EntityTag用於對客戶端的初始(
)響應
以及來自客戶端的恢復請求
Get
為文件建立唯一的字符串
注意
只要文件沒有發生改變
該唯一碼就必須保留
但是
如果文件的確改變了或者被修改了
這個碼必須改變
Return
MyExampleFileID
End Get
End Property
一個簡單的和大致足夠安全的EntityTag可能由文件名和文件最後被修改的日期組成無論使用什麼方法你都必須確保這個值是真的是唯一的不會與其它文件的EntityTag混淆我希望在自己的應用程序中按照客戶顧客和郵編索引來動態地替被建立的文件命名並把用作EntityTag的GUID存儲在數據庫中
ZipFileHandler類讀取和設置公共的State屬性在完成下載以後它把State設置為fsDownloadFinished這個時候你就可以刪除臨時文件了這兒一般需要調用Save方法來維持狀態
Public Property State() As DownloadState
Get
Return m_nState
End Get
Set(ByVal nState As DownloadState)
m_nState = nState
可選操作這個時候你可以自動地刪除文件
如果狀態被設置為Finished 你就再也不需要這個文件了
If nState =DownloadStatefsDownloadFinished Then
Clear()
Else
Save()
End If
Save()
End Set
End Property
在文件狀態發生改變的任何時候ZipFileHandler都應該調用Save方法
保存文件的狀態
這樣在以後才能顯示給用戶
你還可以用它來保存你自己建立的EntityTag
請不要把文件的狀態和EntityTag值保存在Application
Session或Cache中——你必須跨越所有的這些這些對象的生命周期來保存信息
Private Sub Save()
把該文件下載的狀態保存到數據庫或XML文件中
當然
如果你並沒有動態地建立文件
就不需要保存這個狀態
End Sub
前面提到示例代碼只處理一個已有的文件(downloadzip)但是你可以進一步增強這個程序根據需要建立被請求的文件
測試示例代碼的時候你的本地系統或LAN可能太快了以至於無法中斷下載過程因此我推薦你使用慢速LAN連接(在IIS中減少站點的帶寬是一種模擬的方法)或者把服務器放到互聯網上
在客戶端上下載文件仍然很艱難ISP操作的不對的或配置錯誤的Web緩沖服務器都可能使大文件下載過程失敗包括下載狀況惡化或早期對話終結如果文件大小超過了MB你就應該鼓勵顧客使用第三方下載管理軟件盡管某些最新的浏覽器內建了基本的下載管理器
如果你希望進一步擴展示例代碼查閱一下HTTP規范是有益的你可以為下載建立MD校驗值使用ContentMD頭信息添加它們提供一種驗證下載文件完整性的途徑示例代碼除了GET和HEAD之外沒有涉及到其它的HTTP方法
From:http://tw.wingwit.com/Article/program/net/201311/12311.html