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;
        }
    }

你可能感兴趣的:(Godaddy服务器上关于ASP.NET网站建设一些经验 - 断点续传下载 (二))