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

C# 實作 HTTP 斷點續傳下載完整指南

     編輯:關於C語言

前言

斷點續傳是下載工具的核心功能,允許使用者在網路中斷後從中斷處繼續下載,避免重頭開始。本文深入解析 HTTP Range 標頭原理,並使用 `HttpWebRequest` 與 `FileStream` 實作完整的斷點續傳下載器。

HTTP 斷點續傳核心原理

Range 標頭規格

客戶端在請求標頭加入 `Range` 指定下載位元組範圍:

// 從第 1024 位元組開始下載到檔案結尾
Range: bytes=1024-

// 下載第 0~1023 位元組(前 1 KB)
Range: bytes=0-1023

// 多範圍請求(較少用)
Range: bytes=0-500, 1000-1500

伺服器支援續傳時回應 206 Partial Content,並包含 Content-Range 標頭:

HTTP/1.1 206 Partial Content
Content-Range: bytes 1024-2826/2827
Content-Length: 1803

.NET 核心類別說明

類別命名空間關鍵功能
HttpWebRequestSystem.Net發送 HTTP 請求,AddRange() 設定 Range 標頭
HttpWebResponseSystem.Net取得回應串流、狀態碼、Content-Range 標頭
FileStreamSystem.IO隨機存取檔案,Seek() 移動寫入位置
SeekOriginSystem.IO定位基準:Begin/Current/End

完整斷點續傳下載器實作

using System;
using System.IO;
using System.Net;

public class ResumableDownloader
{
    private const int BufferSize = 8192; // 8KB 緩衝區

    public void Download(string url, string filePath)
    {
        long startPos = 0;
        FileStream fs = null;
        Stream responseStream = null;

        try
        {
            // 1. 檢查本地檔案是否存在,決定起始位置
            if (File.Exists(filePath))
            {
                fs = new FileStream(filePath, FileMode.Open, FileAccess.Write);
                startPos = fs.Length;
                fs.Seek(startPos, SeekOrigin.Begin);
                Console.WriteLine($"偵測到部分檔案,從 {startPos} 位元組繼續下載...");
            }
            else
            {
                fs = new FileStream(filePath, FileMode.Create, FileAccess.Write);
                Console.WriteLine("新檔案下載...");
            }

            // 2. 建立 HTTP 請求
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
            request.Method = "GET";
            request.UserAgent = "C# ResumableDownloader/1.0";
            request.Timeout = 30000;
            request.ReadWriteTimeout = 30000;

            // 3. 設定 Range 標頭(關鍵步驟)
            if (startPos > 0)
            {
                request.AddRange(startPos);
            }

            // 4. 取得回應
            using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
            {
                // 檢查伺服器是否支援續傳
                if (startPos > 0 && response.StatusCode != HttpStatusCode.PartialContent)
                {
                    Console.WriteLine("伺服器不支援斷點續傳,重新下載...");
                    fs.SetLength(0); // 清空檔案
                    startPos = 0;
                    fs.Seek(0, SeekOrigin.Begin);
                }

                long totalSize = startPos + response.ContentLength;
                Console.WriteLine($"檔案總大小: {FormatBytes(totalSize)}");

                responseStream = response.GetResponseStream();
                byte[] buffer = new byte[BufferSize];
                int bytesRead;
                long downloaded = startPos;
                DateTime lastReport = DateTime.Now;

                // 5. 迴圈讀取並寫入
                while ((bytesRead = responseStream.Read(buffer, 0, buffer.Length)) > 0)
                {
                    fs.Write(buffer, 0, bytesRead);
                    downloaded += bytesRead;

                    // 進度報告(每秒最多一次)
                    if ((DateTime.Now - lastReport).TotalSeconds >= 1)
                    {
                        double percent = (double)downloaded / totalSize * 100;
                        double speed = bytesRead / 1024.0; // KB/s 近似值
                        Console.Write($"\r進度: {percent:F1}% ({FormatBytes(downloaded)}/{FormatBytes(totalSize)}) 速度: {speed:F0} KB/s");
                        lastReport = DateTime.Now;
                    }
                }
                Console.WriteLine($"\n下載完成: {filePath}");
            }
        }
        catch (WebException ex)
        {
            HandleWebException(ex, filePath, startPos);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"\n下載發生錯誤: {ex.Message}");
        }
        finally
        {
            responseStream?.Close();
            fs?.Close();
        }
    }

    private void HandleWebException(WebException ex, string filePath, long startPos)
    {
        if (ex.Response is HttpWebResponse httpResponse)
        {
            Console.WriteLine($"\nHTTP 錯誤: {(int)httpResponse.StatusCode} {httpResponse.StatusCode}");
            if (httpResponse.StatusCode == HttpStatusCode.RequestedRangeNotSatisfiable)
            {
                Console.WriteLine("範圍請求無效,檔案可能已完整或伺服器檔案已變更");
                File.Delete(filePath); // 刪除損壞檔案
            }
        }
        else
        {
            Console.WriteLine($"\n網路錯誤: {ex.Message}");
        }
    }

    private string FormatBytes(long bytes)
    {
        string[] units = { "B", "KB", "MB", "GB" };
        int i = 0;
        double size = bytes;
        while (size >= 1024 && i < units.Length - 1)
        {
            size /= 1024;
            i++;
        }
        return $"{size:F1} {units[i]}";
    }
}

// 使用範例
class Program
{
    static void Main()
    {
        var downloader = new ResumableDownloader();
        downloader.Download("https://example.com/largefile.zip", @"C:\Downloads\largefile.zip");
        Console.ReadKey();
    }
}

進階功能擴充

1. 多執行緒分段下載(加速)

// 將檔案分成 N 段,每段用一條執行緒下載,最後合併
// 需要伺服器支援 Accept-Ranges: bytes
// 適用於大檔案(>100MB)且頻寬充足的情境

2. 下載進度持久化

// 將 downloaded 位元組數寫入 .progress 檔或資料庫
// 程式重啟時讀取進度繼續,避免記憶體遺失

3. 檔案完整性驗證

// 下載完成後計算 MD5/SHA256 與伺服器提供的雜湊值比對
// 或使用 ETag/Last-Modified 標頭驗證檔案未被竄改

常見問題與解決方案

問題原因解決方案
伺服器回應 200 而非 206不支援 Range 或 Range 無效檢查 Accept-Ranges 標頭,不支援則重新下載
續傳後檔案損壞伺服器檔案已更新(ETag 變更)下載前比對 ETag/Last-Modified,不同則重下
大檔案記憶體不足緩衝區過大或未及時 Flush使用 4-8KB 緩衝區,定期 fs.Flush()
網路中斷拋出例外Read/Write 逾時捕獲 WebException,記錄位置,重試機制

現代化替代方案:HttpClient(.NET Core / .NET 5+)

using System.Net.Http;

public async Task DownloadWithHttpClientAsync(string url, string filePath, CancellationToken ct = default)
{
    using var client = new HttpClient();
    using var request = new HttpRequestMessage(HttpMethod.Get, url);
    
    long startPos = 0;
    FileMode mode = FileMode.Create;
    
    if (File.Exists(filePath))
    {
        var fi = new FileInfo(filePath);
        startPos = fi.Length;
        mode = FileMode.Open;
        request.Headers.Range = new System.Net.Http.Headers.RangeHeaderValue(startPos, null);
    }

    using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ct);
    response.EnsureSuccessStatusCode();

    using var fs = new FileStream(filePath, mode, FileAccess.Write, FileShare.None, 8192, true);
    if (startPos > 0) fs.Seek(startPos, SeekOrigin.Begin);
    
    await response.Content.CopyToAsync(fs, ct);
}

總結

斷點續傳的核心在於 HTTP Range 標頭 與 FileStream 隨機存取 的結合。傳統 `HttpWebRequest` 雖屬舊 API 但相容性佳;新專案建議使用 `HttpClient` 搭配非同步串流複製,程式碼更簡潔且效能更好。實務部署時務必加入重試機制、完整性驗證與進度持久化,打造健壯的下載體驗。

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