首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

ASP.NET 實作支援斷點續傳 (Range Requests) 的檔案下載處理器

     編輯:關於C語言

前言

大型檔案下載若不支援斷點續傳,使用者一旦網路中斷必須重頭下載,體驗極差。本文實作符合 RFC 7233 的 Range 請求處理器,支援單一區間下載、多執行緒分段下載(FlashGet/IDM/FDM 等)、206 Partial Content 回應、Content-Range/Accept-Ranges 標頭,並修正原始碼中 Content-Range 計算錯誤與串流處理問題。

完整 Generic Handler 實作 (.ashx)

using System;
using System.IO;
using System.Web;

namespace FileDownload
{
    public class ResumableDownloadHandler : IHttpHandler
    {
        public bool IsReusable => false;

        public void ProcessRequest(HttpContext context)
        {
            // 1. 取得檔案路徑 (實務應從資料庫/配置查詢,防止路徑遍歷攻擊)
            string filePath = context.Server.MapPath("~/App_Data/Downloads/largefile.zip");
            if (!File.Exists(filePath))
            {
                context.Response.StatusCode = 404;
                context.Response.Write("檔案不存在");
                return;
            }

            var fileInfo = new FileInfo(filePath);
            long fileSize = fileInfo.Length;
            string fileName = fileInfo.Name;

            // 2. 解析 Range 標頭
            string rangeHeader = context.Request.Headers["Range"];
            long start = 0;
            long end = fileSize - 1;
            bool isPartial = false;

            if (!string.IsNullOrEmpty(rangeHeader) && rangeHeader.StartsWith("bytes="))
            {
                try
                {
                    string range = rangeHeader.Substring("bytes=".Length);
                    string[] parts = range.Split('-');
                    if (parts.Length == 2)
                    {
                        if (long.TryParse(parts[0], out long s)) start = s;
                        if (long.TryParse(parts[1], out long e)) end = e;
                        // 修正:結束位置不可超過檔案大小
                        if (end >= fileSize) end = fileSize - 1;
                        if (start <= end && start < fileSize)
                        {
                            isPartial = true;
                        }
                    }
                }
                catch { /* 格式錯誤視為完整下載 */ }
            }

            long contentLength = end - start + 1;

            // 3. 設定回應標頭
            context.Response.Clear();
            context.Response.Buffer = false; // 關鍵:避免大檔案緩衝至記憶體
            context.Response.ContentType = "application/octet-stream";
            context.Response.AddHeader("Content-Disposition", $"attachment; filename=\"{HttpUtility.UrlEncode(fileName, System.Text.Encoding.UTF8)}\"");
            context.Response.AddHeader("Accept-Ranges", "bytes");
            context.Response.AddHeader("Content-Length", contentLength.ToString());
            context.Response.AddHeader("Last-Modified", fileInfo.LastWriteTimeUtc.ToString("R"));

            if (isPartial)
            {
                context.Response.StatusCode = 206; // Partial Content
                // 修正:Content-Range 格式為 bytes start-end/total
                context.Response.AddHeader("Content-Range", $"bytes {start}-{end}/{fileSize}");
            }
            else
            {
                context.Response.StatusCode = 200;
            }

            // 4. 串流傳輸 (支援大檔案、低記憶體)
            const int bufferSize = 64 * 1024; // 64KB
            byte[] buffer = new byte[bufferSize];

            try
            {
                using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
                {
                    fs.Seek(start, SeekOrigin.Begin);
                    long remaining = contentLength;

                    while (remaining > 0 && context.Response.IsClientConnected)
                    {
                        int read = fs.Read(buffer, 0, (int)Math.Min(bufferSize, remaining));
                        if (read == 0) break;

                        context.Response.OutputStream.Write(buffer, 0, read);
                        remaining -= read;
                    }
                    
                    context.Response.Flush();
                }
            }
            catch (Exception ex) when (ex is HttpException || ex is IOException)
            {
                // 客戶端斷線、取消下載等,記錄日誌即可
            }
            finally
            {
                context.Response.SuppressContent = true;
                context.ApplicationInstance.CompleteRequest(); // 避免 ThreadAbortException
            }
        }
    }
}

Web.config 註冊 (IIS 7+ 整合模式)

<system.webServer>
  <handlers>
    <add name="ResumableDownload" verb="GET,HEAD" path="download.ashx" 
         type="FileDownload.ResumableDownloadHandler" preCondition="integratedMode" />
  </handlers>
  <!-- 關閉 IIS 內建靜態檔案處理器的範圍請求,避免衝突 -->
  <staticContent>
    <clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="365.00:00:00" />
  </staticContent>
</system.webServer>

前端下載連結

<a href="/download.ashx?file=largefile.zip">下載大型檔案 (支援斷點續傳)</a>

關鍵技術細節解析

1. Range 標頭解析規範 (RFC 7233)

  • 格式:Range: bytes=start-end (單一區間)
  • start 可省略:bytes=-500 表示最後 500 位元組
  • end 可省略:bytes=500- 表示第 500 位元組到結尾
  • 多區間:bytes=0-499,500-999 (本範例未支援,回應 416 Range Not Satisfiable 或忽略多區間)

2. 回應標頭必要性

標頭必要性說明
Accept-Ranges: bytes必要告知客戶端支援位元組範圍請求
Content-Length必要本次回應實體長度 (而非檔案總長)
Content-Range206 時必要格式:bytes start-end/total,原始碼錯誤寫成 bytes start-(total-start)/total
ETag / Last-Modified建議支援 If-Range 條件請求,避免檔案異動時續傳錯誤

3. 串流傳輸關鍵設定

  • Response.Buffer = false:關閉 ASP.NET 內建緩衝,直接寫入 OutputStream,否則大檔案會佔用大量伺服器記憶體。
  • FileShare.Read:允許其他進程同時讀取同一檔案。
  • context.Response.IsClientConnected:偵測客戶端斷線,及時停止傳輸釋放資源。
  • CompleteRequest():取代 Response.End() 避免 ThreadAbortException 污染執行緒池。

4. 安全性強化

  • 路徑驗證:嚴禁直接信任 QueryString 路徑。應用 fileId 查詢資料庫取得實體路徑,並驗證 Path.GetFullPath 在允許目錄下。
  • 防止列舉:使用 GUID/雜湊值作為下載 Token,而非檔名。
  • 權限檢查:下載前驗證使用者身分、下載次數限制、頻寬節流。

進階:ASP.NET Core Minimal API 實作

app.MapGet("/download/{fileId:guid}", async (Guid fileId, HttpContext ctx, IFileService svc) =>
{
    var file = await svc.GetFileMetaAsync(fileId);
    if (file == null) return Results.NotFound();

    var rangeHeader = ctx.Request.Headers.Range.ToString();
    var (start, end, isPartial) = ParseRange(rangeHeader, file.Size);
    
    ctx.Response.Headers.AcceptRanges = "bytes";
    ctx.Response.Headers.ContentDisposition = $"attachment; filename*=UTF-8''{Uri.EscapeDataString(file.Name)}";
    ctx.Response.Headers.LastModified = file.LastModified.ToString("R");

    if (isPartial)
    {
        ctx.Response.StatusCode = 206;
        ctx.Response.Headers.ContentRange = $"bytes {start}-{end}/{file.Size}";
    }

    ctx.Response.ContentType = "application/octet-stream";
    
    await ctx.Response.SendFileAsync(file.PhysicalPath, start, end - start + 1, ctx.RequestAborted);
});

SendFileAsync 內部使用 FileStream + MemoryMappedFile 零拷貝傳輸,效能極佳。

測試驗證方法

  1. curl 測試:curl -H "Range: bytes=0-1023" -I https://localhost/download.ashx 應回 206、Content-Range: bytes 0-1023/總大小。
  2. 下載工具:使用 FDM/IDM/FlashGet 下載,觀察是否顯示多執行緒分段、支援暫停/繼續。
  3. 網路中斷模擬:下載至 50% 拔網線,重新連線後點擊「繼續」,檢查檔案完整性 (SHA256)。

常見問題 FAQ

Q: IIS 仍回傳 200 而非 206?
A: IIS 靜態檔案處理器可能搶先處理。確保 <handlers> 註冊順序優先,或在 web.config 移除靜態處理器對特定路徑的處理。
Q: 瀏覽器下載大檔案記憶體暴增?
A: 確認 Response.Buffer = false,且未在程式碼中讀入完整檔案至 byte[]。Core 使用 SendFileAsync 自動零拷貝。
Q: 續傳下載的檔案損壞?
A: 檢查 Content-Range 計算是否正確 (start-end/total),確認 FileStream.Seek 位置、contentLength 計算無誤。下載完成後驗證 SHA256。
Q: 如何支援多區間請求 (bytes=0-99,100-199)?
A: 需回傳 multipart/byteranges,實作複雜度高。建議回應 416 Range Not Satisfiable 或忽略多區間視為完整下載,主流下載工具單區間即可多執行緒分段。

總結

支援斷點續傳的核心在於:正確解析 Range 標頭、回應 206 + Content-Range、關閉緩衝串流傳輸、安全路徑驗證。ASP.NET Core 的 SendFileAsync 大幅簡化實作並提升效能。建議生產環境結合 CDN (支援 Range 回源) 與物件儲存 (S3/MinIO) 卸載頻寬與運維壓力。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved