天天看点

Godaddy服务器上关于ASP.NET网站建设一些经验 - 断点续传下载 (二)

续上一篇 (http://blog.csdn.net/querw/archive/2009/08/24/4477182.aspx) 谈谈在APS.NET中如何控制文件下载.

设计目的和要求

假设这么一个应用场景:

一个主机,上面存有许多文件资料,有各种文件格式.(PDF, DOC, EXE ... 等等).

该主机上运行一个ASP.NET网站, 用户注册,并付费之后允许他/她下载资料.

文件是放在IIS服务器上的, 如果用户知道具体路径那么他是可以随时下载的. (在没有或者不能设置访问权限的情况下.)

如果直接把下载路径发送给付费用户,肯定是行不通的,会被散播出去. 所以不能把让客户端得知具体路径,文件内容由 ASP.NET 服务器页面读取后发送给客户端.

我要做的就是: 编写一个ASP.NET 页面服务器代码, 读取指定文件,并发送给客户 .

总体思路

.net 里, 有2个函数可以用来发送文件 Response.WriteFile 和 Response.TransmiteFile

它们的主要区别是: WriteFile 是先把文件内容读取到服务器缓冲,然后再发送到客户端. 所以对于大文件,会造成服务器很大的压力.

一般用来处理小文件,比如,发送给 excel 报表之类的. TransmiteFile 不缓冲数据, 直接抛给客户端, 所以可以用来发大文件.

( 我采用 TransmiteFile 来实现.)

具体实现

1. 给客户一个链接,形如 http://xxxx/downloads.aspx?Key=ABCD123456

2. 在downloads.aspx的服务器代码中, 通过Key的值,查询数据库,得到服务器上的真实文件路径. 这个时候,控制权在 downloads.aspx, 所以可以编写复杂的控制功能, 比如看看用户有没有登录,有没有付费之类的,从而避免外部盗链.

3. 得到文件路径后,调用 Response.TransmiteFile 发送文件给客户端.

4. 因为给客户的链接里没有任何文件名的信息, 所以要在HTTP响应头里添加一句,告诉客户端文件名:  Response.AddHeader("Content-Disposition", "attachment; filename=/"" + 你的文件名 + "/""); (如果要支持中文,要考虑编码的问题, 我这里不说,不是我们的主题.)

5. 如果是一个大文件, 比如1G, 不支持断点续传,是没有意义的. 那么如何实现呢?

(1) 要让客户端知道我们的服务器支持断点续传, 要在HTTP响应头中包含 Accept-Ranges: bytes 和 ETag: "XXXX".

 ETag 是一个文件的标识, 供客户端判断它请求的是同一个文件, ETag 的内容在HTTP规范里并没有具体要求,只要保证在同一个服务器上,同一个文件有相同的ETag 就行了, 一般就根据文件名和最后修改时间生成一个字符串就可以了.

代码示例:

Response.AddHeader("Accept-Ranges", "bytes");  // 断点续传控制.

Response.AddHeader("ETag", "/"" + strETag + "/""); // 允许断点续传

(2) 要处理客户端请求中的 "Range" 字段. 一般格式是这样: Range: bytes=1234- 或者 Range: bytes=1234-12345

分别表示从地1235个字节开始下载和下载第1235到第12346个字节之间的数据.

服务器首先要添加 Content-Range 响应头, 然后用 TransmiteFile 发送指定的数据.

代码示例:

Response.StatusCode = 206;

Response.AddHeader("Content-Length", (lTo - lFrom + 1).ToString());

Response.AddHeader("Content-Range", string.Format("bytes {0}-{1}/{2}", lFrom, lTo, fi.Length));  // 参数0 和 参数1 是位置. 参数2是文件长度

Response.TransmitFile(strFilePath, lFrom, lTo - lFrom + 1);

( 其中, lFrom 和 lTo 是根据客户端请求中的 Range 字段得到的.)

总结

这个功能说起来一点点文件就写完了,做的时候做了很久. 中间还碰到一个问题: 我用 VS2008 开发, 没有在机器上装IIS. 结果调试的时候,发现Accept-Ranges 和 Content-Range两个响应头始终加不进去.

后来把代码上传到一个真实服务器才测试通过, 看来 VS2008 自带的.net服务器设置有写古怪.

说一下优缺点:

1. 可以随心所欲的控制下载.

2. 可以绕过服务器文件类型下载的限制, 比如我的服务器不允许下载 ISO 和 NRG 文件扩展名的文件, 如果直接输入RUL会提示404, 但是用上述的方法可以下载.

3.用这种办法的话,下载是在.net的一个线程里做的,如果用户量大的话,需要维护多个响应, 我不知道会不会对服务器性能有什么影响.

目前我还不了解这种方法和直接输入URL下载对IIS服务器来说有没有什么不同.

不过,对于IIS来说, 如果用户直接输入文件的URL通过下载工具来多线程下载, 也同样会有这个问题, 要维护多个响应.

如果您有什么见解,请赐教, 谢谢. [email protected]

附注:

1. TransmitFile(String) ( 函数是 .net 2.0 才加上去的.

2. TransmitFile(String, Int64, Int64) 带发送位置参数的重载是 .net 2.0 sp1 以后才支持的. 所以要用本文所说的方法实现断点续传, 至少要支持.net 2.0 sp1

3. 我没有检测请求头中的 If-Range 和 Unless-Modified-Since, 如果有需要,在得到文件名之后就可以校验一下, 分别对应 ETag 和 Last-Modified.

4. 本文才刚发到CSDN没两天就被 www.diybl.com 转载, 居然注明作者 "佚名",  我不反对转载本文 , 本来就是要和大家分享, 但是我要求保留我的署名 , 不过分吧? (也许不是我第一个用这种方法并公布出来, 但是文章确是我原创,并且编写代码做了测试.)

=============================传说中的分割线======================================

上面说的可能比较简略, 我贴一段代码,附带注释,不求所有人都能看懂, 但是如果你正在做类似的工作,相信能有所帮助

             // 1. 获取服务器上的文件路径 // 这里,如果文件路径有问题, 无法映射则会抛出异常, strURL 是根据 Key从数据库中查询到的真实文件路径

                  string strFilePath = Server.MapPath("~" + strURL);

                  // 2. 获取文件名

                  string strFileName = System.IO.Path.GetFileName(strFilePath);

                  // 3. 确认文件是否存在

                  FileInfo fi = new FileInfo(strFilePath);

                  if (!fi.Exists)

                  {

                      // 退出点,文件不存在

                  }

                  // 4. 抛给客户端

                  strFileName.Replace(" ", "%20"); // 处理文件名含空格的情况

                  string strETag = strFileName.ToUpper() + ":" + fi.Length.ToString();  // 我的Etag 是用文件名和字节数构成,马马虎虎凑合用.

                  string strLastTime = fi.LastWriteTimeUtc.ToString("r");

                  Response.Clear();  // 先把响应流清空

                  Response.ContentType = "application/octet-stream";  // 指定文件类型,使客户端总是弹出保存文件的框框.

                  Response.AddHeader("Content-Disposition", "attachment; filename=/"" + strFileName + "/"");

                  Response.AddHeader("Accept-Ranges", "bytes");  // 断点续传控制.

                  Response.AddHeader("ETag", "/"" + strETag + "/""); // 允许断点续传

                  Response.AddHeader("Last-Modified", strLastTime);//把最后修改日期写入响应

                  // 获取客户端请求的范围, 并且要校验这个范围的有效性

                  long lFrom = 0;

                  long lTo = 0;

                  bool bParts = false;

                  string strRange = Request.Headers["Range"];

                  if (ParseRange(strRange, out lFrom, out lTo))  /// ParseRange 是我自己写的函数, 从 Range 中读取2个位置.代码在后面.

                  {

                      if (-1 == lFrom && -1 == lTo)

                      {

                          // 不允许2个值都不指定

                      }

                      else

                      {

                          if (lTo == -1) lTo = fi.Length - 1;  // 客户端未指定结束位置,则认为是文件的最后一个字符 Range: bytes=123- 的情况

                          if (lFrom == -1) // Range: bytes=-123 的情况, 请求最后的123个字节

                          {

                              lFrom = fi.Length - lTo;

                              lTo = fi.Length - 1;

                          }

                          if (lFrom < 0 || lFrom >= fi.Length || lFrom > lTo || lTo < 0 || lTo >= fi.Length)

                          {

                              // 以上几种情况下,范围的值能解析出来,但是不合法.

                              // 首先 From 和 To 的下标都应该在文件长度范围内

                              // 其次 From 应该 <= To

                          }

                          else

                          {

                              bParts = true;

                          }

                      }

                  }

                  // 根据用户请求,返回数据段或者整个文件

                  if(bParts)

                  {

                      Response.StatusCode = 206;

                      Response.AddHeader("Content-Length", (lTo - lFrom + 1).ToString());

                      Response.AddHeader("Content-Range", string.Format("bytes {0}-{1}/{2}", lFrom, lTo, fi.Length));  // 参数0 和 参数1 是位置,从0开始. 参数2是文件长度

                      Response.TransmitFile(strFilePath, lFrom, lTo - lFrom + 1);

                  }

                  else

                  {

                      Response.AddHeader("Content-Length", fi.Length.ToString());

                      Response.TransmitFile(strFilePath);

                  }

                  Response.End();

              }

=============================传说中的分割线======================================

protected bool ParseRange(string strRange, out long lFrom, out long lTo)

    {

        lFrom = 0;

        lTo = 0;

        long lTemp = 0;

        if (strRange == null || strRange == "")

        {

            return false; // 字符串为空

        }

        else

        {

            strRange = strRange.Replace(" ", ""); // 去除多余的空格

            string[] range = strRange.Split(new char[] { '=', '-' });

            // 1.分割后,包含3段 第一段是 "Range: bytes", 第二段是起始位置, 第三段是结束位置

            if (range.Length != 3)

            {

                return false; // 格式不正确 只支持 Range: bytes=89294317- 或者 Range: bytes=1234-1235 或者 Range: bytes=-500 3种格式.

            }

            // 2. 解析起始位置

            if (range[1].Length <= 0)

            {

                // 起始位置未指定

                lFrom = -1;

            }

            else

            {

                if (!long.TryParse(range[1], out lTemp))

                {

                    return false; // 起始位置无法解析

                }

                lFrom = lTemp;

            }

            // 3. 解析结束位置

            if (range[2].Length <= 0)

            {

                lTo = -1; // 没有指定结束位置 Range: bytes=1234- 的情况

            }

            else

            {

                if (!long.TryParse(range[2], out lTemp))  // 排除 byte=xxxx- 的情况 TryParse 失败, 会把lTemp 置零

                {

                    return false; // 第三度的内容不为空,但是无法解析

                }

                lTo = lTemp;

            }

            return true;

        }

    }