日期:2014-03-24  浏览次数:20462 次

在Web应用程序中处理大文件下载的问题一直出了名的困难,因此对于大多数站点来说,如果用户的下载被中断了,它们只能说悲哀降临到用户的身上了。但是我们现在不必这样了,因为你可以使自己的ASP.NET应用程序有能力支持可恢复(继续)的大文件下载。使用本文提供的方法的时候,你可以跟踪下载的过程,这样你就可以处理动态建立的文件--而且要达到这个目标根本不需要旧式的ISAPI动态链接库和非受控的(unmanaged)C++代码。

  为客户端提供从互联网上下载文件的服务最容易了,对吗?仅仅只需要把可下载的文件复制到你的Web应用程序目录中,发布链接并让IIS完成所有相关的工作。但是,文件服务不应该比脖子上的疼痛还要多(还要麻烦),你不希望整个世界都能访问自己的数据,你不希望服务器被数百个静态文件塞满了,你甚至于希望下载临时文件--只有当客户端开始下载后的空闲时间才建立这些文件。

  不幸的是,使用IIS对下载请求的默认的响应是不可能达到这些效果的。因此在一般情况下,为了获得对下载过程的控制权,开发者需要链接到一个定制的.aspx页面,在这个页面中它们检查用户凭证(credential)、建立可以下载的文件并使用下面的代码把该文件推送给客户端:

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应用程序增加可靠的下载管理能力了。

  获取下载过程的控制权的方法是