// 輸出硬盤文件,提供下載 // 輸入參數 _Request: Page.Request對象, _Response: Page.Response對象, _fileName: 下載文件名, _fullPath: 帶文件名下載路徑, _speed 每秒容許下載的字節數 // 返回是否成功 public static bool ResponseFile(HttpRequest _Request,HttpResponse _Response,string _fileName,string _fullPath, long _speed) { try { FileStream myFile = new FileStream(_fullPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); BinaryReader br = new BinaryReader(myFile); try { _Response.AddHeader("Accept-Ranges", "bytes"); _Response.Buffer = false; long fileLength = myFile.Length; long startBytes = 0; int pack = 10240; //10K bytes //int sleep = 200; //每秒5次 即5*10K bytes每秒 int sleep = (int)Math.Floor(1000 * pack / _speed) + 1; if (_Request.Headers["Range"] != null) { _Response.StatusCode = 206; string[] range = _Request.Headers["Range"].Split(new char[] {'=', '-'}); startBytes = Convert.ToInt64(range[1]); } _Response.AddHeader("Content-Length", (fileLength - startBytes).ToString()); if (startBytes != 0) { _Response.AddHeader("Content-Range", string.Format(" bytes {0}-{1}/{2}", startBytes, fileLength-1, fileLength)); } _Response.AddHeader("Connection", "Keep-Alive"); _Response.ContentType = "application/octet-stream"; _Response.AddHeader("Content-Disposition","attachment;filename=" + HttpUtility.UrlEncode(_fileName,System.Text.Encoding.UTF8) ); br.BaseStream.Seek(startBytes, SeekOrigin.Begin); int maxCount = (int) Math.Floor((fileLength - startBytes) / pack) + 1; for (int i = 0; i < maxCount; i++) { if (_Response.IsClientConnected) { _Response.BinaryWrite(br.ReadBytes(pack)); Thread.Sleep(sleep); } else { i=maxCount; } } } catch { return false; } finally { br.Close(); myFile.Close(); } } catch { return false; } return true; } 調用例 Page.Response.Clear(); bool success = ResponseFile(Page.Request, Page.Response, "filename", @"C:\download.date", 1024000); if(!success) Response.Write("下載文件出錯!"); Page.Response.End();
上文摘自 http://www.iwms.net/n1210c12.aspxweb
在ASP.NET中跟蹤和恢復大文件下載 (http://www.iwms.net/n1300c12.aspx)數據庫
在Web應用程序中處理大文件下載的問題一直出了名的困難,所以對於大多數站點來講,若是用戶的下載被中斷了,它們只能說悲哀降臨到用戶的身上了。可是咱們如今沒必要這樣了,由於你可使本身的ASP.NET應用程序有能力支持可恢復(繼續)的大文件下載。使用本文提供的方法的時候,你能夠跟蹤下載的過程,這樣你就能夠處理動態創建的文件--並且要達到這個目標根本不須要舊式的ISAPI動態連接庫和非受控的(unmanaged)C++代碼。
爲客戶端提供從互聯網上下載文件的服務最容易了,對嗎?僅僅只須要把可下載的文件複製到你的Web應用程序目錄中,發佈連接並讓IIS完成全部相關的工做。可是,文件服務不該該比脖子上的疼痛還要多(還要麻煩),你不但願整個世界都能訪問本身的數據,你不但願服務器被數百個靜態文件塞滿了,你甚至於但願下載臨時文件--只有當客戶端開始下載後的空閒時間才創建這些文件。
不幸的是,使用IIS對下載請求的默認的響應是不可能達到這些效果的。所以在通常狀況下,爲了得到對下載過程的控制權,開發者須要連接到一個定製的.aspx頁面,在這個頁面中它們檢查用戶憑證(credential)、創建能夠下載的文件並使用下面的代碼把該文件推送給客戶端:
api
Response.WriteFile Response.End() |
而這就是出現真正麻煩的地方。
有什麼問題?
WriteFile方法看起來很是完美,它使文件的二進制數據流向客戶端。可是直到最近咱們才知道,WriteFile方法是一個出名的內存佔用狂,它把整個文件載入服務器的RAM中來提供服務(實際上它甚至於會佔用文件兩倍大小的空間)。對於大文件,這會引發服務內存問題,而且可能重複ASP.NET過程。可是在2004年6月微軟發佈了一個補丁解決了這個問題。這個補丁如今是.NET Framework 1.1補丁包(SP1)的一部分。
這個補丁引入了TransmitFile方法,它把一個磁盤文件讀入到較小的內存緩衝區以後就開始傳輸該文件。儘管這個方案解決了內存和循環的問題,可是它仍然不能使人滿意。你不能控制響應的生命週期。你沒法知道下載是否正確地完成了,你沒有辦法知道下載是否被中斷了,而且(若是你創建了臨時文件)你也不知道是否應該、以及何時能夠刪除這些文件。更糟的是,若是下載的確失敗了,TransmitFile方法又從客戶端下次嘗試的文件頭部開始下載。
其中一種可能的解決方案--實現後臺智能傳輸服務(BITS)對於多數站點來講是不可行的,由於這會毀掉維持客戶端瀏覽器和操做系統獨立性而做出的努力。
使人滿意的解決方案的基礎仍是來自微軟用於解決WriteFile引發的內存混亂問題的第一次嘗試(見知識庫文章812406)。那篇文章演示了智能的大塊數據下載過程,它從文件流中讀取數據。在服務器把字節塊發送給客戶端以前,它使用Response.IsClientConnected屬性檢查客戶端是否仍然保持着鏈接。若是仍然保持鏈接,它就繼續發送流字節,不然就中止,以防止服務器發送沒必要要的數據。
這就是咱們採用的方法,特別是在下載臨時文件的時候。在IsClientConnected返回False的狀況下,你就知道下載過程被中斷了,你應該保存文件;反之,當這個過程成功完成的時候,你就刪除臨時文件。此外,爲了恢復中斷了的下載,你須要作的工做是從上次下載嘗試過程當中客戶端鏈接失敗的文件點開始下載。
HTTP協議和頭信息(Header)支持
HTTP協議支持能夠用於處理被中斷下載的頭信息。使用少許的HTTP頭信息,你能夠加強本身的下載過程,使它徹底遵循HTTP協議規範。這個規範與ranges一塊兒提供恢復被中斷的下載所須要的一切信息。
下面是它的工做方式。首先,若是服務器支持客戶端斷點續傳,它就在初始的響應中發送Accept-Ranges頭信息。服務器還發送一個實體標籤(entity tag)頭信息(ETag),它包含一個惟一的標識字符串。
下面的代碼顯示了IIS發送給客戶端的用於響應一個初始下載請求的一些頭信息,它向客戶端傳遞了被請求的文件的詳細信息。
數組
HTTP/1.1 200 OK Connection: close Date: Tue, 19 Oct 2004 15:11:23 GMT Accept-Ranges: bytes Last-Modified: Sun, 26 Sep 2004 15:52:45 GMT ETag: "47febb2cfd76c41:2062" Cache-Control: private Content-Type: application/x-zip-compressed Content-Length: 2844011 |
在接收這些頭信息以後,若是下載被中斷了,IE瀏覽器在後來的下載請求中會把Etag值和Range頭信息發送回服務器。下面的代碼顯示了嘗試恢復被中斷下載時IE發送給服務器的一些頭信息。
瀏覽器
GET http://192.168.100.100/download.zip HTTP/1.0 Range: bytes=822603- Unless-Modified-Since: Sun, 26 Sep 2004 15:52:45 GMT If-Range: "47febb2cfd76c41:2062" |
這些頭信息代表IE緩存了IIS提供的實體標籤,並在If-Range頭信息中把它發送回服務器了,這是確保下載從準確相同的文件恢復的一種途徑。不幸的是,並不是全部的瀏覽器的工做方式都相同。客戶端發送的用於驗證文件的其它HTTP頭信息多是If-Match、If-Unmodified-Since或者Unless-Modified-Since。很明顯,該規範對於客戶端軟件必須支持哪些頭信息,或者必須使用哪些頭信息沒有明確的規定。所以,有些客戶端根本就沒有使用頭信息,而IE只使用If-Range和Unless-Modified-Since。你最好用代碼檢查這些信息。採用這種方式的時候,你的應用程序能夠在很是高的層次遵循HTTP規範,並可使用多種瀏覽器。Range頭信息指明瞭被請求的字節範圍--在例子中它是服務器應該恢復文件流的起始點。
當IIS接收到恢復下載的請求類型時,它發回包含下面的頭信息的響應信息:
緩存
HTTP/1.1 206 Partial Content Content-Range: bytes 822603-2844010/2844011 Accept-Ranges: bytes Last-Modified: Sun, 26 Sep 2004 15:52:45 GMT ETag: "47febb2cfd76c41:2062" Cache-Control: private Content-Type: application/x-zip-compressed Content-Length: 2021408 |
請注意上面的代碼與最初的下載請求的HTTP響應有點差異--恢復下載的請求是206而最初下載的請求是200。這代表經過線路傳遞進來的內容是部分文件。這一次Content-Range頭信息指出了被傳遞字節的精確數量和位置。
IE對於這些頭信息是很挑剔的。若是最初的響應沒有包含Etag頭信息,IE永遠不會嘗試恢復下載。我測試過的其它客戶端不使用ETag頭信息,它們簡單得依賴於文件名、請求範圍,並使用Last-Modified頭信息(若是它們試圖驗證該文件)。
深刻了解HTTP協議
前面的部分中顯示的頭信息對於使恢復下載的解決方案運行來講是足夠的,可是它沒有徹底覆蓋HTTP規範。
在單個請求中,Range頭信息能夠詢問多個範圍,這種特性稱爲"多部分範圍(multipart ranges)"。請不要與分段下載(segmented downloading)混淆,幾乎全部的下載工具都使用分段下載來提升下載速度。這些工具聲稱經過打開兩個或多個併發的鏈接(每一個鏈接請求文件的不一樣範圍)提升了下載速度。
多部分範圍的想法並無開啓多個鏈接,可是它可使客戶端軟件能夠在單個請求/響應週期中請求某個文件的最前面的十個和最後面的十個字節。
誠實地說,我歷來都沒有找到使用這種特性軟件片段。可是我拒絕在代碼聲明中寫入"它並非徹底的HTTP兼容的"。略去這個特性一定會觸犯墨菲法則(Murphy's Law)。不管如何,多部分範圍仍是被用於電子郵件傳輸中,把頭信息、普通文本和附件分開。安全
咱們知道了客戶端和服務器如何交換頭信息以保證可恢復的下載,把這些知識與文件塊流的思想結合起來,你就能夠給本身的ASP.NET應用程序增長可靠的下載管理能力了。
獲取下載過程的控制權的方法是從客戶端截取下載請求、讀取頭信息並適當地響應。在.NET以前,你必須編寫ISAPI(Internet服務器API)應用程序來實現這種功能,可是.NET框架組件提供了一個IHttpHandler接口,在類中實現的時候,它容許你僅僅使用.NET代碼就可以截取和處理請求。這意味着你的應用程序對於下載過程有徹底控制權和響應性,不再會涉及或使用IIS的自動化函數。
示例代碼在HttpHandler.vb文件中包含了一個自定義的HttpHandler類(ZIPHandler)。ZipHandler實現了IhttpHandler接口,而且處理對全部.zip文件的請求。
爲了測試示例代碼,你須要在IIS中創建一個新的虛擬目錄,並把源文件複製到那兒。在該目錄中創建一個叫作download.zip的文件(請注意IIS和ASP.NET不能處理大於2GB的下載,所以要確保你的文件沒有超過該限制)。配置你的IIS虛擬目錄,經過aspnet_isapi.dll映射.zip擴展名。
HttpHandler類:ZIPHandler
在ASP.NET中映射了.zip擴展名以後,客戶端每次向服務器請求.zip文件的時候,IIS調用ZipHandler類的ProcessRequest方法(見下載代碼)。
ProcessRequest方法首先創建自定義的FileInformation類(見下載代碼)的一個實例,它封裝了下載的狀態(例如進行中、被中斷了等等)。示例把download.zip示例文件的路徑硬編碼到代碼中了。若是把這段代碼應用於你本身的應用程序,須要修改它來打開被請求的文件。
服務器
' 使用objRequest檢測請求了哪一個文件,用該文件打開objFile。 ' 例如objFile = New Download.FileInformation(<完整文件名>) objFile = New Download.FileInformation( _ objContext.Server.MapPath("~/download.zip")) |
接下來,程序使用描述的HTTP頭信息(若是請求提供了頭信息)執行一系列的驗證檢查。它把每種檢查都封裝在小型私有函數中,若是驗證成功的話就返回True。若是某個驗證檢查失敗了,響應會當即終止,併發送適當的StatusCode值。
併發
If Not objRequest.HttpMethod.Equals(HTTP_METHOD_GET) Or Not objRequest.HttpMethod.Equals(HTTP_METHOD_HEAD) Then ' 目前只支持GET和HEAD方法 objResponse.StatusCode = 501 ' 沒有執行 ElseIf Not objFile.Exists Then ' 沒法找到被請求的文件 objResponse.StatusCode = 404 ' 沒有找到 ElseIf objFile.Length > Int32.MaxValue Then ' 文件太大了 objResponse.StatusCode = 413 ' 請求實體太大 ElseIf Not ParseRequestHeaderRange(objRequest, alRequestedRangesBegin, alRequestedRangesend, _ objFile.Length, bIsRangeRequest) Then ' Range請求中包含無用的實體 objResponse.StatusCode = 400 ' 無用的請求 ElseIf Not CheckIfModifiedSince(objRequest,objFile) Then ' 實體沒有被修改過 objResponse.StatusCode = 304 ' 沒有被修改過 ElseIf Not CheckIfUnmodifiedSince(objRequest,objFile) Then ' 實體在上次被請求的日期以後被修改過 objResponse.StatusCode = 412 ' 預處理失敗 ElseIf Not CheckIfMatch(objRequest, objFile) Then ' 實體與請求不匹配 objResponse.StatusCode = 412 ' 預處理失敗 ElseIf Not CheckIfNoneMatch(objRequest, objResponse,objFile) Then ' 實體的確與none-match請求匹配。 ' 響應代碼位於CheckIfNoneMatch函數中 Else ' 初步檢查成功 |
這些初步檢查的函數中的ParseRequestHeaderRange(見下載代碼)檢查客戶端是否請求了文件範圍(這意味着是一個局部下載)。若是被請求的範圍是無效的(無效範圍指超越文件大小或包含不合理數字的範圍數值),該方法把bIsRangeRequest設置爲True。若是請求了範圍,CheckIfRange方法會驗證IfRange頭信息。
若是被請求的範圍是有效的,代碼會計算響應信息的大小。若是客戶端請求了多個範圍,響應信息大小的數值會包含多部分頭部信息長度的數值。
若是不能肯定某個發送的頭部信息值,程序將把這個下載請求做爲最初請求而不是部分下載來處理,從文件的頂部開始發送一個新的下載流。
app
If bIsRangeRequest AndAlso CheckIfRange(objRequest, objFile) Then ' 這是範圍請求 ' 若是Range數組包含多個實體,它仍是一個多部分範圍請求 bMultipart = CBool(alRequestedRangesBegin.GetUpperBound(0)>0) ' 進入每一個範圍來獲取整個響應長度 For iLoop = alRequestedRangesBegin.GetLowerBound(0) To alRequestedRangesBegin.GetUpperBound(0) ' 內容的長度(這個範圍的) iResponseContentLength += Convert.ToInt32(alRequestedRangesend( _ iLoop) - alRequestedRangesBegin(iLoop)) + 1 If bMultipart Then ' 若是是多部分範圍請求,計算出將發送的中間頭信息的長度 iResponseContentLength += MULTIPART_BOUNDARY.Length iResponseContentLength += objFile.ContentType.Length iResponseContentLength += alRequestedRangesBegin(iLoop).ToString.Length iResponseContentLength += alRequestedRangesend(iLoop).ToString.Length iResponseContentLength += objFile.Length.ToString.Length ' 49是多部分下載中換行和其它必要的字符的長度 iResponseContentLength += 49 End If Next iLoop If bMultipart Then ' 若是是多部分範圍請求, ' 咱們還必須計算出將發送的最後一箇中間頭信息的長度 iResponseContentLength +=MULTIPART_BOUNDARY.Length ' 8 是破折號和換行符的長度 iResponseContentLength += 8 Else ' 不是多部分下載,所以咱們必須說明初始HTTP頭信息的響應範圍 objResponse.AppendHeader( HTTP_HEADER_CONTENT_RANGE, "bytes " & _ alRequestedRangesBegin(0).ToString & "-" & _ alRequestedRangesend(0).ToString & "/" & _ objFile.Length.ToString) 'End If ' 範圍響應 objResponse.StatusCode = 206 ' 局部響應 Else ' 這不是範圍請求,或者被請求的範圍實體ID與當前的實體ID不匹配, ' 所以開始新的下載 ' 指明文件完成部分的大小等於內容的長度 iResponseContentLength =Convert.ToInt32(objFile.Length) ' 返回正常的OK狀態 objResponse.StatusCode = 200 End If ' 接下來服務器必須發送幾個重要的響應頭信息,例如內容長度、Etag、和文件的內容類型: ' 把內容長度寫入響應 objResponse.AppendHeader( HTTP_HEADER_CONTENT_LENGTH,iResponseContentLength.ToString) ' 把最後修改日期寫入響應 objResponse.AppendHeader( HTTP_HEADER_LAST_MODIFIED,objFile.LastWriteTimeUTC.ToString("r")) ' 告訴客戶端軟件咱們接受了範圍請求 objResponse.AppendHeader( HTTP_HEADER_ACCEPT_RANGES,HTTP_HEADER_ACCEPT_RANGES_BYTES) ' 把文件的實體標籤寫入響應(用引號括起來) objResponse.AppendHeader(HTTP_HEADER_ENTITY_TAG, """" & objFile.EntityTag & """") ' 把內容類型寫入響應 If bMultipart Then ' 多部分消息有這種特殊的類型 ' 在例子中文件實際的mime類型在之後才寫入響應 objResponse.ContentType = MULTIPART_CONTENTTYPE Else ' 單個部分消息擁有的文件內容類型 objResponse.ContentType = objFile.ContentType End If |
下載所須要的一切都準備好了,能夠開始下載文件了。你將使用FileStream對象從文件中讀取字節塊。把FileInformation實例objFile的State屬性設置爲fsDownloadInProgress。只要客戶端保持鏈接,服務器就從文件中讀取字節塊併發送給客戶端。對於多部分下載,這段代碼會發送特定的頭信息。若是客戶端中斷鏈接,服務器就把文件狀態設置爲fsDownloadBroken。若是服務器完成了被請求範圍的發送過程,它會把狀態設置爲fsDownloadFinished(見下載代碼)。
FileInformation輔助類
在ZIPHandler部分中你會發現,FileInformation是一個輔助類,它封裝了下載狀態信息(例以下載中、中斷等等)。
爲了創建FileInformation的實例,你須要把被請求文件的路徑傳遞給該類的構造函數:
Public Sub New(ByVal sPath As String) m_objFile = New System.IO.FileInfo(sPath) End Sub |
FileInformation使用System.IO.FileInfo對象來獲取文件的信息,這些信息是做爲該對象的屬性暴露的(例如文件是否存在、文件全名、大小等等)。這個類還暴露了一個DownloadState枚舉,它描述了下載請求的多種狀態:
<Flags()> Enum DownloadState ' Clear:沒有下載過程,文件可能在維護 fsClear = 1 ' Locked:動態創建的文件不能被更改 fsLocked = 2 ' In Progress:文件被鎖定了,下載過程正在進行 fsDownloadInProgress = 6 ' Broken:文件被鎖定了,下載過程正在進行,可是被取消了 fsDownloadBroken = 10 ' Finished:文件被鎖定了,下載過程完成了 fsDownloadFinished = 18 End Enum |
FileInformation還提供了EntityTag屬性值。示例代碼中的這個值是硬編碼的,這是因爲示例代碼只使用了一個下載文件,而且該文件不會被改變,可是對於實際應用程序來講,你會提供多個文件,甚至於動態地創建文件,你的代碼必須爲每一個文件提供一個惟一的EntityTag值。此外,每次改變或修改該文件的時候,這個值也必須改變。這使客戶端軟件可以驗證它們已經下載的字節塊是否仍然是最新的。下面是示例代碼中返回硬編碼EntityTag值的部分:
Public ReadOnly Property EntityTag() As String ' EntityTag用於對客戶端的初始(200)響應,以及來自客戶端的恢復請求 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 =DownloadState.fsDownloadFinished Then ' Clear() ' Else ' Save() ' End If Save() End Set End Property |
在文件狀態發生改變的任什麼時候候ZipFileHandler都應該調用Save方法,保存文件的狀態,這樣在之後才能顯示給用戶。你還能夠用它來保存你本身創建的EntityTag。請不要把文件的狀態和EntityTag值保存在Application、Session或Cache中--你必須跨越全部的這些這些對象的生命週期來保存信息。
Private Sub Save() ' 把該文件下載的狀態保存到數據庫或XML文件中。 ' 固然,若是你並無動態地創建文件,就不須要保存這個狀態。 End Sub |
前面提到,示例代碼只處理一個已有的文件(download.zip),可是你能夠進一步加強這個程序,根據須要創建被請求的文件。
測試示例代碼的時候,你的本地系統或LAN可能太快了,以致於沒法中斷下載過程,所以我推薦你使用慢速LAN鏈接(在IIS中減小站點的帶寬是一種模擬的方法)或者把服務器放到互聯網上。
在客戶端上下載文件仍然很艱難。ISP操做的不對的或配置錯誤的Web緩衝服務器均可能使大文件下載過程失敗,包括下載情況惡化或早期對話終結。若是文件大小超過了255MB,你就應該鼓勵顧客使用第三方下載管理軟件,儘管某些最新的瀏覽器內建了基本的下載管理器。
若是你但願進一步擴展現例代碼,查閱一下HTTP規範是有益的。你能夠爲下載創建MD5校驗值,使用Content-MD5頭信息添加它們,提供一種驗證下載文件完整性的途徑。示例代碼除了GET和HEAD以外沒有涉及到其它的HTTP方法。