Godaddy伺服器上關於ASP.NET網站建設一些經驗 – 斷點續傳下載 (二)

來源:互聯網
上載者:User

續上一篇 (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通過下載工具來多線程下載, 也同樣會有這個問題, 要維護多個響應.

如果您有什麼見解,請賜教, 謝謝. querw@sina.com

 

附註:

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

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.