传奇般的巴别塔

在代码中游走,在夜幕里吟唱,徘徊在城市的边缘由着理性和感性纠缠、厮杀

导航

在ASP.NET程序中集成更好的下载体验

Posted on 2008-06-16 11:03  evanescencex  阅读(6347)  评论(11编辑  收藏  举报

     最近在写一个Web版本的文件管理器,正好又有朋友问起web页面上面可以让图片也变成下载模式的那种链接方式在ASP.NET里面怎么实现,我给他写了一个大概,觉得也应该当作笔记贴出来,帮他写的时候,突然发现很多问题自己也不是明白,所以逐一查找了一番,贴出来和大家分享!

     应用场景,很多时候都有盗链等各种各样的原因,用ASP.NET呢,最基本的一个问题,我的所有数据文件都保存在App_Data,这个文件夹和配置文件Web.Config一样,直接是无法访问其中内容的,所以如果里面上传了文件,无论是图片,还是压缩包,想下载就要通过某个点Response.WriteFile出去,不过在讨论的时候又发现了一些新的内容,如下:

首先,是下载的基础,Http Header 的做两个设置:

  1. Content-Type : (这个~很无语的东西,每次都记不住,现查!Wiki)
              application/octet-stream           万金油型,什么文件都适合!
              application/x-zip-compressed      专门针对Zip文件的,但是在某些情况下有奇效,这个后面讲
  2. Content-Disposition : 此属性设置内容输出的方式和属性,不大会使,常用就两种操作方式,一个是inline,另一个就是attachment;在输出类型之后可以跟着一些参数,在操作下载的时候如果我们不希望我们输出的文件编程abc.aspx的名字,就要设置filename的参数项,其他的参数项有:creation-date,modification-date,read-date,size。这些内容在后面讲高级的下载输出时会用得到哦。

      只要对上述的两个设置项进行设置以后就可以正常输出问题了,还需要服务器段的代码,以下我列出了三个实现,第一个是最简单的原型,然后再它的基础上有一个备选,最后一个是一个来自MSDN的高级解决方案,没研究明白到底是否该用~

最简单的实现:
      新建一个WebForm页面,然后在Page_load里面添加内容:     

protected void Page_Load(object sender, EventArgs e)
{
if (null != Request.QueryString["key"])
{
string path = Request.PhysicalApplicationPath + @"App_Data\" 
+ Request.QueryString["key"].Replace('/', Path.DirectorySeparatorChar); if (File.Exists(path)) { FileInfo fi = new FileInfo(path); Response.Clear(); Response.ContentType = "application/octet-stream"; // 注意!这个地方一定要用AppendHeader。MSDN上很多地方指导使用 // Response.Headers.Add 或 Response.AddHeader // 但是在MSDN中明确写出,这些都是为了兼容ASP,在.NET 3.5要求使用下面这种方式。 // 如果使用了上述两种方式可能会产生“此操作要求使用 IIS 集成管线模式。 ”的异常。 Response.AppendHeader("Content-Disposition", string.Format
("attachment;filename=\"{0}\"",HttpUtility
.UrlEncode(fi.Name, System.Text.Encoding.UTF8))); Response.AppendHeader("Content-Length", fi.Length.ToString()); Response.WriteFile(fi.FullName); } else Response.Write(string.Format("access is error.{0} is no exist.", path)); } else { Response.Write("i need key!"); } }
代码如上所示很简单,但是注释部分,我搞了小半个小时~感觉最近手艺有点潮。
上面对代码访问http://localhost:60534/WebForm1.aspx?key=[(目录)/](文件名)
就可以访问的到了,这里面的实例都是通过Asp.NET WebForm来完成的,我在后面会附一个由IHttpHandler实现的代码实例,这样结合URL Rewriter可以做出来很好的访问方式。

升级版本:

       Response.WriteFile使用起来很方便,但是当网站为浏览者提供大块头文件的下载服务时就会发现WriterFile简直就是恶梦,它会非常占用资源(以下是本人猜测,如果有不对的地方,请指正!) ,当你的快餐店来了一个胃口很大的客人,要了一百包薯条准备整个上午都在店里面看表格,恰好碰上一个死心眼的大厨,他总觉得自己应该在最快的时候内把所有的东西都做好,然后把它们完整的呈现在顾客的面前,结果呢!可想而知,那个顾客因为饥饿而晕倒在了自己的座位上!这就是我们今天要讲的内容,你的Server也许只有2G的内容,当然IIS的限制也正好在这个位置,但是如果同时有人发起了两个以上大文件的请求的时候,你的内存就会忙于装填那些将要发包出去的字节码,而这个动作可能会和其他千万个Action一起哄抢本来就不多的资源,有没有什么办法可以解决呢?我们来看看下面的方案(声明这个方案也不是我想出来的,出自MSDN Magazine,就是忘记哪一期了!):

                    int chunkSize = 1000;
byte[] buffer = new byte[chunkSize];
using (FileStream fs = fi.Open(FileMode.Open))
{
while (fs.Position >= 0 && Response.IsClientConnected)
{
int tmp = fs.Read(buffer, 0, chunkSize);
Response.OutputStream.Write(buffer, 0, tmp);
Response.Flush();
}
}

代码很简单,就是用上面的代码替换掉Response.WriteFile方法,这样在内存中建立一个buffer的缓冲区(如果我的想法没有错的话,原理先放在一边,事实上这些代码确实起作用了!),然后去轮循字节信息,这样处理较第一种方式快很多输出1G的内容很快,但是没有进行具体测试,不知道会不会给CPU或是其他方面带来新的负载。而Response.IsClientConnected可以判断连接状态是否激活,就好比上面那个顾客只吃了50包,就撑倒了,那我们就需要把手头的事情放下,帮忙打个120。
下面的压缩包是一个IHttpHandler实现的App_Data目录内容浏览和文件下载的示例,还有很多缺陷,比如说没有针对权限作出甄别监测,当然需要只是简单的管理权限,那就在<location>节点里面配置一下也好,Web.config的配置代码如下:

<httpHandlers>
    <add verb="GET" path="adbrowser.o" type="I.HttpHandler.AppDataBrowser,I.Controls" />
<add verb="*" path="download.o" type="I.HttpHandler.Download,I.Controls" />
</httpHandlers>

点击下载I.Controls.rar

备选版本:

所谓高级版,其实又算是一个微软的私有定义了,使用TransmitFile可以分段输出,大家都知道IE支持断点续传的,但是有时候当我们下载一半中断之后,我们再去请求的时候,突然IE的普通下载就变成续传型了,很神奇,能碰到机会和出去逛街捡了一万块钱的几率相当。传说中在IE请求的时候会传入时会附加一个Header,叫做Range用来框定目前下载文件的长度,和已下载的字节位置,然后结合creation-date,modification-date去判断是否可以续连上一次下载的内容接着下载,但是这个又有一个新的麻烦的点,首先我用Reflector拆开TransmitFile看了一下,与WriteFile一样的实现机制,依然会有资源占有的问题,然后就是只针对IE,用Fiddler抓了一上午都没有发现FF或Opera之流,但是没有对迅雷或者是快车进行监测,不知道这个会不会再下载工具中有什么实际用途,最好的解决方案就是把range信息自己摘出来解析,然后自己去附加日期信息,用第二种方式缓冲输出。设想是这样的,因为我对HTTP请求不是很了解,请了解的站出来,指点一下。这个版本没有写,就是思考而已,有朋友感兴趣可以实现以下,记得告诉我什么感觉~