1. 程式人生 > >下載檔案的基本原理

下載檔案的基本原理

    基本下載連結

讓我們首先來解決缺失連結的問題。如果您不知道某檔案的路徑將是什麼,您只需稍後從資料庫中拉出連結列表即可。您甚至可以通過在執行時於給定的目錄中列舉檔案來動態建立連結列表。這裡我將探討第二種方法。

假設我在 Visual Basic® 2005 中建立一個 DataGrid,並在其中填入指向下載目錄中所有檔案的連結,如圖1 所示。要完成此操作,可先在頁面內使用 Server.MapPath 來檢索下載目錄的完整路徑(此例中為 ./downloadfiles/),再使用 DirectoryInfo.GetFiles 檢索該目錄中所有檔案的列表,然後從 FileInfo 物件的最終所得陣列建立一個 DataTable(其中含有代表每個相關屬性的列)。可將 DataTable 繫結到頁面上的 DataGrid,通過該 DataTable 可生成帶有以下 HyperLinkColumn 定義的連結:

<asp:HyperLinkColumn DataNavigateUrlField="Name"
DataNavigateUrlFormatString="downloadfiles/{0}"
DataTextField="Name"
HeaderText="File Name:"
SortExpression="Name" />

如果您單擊這些連結,就會發現瀏覽器對每個檔案型別的處理方式都不同,具體取決於註冊了哪些助手應用程式來開啟每個檔案型別。預設情況下,如果您單擊 .asp 頁面、.html 頁面、.jpg、.gif 或 .txt,它會在瀏覽器其本身中開啟,並且不出現“另存為”對話方塊。這是因為這些檔案的副檔名都屬於已知的 MIME 型別。因此,要麼瀏覽器本身知道如何呈現檔案,要麼作業系統具有一個將被瀏覽器使用的助手應用程式。Webcasts(.wmv、.avi 等等)、PodCasts(.mp3 或 .wma)、PowerPoint® 檔案以及所有的 Microsoft® Office 文件都屬於已知的 MIME 型別,如果您不想在預設情況下聯機開啟這些檔案,就產生了一個難題。

.

圖 1 DataGrid 中簡單的 HTML 連結

此外,如果您允許以此方式下載,則只有一個非常普通的訪問控制機制可供您使用。您可以逐個目錄地控制下載訪問,但是逐一控制對各個檔案或檔案型別的訪問需要詳盡複雜的訪問控制,這對於 Web 主管和系統管理員而言是一個非常麻煩的過程。幸運的是,ASP.NET 和 .NET Framework 提供了大量的解決方案。其中包括:

使用 Response.WriteFile 方法

使用 Response.BinaryWrite 方法流式傳送檔案

使用 ASP.NET 2.0 中的 Response.TransferFile 方法

使用 ISAPI 篩選器

寫入到自定義瀏覽器控制元件

返回頁首返回頁首

適用於所有檔案型別的強制下載

在剛才所列的解決方案中最簡單易用的就是 Response.WriteFile 方法。其基本語法非常簡單;這個完整的 ASPX 頁面將查詢被指定為查詢字串引數的檔案路徑,並將該檔案一直伺服到客戶端:

<%@ Page language="VB" AutoEventWireup="false" %>
<html>
<body>
<%
If Request.QueryString("FileName") Then
Response.Clear()
Response.WriteFile(Request.QueryString("FileName"))
Response.End()
End If
%>
</body>
</html>

當在 IIS 輔助程序中執行的程式碼(IIS 5.0 上的 aspnet_wp.exe 或 IIS 6.0 上的 w3wp.exe)呼叫 Response.Write 時,ASP.NET 輔助程序開始向 IIS 程序(inetinfo.exe 或 dllhost.exe)傳送資料。在資料從輔助程序傳送到 IIS 程序的過程中,要在記憶體中進行緩衝處理。這在許多情況下不會產生什麼問題。但對於非常大的檔案,這卻算不上一個很好的解決方案。

從有利方面看,由於傳送檔案的 HTTP 響應是在 ASP.NET 程式碼中建立的,因此您對所有的 ASP.NET 身份驗證和授權機制都擁有完全訪問許可權,從而就可以根據身份驗證狀態、執行時存在的 Identity 和 Principal 物件或者其他任何您認為適合的機制來做出決策。

這樣,您就可以整合現有的安全機制(例如內建的 ASP.NET 使用者和組機制)、Microsoft 伺服器載入項(例如授權管理器和定義的角色組)、Active Directory® 應用程式模式 (ADAM) 乃至 Active Directory,以提供對下載許可權的精確控制。

從應用程式程式碼內部啟動下載還可以讓您替換對已知 MIME 型別的預設行為。要完成此操作,您需要更改所顯示的連結。以下程式碼構造了一個將回發到 ASPX 頁面的超連結:

<!-- in the DataGrid definition in FileFetch.aspx -- >
<asp:HyperLinkColumn DataNavigateUrlField="Name"
DataNavigateUrlFormatString="FileFetch.aspx?FileName={0}"
DataTextField="Name"
HeaderText="File Name:"
SortExpression="Name" />

接下來,當頁面受到請求時,您需要檢查查詢字串以確定該請求是否是一個包含要傳送到客戶端瀏覽器的檔名引數的回發(參見圖2)。現在,由於有了 Content-Disposition 響應標頭,當您單擊網格中的某個連結時,無論檔案是否為 MIME 型別,都會出現“儲存”對話方塊(參見圖3)。同時還應注意,我已根據呼叫 IsSafeFileName 方法的結果限定了可對哪些檔案進行下載。有關這樣操作的原因以及此方法可實現什麼結果的詳細資訊,請參閱“無意的檔案訪問”提要欄。

.

圖 3 強制顯示檔案下載對話方塊

在使用此方法時要考慮的一個重要度量標準就是檔案下載的大小。您必須限制檔案的大小,否則就會將您的站點暴露給“拒絕服務”攻擊。如果試圖下載大小超出資源允許範圍的檔案,將會產生表明該頁無法顯示的執行時錯誤,或顯示如下所示的錯誤訊息:

無法訪問伺服器應用程式
您目前無法訪問此 Web 伺服器中的 Web 應用程式。請在 Web 瀏覽器中點選“重新整理”按鈕以重新提交請求。
管理員通知:可在 Web 伺服器的系統事件日誌中找到造成此特定請求失敗的詳細資訊。請檢視此日誌條目以找到造成此錯誤的原因。

可下載的檔案大小上限是伺服器硬體配置和執行時狀態的一個要素。要應對此問題,請參閱知識庫文章“FIX:下載大檔案導致大記憶體丟失並導致 Aspnet_wp.exe 程序以迴圈”,網址為 support.microsoft.com/kb/823409

在下載視訊之類的大檔案時,此方法可能會出現一些症狀,尤其是在執行 Windows 2000 和 IIS 5.0 的 Web 伺服器(或以相容模式執行 IIS 6.0 的 Windows Server™ 2003)上更是如此。在配置了最低記憶體的 Web 伺服器上,此問題會更加嚴重,因為必須先將檔案載入到伺服器記憶體中才能將其下載到客戶端。

我曾對一個執行 IIS 5.0 並且 RAM 為 2GB 的伺服器進行過測試,實踐證明,當檔案大小接近 200MB 時,下載就會失敗。在生產環境中,同時執行的使用者下載越多,就有越多的伺服器記憶體限制導致使用者下載失敗。對於此問題的解決方案需要使用幾行更簡明直接的程式碼。

將大檔案分為小塊下載

先前程式碼示例所存在的檔案大小問題源於對 Response.WriteFile 的單一呼叫,該呼叫將在記憶體中緩衝整個原始檔。處理大檔案的更有效方法就是將檔案分成小的、易管理的檔案塊來讀取併發送到客戶端,如圖4 中的示例所示。此版本的 Page_Load 事件處理程式每次使用 while 迴圈讀取檔案中的 10,000 個位元組,然後將這些檔案塊傳送給瀏覽器。因此,在執行時檔案不會有任何重要部分保留在記憶體中。檔案塊大小目前被設為一個常量,但可通過程式設計方式對其修改,甚至也可以將其移動到配置檔案中,以便根據伺服器限制和效能要求對其進行更改。我使用一個大小高達 1.6GB 的檔案測試了此程式碼,結果是下載速度非常塊,並且不會耗用大量的伺服器記憶體。

IIS 本身並不支援檔案大小超出 2GB 的檔案下載。如果您要下載較大的檔案,則需要使用 FTP、第三方控制元件、Microsoft 後臺智慧傳送服務 (BITS) 或一個自定義解決方案(例如,通過套接字將資料流式傳送到託管瀏覽器的自定義控制元件)。

更有效的解決方案

檔案下載要求的共同性以及通常檔案大小都在不斷增加的這個事實促使 ASP.NET 開發團隊在 ASP.NET 中添加了一個特定方法,以便在下載檔案時,不必在記憶體中對檔案進行緩衝處理就可以將其傳送到瀏覽器。該方法就是 Response.TransmitFile,在 ASP.NET 2.0 中提供。

TransmitFile 的用法與 WriteFile 非常相似,但 TransmitFile 通常會產生更好的效能特徵。TransmitFile 還可以與其他功能性相媲美。看一下圖5 中的程式碼,此程式碼使用新增的 TransmitFile 的一些附加功能來避免上述的記憶體使用問題。

我只需額外新增幾行程式碼就可以增加一些安全性和容錯性。首先,我使用被請求檔案的副檔名添加了一些安全性和邏輯限制來確定 MIME 型別,並通過設定 Response 物件的“ContentType”屬性來指定 HTTP 標頭中被請求的 MIME 型別:

Response.ContentType = "application/x-zip-compressed"

這使我可以將下載目標僅限制為某些內容型別,並可將不同的副檔名對映到一種單一內容型別。還應注意一下新增 Content-Disposition 標頭的語句。此語句使我可以指定要下載的檔名,此檔名不同於伺服器硬碟上的原始檔名。

在此程式碼中,我通過在原始檔名中附加一個字首來建立一個新檔名。儘管此處的字首是靜態不變的,但我可以動態建立一個字首,以便下載的檔名絕對不會與使用者硬碟上已有的檔名相沖突。

但是,如果在獲取大檔案的中途出現下載失敗怎麼辦?儘管該程式碼迄今為止已從簡單的下載連結跨出了一大步,但我仍然無法妥善處理失敗的下載並在中斷後繼續下載已將部分內容從伺服器移至客戶端的檔案。我至今所檢驗過的所有解決方案都需要使用者在下載失敗時從頭開始重新下載。

恢復失敗的下載

要解決恢復失敗下載這個問題,讓我們回顧一下將檔案手動拆分成塊進行傳送的方法。儘管不像使用 TransmitFile 方法的程式碼那樣簡單,但手動編寫分塊讀取和傳送檔案的程式碼具備一個優點。在任何給定時刻,執行時狀態都包含了已傳送到客戶端的位元組數,通過從整個檔案大小中減去該位元組數,就會得到為使此檔案完整還需要傳送的剩餘位元組數。

如果回顧一下該程式碼,您就會發現讀取/傳送迴圈會在某迴圈構成 Response.IsClientConnected 結果的條件時進行檢驗。該測試將確保在與客戶端斷開連線時將傳送過程暫停。在測試結果為“假”(啟動檔案下載的 Web 瀏覽器已斷開連線)的第一次迴圈迭代中,伺服器將停止傳送資料,並且可記錄要完成檔案所需傳送的剩餘位元組數。此外,如果使用者試圖完成失敗的下載,可將客戶端收到的部分檔案進行儲存。

可恢復下載解決方案的剩餘部分是通過 HTTP 1.1 協議中的一些鮮為人知的功能實現的。通常,HTTP 的無狀態性質是 Web 開發人員的剋星,但在本例中,HTTP 規範卻提供了很大幫助。具體來說,有兩個 HTTP 1.1 標頭元素與我們要完成的這項任務相關。Accept-Ranges 和 Etag。

Accept-Ranges 標頭元素可以非常簡單地向客戶端(本例中指 Web 瀏覽器)指明,此程序支援可恢復下載。實體標記或 Etag 元素將為該會話指定一個唯一識別符號。因此,可由 ASP.NET 應用程式傳送到瀏覽器以開始一個可恢復下載的 HTTP 標頭可能如下所示:

HTTP/1.1 200 OK
Connection: close
Date: Mon, 22 May 2006 11:09:13 GMT
Accept-Ranges: bytes
Last-Modified: Mon, 22 May 2006 08:09:13 GMT
ETag: "58afcc3dae87d52:3173"
Cache-Control: private
Content-Type: application/x-zip-compressed
Content-Length: 39551221

由於使用了 ETag 和 Accept-Headers,瀏覽器知道了 Web 伺服器將支援可恢復下載。

如果下載失敗,則當該檔案再一次被請求時,Internet Explorer 將傳送 ETag、檔名和指明在中斷前已成功下載的檔案位元組數的值範圍,以便 Web 伺服器 (IIS) 可以嘗試恢復下載。第二次請求可能如下所示。

GET http://192.168.0.1/download.zip HTTP/1.0
Range: bytes=933714-
Unless-Modified-Since: Sun, 26 Sep 2004 15:52:45 GMT
If-Range: "58afcc3dae87d52:3173"

請注意,If-Range 元素包含伺服器可用於標識要重新發送的檔案的原始 ETag 值。您還會看到 Unless-Modified-Since 元素包含了最初下載的開始日期和時間。伺服器將利用此資訊來確定自最初下載開始後該檔案是否已被修改過。如果已被修改,則伺服器將從頭開始重新下載。

Range 元素也包含在標頭中,它會向伺服器指明還需要傳送多少位元組才能完成檔案,伺服器可以利用此資訊來確定應從已部分下載檔案的何處開始繼續下載。

不同瀏覽器使用這些標頭的方式略有不同。客戶端可能傳送的用於唯一標識該檔案的其他 HTTP 標頭包括:If-Match、If-Unmodified-Since 和 Unless-Modified-Since。請注意,HTTP 1.1 在某個客戶端應該需要支援哪些標頭方面並沒有特定要求。因此,就有可能出現這樣的情況,某些 Web 瀏覽器不支援這些 HTTP 標頭中的任一個,而其他瀏覽器可能使用不同於 Internet Explorer® 要求的標頭的另一個標頭。

預設情況下,IIS 將包含一個如下所示的標頭集:

HTTP/1.1 206 Partial Content
Content-Range: bytes 933714-39551221/39551222
Accept-Ranges: bytes
Last-Modified: Sun, 26 Sep 2004 15:52:45 GMT
ETag: "58afcc3dae87d52:3173"
Cache-Control: private
Content-Type: application/x-zip-compressed
Content-Length: 2021408

此標頭集包含的響應程式碼不同於原始請求的響應程式碼。原始響應包含的程式碼為 200,而該請求使用的響應程式碼為 206(即“恢復下載”),用於向客戶端指明,後面的資料不是一個完整檔案,而只是繼續先前啟動的下載,該下載的檔名由 ETag 標識。

儘管某些 Web 瀏覽器依賴的是檔名其本身,但 Internet Explorer 非常明確地要求 ETag 標頭。如果 ETag 標頭在最初下載響應或下載恢復中不存在,則 Internet Explorer 不會嘗試恢復下載,而只是開始一個新下載。

為使 ASP.NET 下載應用程式實現可恢復下載功能,您需要能夠攔截瀏覽器發出的請求(進行下載恢復),並使用請求中的 HTTP 標頭在 ASP.NET 程式碼中明確表達相應的響應。要完成此操作,您應在正常處理序列中早一些捕獲該請求。

令人欣慰的是,.NET Framework 可以助我們一臂之力。這是 .NET 基本設計前提的一個極好例子,為開發人員每天都需要執行的大部分標準探測工作提供了一個被良好分解的功能物件庫。

在這種情況下,您可以利用 .NET Framework 中 System.Web 名稱空間所提供的 IHttpHandler 介面來構建您自己的自定義 HTTP 處理程式。通過建立您自己的實現 IHttpHandler 的類,您將能夠攔截對特定檔案型別的 Web 請求並用自己的程式碼響應這些請求,而不是僅讓 IIS 以其預設行為做出響應。

本文中的下載程式碼包含了支援可恢復下載的 HTTP 處理程式的工作實現。儘管對於此功能存在多個程式碼,並且其實現需要對 HTTP 機制有一定了解,但 .NET Framework 使此實現變得相對簡單。此解決方案提供了下載大檔案的能力,並且在下載啟動後可以繼續進行瀏覽。然而,還有某些基礎結構注意事項不在您的控制範圍之內。

例如,許多公司和 Internet 服務提供商會維護他們自己的快取記憶體機制。出現故障或配置錯誤的 Web 快取記憶體伺服器會因檔案損壞或會話過早終止而導致大檔案下載失敗,尤其在您的檔案大小超過 255MB 時更是如此。

如果您需要下載超過 255MB 的檔案或使用其他自定義功能,您可能會考慮使用自定義的或第三方下載管理器。例如,您可能會構建一個自定義瀏覽器控制元件或瀏覽器助手功能來管理下載,將它們提交給 BITS,或甚至用自定義程式碼將檔案請求提交給 FTP 客戶端。擺在眼前的選擇數不勝數,應根據您的特定需要來量身定製。

從通過兩行程式碼實現的大檔案下載到具有自定義安全性的可分段的可恢復下載,.NET Framework 和 ASP.NET 為網站的終端使用者提供了多種選擇來打造最適合的下載體驗。

宣告:以上內容轉自微軟中國MSDN