本页内容
基本下载链接 | |
适用于所有文件类型的强制下载 | |
将大文件分为小块下载 | |
更有效的解决方案 | |
恢复失败的下载 |
提要栏
您的用户极有可能需要从贵组织的网站下载文件。既然提供下载和提供链接一样容易,您当然不需要去阅读有关此过程的文章,对吧?但随着 Web 领域的巨大进步,我们有很多理由可以相信,这个过程不一定像我们想像的那么容易。也许您希望将文件作为一个文件下载,而不是作为内容在浏览器中显示。也许您还不知道这些文件的路径(或者它们根本就不在磁盘上),因此那些简单的 HTML 链接不可能实现下载。也许您会担心用户在下载大文件期间会失去连接。
在本文中,我将介绍一些解决这些问题的方法,这样您的用户就可以拥有快速、无错的下载体验了。在整篇文章中,我将讨论动态生成的链接,说明如何绕过默认文件行为,并借用 HTTP 1.1 功能来例示可恢复的由 ASP.NET 驱动的下载。
基本下载链接
让我们首先来解决缺失链接的问题。如果您不知道某文件的路径将是什么,您只需稍后从数据库中拉出链接列表即可。您甚至可以通过在运行时于给定的目录中枚举文件来动态建立链接列表。这里我将探讨第二种方法。
假设我在 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 OKConnection: closeDate: Mon, 22 May 2006 11:09:13 GMTAccept-Ranges: bytesLast-Modified: Mon, 22 May 2006 08:09:13 GMTETag: "58afcc3dae87d52:3173"Cache-Control: privateContent-Type: application/x-zip-compressedContent-Length: 39551221
由于使用了 ETag 和 Accept-Headers,浏览器知道了 Web 服务器将支持可恢复下载。
如果下载失败,则当该文件再一次被请求时,Internet Explorer 将发送 ETag、文件名和指明在中断前已成功下载的文件字节数的值范围,以便 Web 服务器 (IIS) 可以尝试恢复下载。第二次请求可能如下所示。
GET http://192.168.0.1/download.zip HTTP/1.0Range: bytes=933714-Unless-Modified-Since: Sun, 26 Sep 2004 15:52:45 GMTIf-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 ContentContent-Range: bytes 933714-39551221/39551222Accept-Ranges: bytesLast-Modified: Sun, 26 Sep 2004 15:52:45 GMTETag: "58afcc3dae87d52:3173"Cache-Control: privateContent-Type: application/x-zip-compressedContent-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 为网站的最终用户提供了多种选择来打造最适合的下载体验。
Joe Stagner 于 2001 年加入 Microsoft 担任技术专家,目前是“工具和平台产品”小组中“开发人员社区”的项目经理。30 年的开发经验为他赢得了跨多种技术平台创建商业软件应用程序的机会。