天天看點

使用 C# 下載下傳檔案的十八般武藝

檔案下載下傳是一個軟體開發中的常見需求。本文從最簡單的下載下傳方式開始步步遞進,講述了檔案下載下傳過程中的常見問題并給出了解決方案。并展示了如何使用多線程提升 HTTP 的下載下傳速度以及調用 aria2 實作非 HTTP 協定的檔案下載下傳。

簡單下載下傳

在 .NET 程式中下載下傳檔案最簡單的方式就是使用 WebClient 的 DownloadFile 方法:

var url = "https://www.coderbusy.com";
var save = @"D:\1.html";
using (var web = new WebClient())
{
    web.DownloadFile(url,save);
}      

異步下載下傳

該方法也提供異步的實作:

var url = "https://www.coderbusy.com";
var save = @"D:\1.html";
using (var web = new WebClient())
{
    await web.DownloadFileTaskAsync(url, save);
}      

下載下傳檔案的同時向伺服器發送自定義請求頭

如果需要對檔案下載下傳請求進行定制,可以使用 HttpClient :

var url = "https://www.coderbusy.com";
var save = @"D:\1.html";
var http = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get,url);
//增加 Auth 請求頭
request.Headers.Add("Auth","123456");
var response = await http.SendAsync(request);
response.EnsureSuccessStatusCode();
using (var fs = File.Open(save, FileMode.Create))
{
    using (var ms = response.Content.ReadAsStream())
    {
        await ms.CopyToAsync(fs);
    }
}      

如何解決下載下傳檔案不完整的問題

以上所有代碼在應對小檔案的下載下傳時沒有特别大的問題,在網絡情況不佳或檔案較大時容易引入錯誤。以下代碼在開發中很常見:

var url = "https://www.coderbusy.com";
var save = @"D:\1.html";
if (!File.Exists(save))
{
    Console.WriteLine("檔案不存在,開始下載下傳...");
    using (var web = new WebClient())
    {
        await web.DownloadFileTaskAsync(url, save);
    }
    Console.WriteLine("檔案下載下傳成功");
}
Console.WriteLine("開始處理檔案");
//TODO:對檔案進行處理      

如果在 DownloadFileTaskAsync 方法中發生了異常(通常是網絡中斷或網絡逾時),那麼下載下傳不完整的檔案将會保留在本地系統中。在該任務重試執行時,因為檔案已存在(雖然它不完整)是以會直接進入處理程式,進而引入異常。

一個簡單的修複方式是引入異常處理,但這種方式對應用程式意外終止造成的檔案不完整無效:

var url = "https://www.coderbusy.com";
var save = @"D:\1.html";
if (!File.Exists(save))
{
    Console.WriteLine("檔案不存在,開始下載下傳...");
    using (var web = new WebClient())
    {
        try
        {
            await web.DownloadFileTaskAsync(url, save);
        }
        catch
        {
            if (File.Exists(save))
            {
                File.Delete(save);
            }
            throw;
        }
    }
    Console.WriteLine("檔案下載下傳成功");
}
Console.WriteLine("開始處理檔案");
//TODO:對檔案進行處理      

筆者更喜歡的方式是引入一個臨時檔案。下載下傳操作将資料下載下傳到臨時檔案中,當确定下載下傳操作執行完畢時将臨時檔案改名:

var url = "https://www.coderbusy.com";
var save = @"D:\1.html";
if (!File.Exists(save))
{
    Console.WriteLine("檔案不存在,開始下載下傳...");
    using (var web = new WebClient())
    {
        try
        {
            await web.DownloadFileTaskAsync(url, save);
        }
        catch
        {
            if (File.Exists(save))
            {
                File.Delete(save);
            }
            throw;
        }
    }
    Console.WriteLine("檔案下載下傳成功");
}
Console.WriteLine("開始處理檔案");
//TODO:對檔案進行處理      

使用 Downloader 進行 HTTP 多線程下載下傳

在網絡帶寬充足的情況下,單線程下載下傳的效率并不理想。我們需要多線程和斷點續傳才可以拿到更好的下載下傳速度。

Downloader 是一個現代化的、流暢的、異步的、可測試的和可移植的 .NET 庫。這是一個包含異步進度事件的多線程下載下傳程式。Downloader 與 .NET Standard 2.0 及以上版本相容,可以在 Windows、Linux 和 macOS 上運作。

使用 C# 下載下傳檔案的十八般武藝

GitHub 開源位址:

https://github.com/bezzad/Downloader

NuGet 位址:

https://www.nuget.org/packages/Downloader

從 NuGet 安裝 Downloader 之後,建立一個下載下傳配置:

var downloadOpt = new DownloadConfiguration()
{
    BufferBlockSize = 10240, // 通常,主機最大支援8000位元組,預設值為8000。
    ChunkCount = 8, // 要下載下傳的檔案分片數量,預設值為1
    MaximumBytesPerSecond = 1024 * 1024, // 下載下傳速度限制為1MB/s,預設值為零或無限制
    MaxTryAgainOnFailover = int.MaxValue, // 失敗的最大次數
    OnTheFlyDownload = false, // 是否在記憶體中進行緩存? 預設值是true
    ParallelDownload = true, // 下載下傳檔案是否為并行的。預設值為false
    TempDirectory = "C:\\temp", // 設定用于緩沖大塊檔案的臨時路徑,預設路徑為Path.GetTempPath()。
    Timeout = 1000, // 每個 stream reader  的逾時(毫秒),預設值是1000
    RequestConfiguration = // 定制請求頭檔案
    {
        Accept = "*/*",
        AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
        CookieContainer =  new CookieContainer(), // Add your cookies
        Headers = new WebHeaderCollection(), // Add your custom headers
        KeepAlive = false,
        ProtocolVersion = HttpVersion.Version11, // Default value is HTTP 1.1
        UseDefaultCredentials = false,
        UserAgent = $"DownloaderSample/{Assembly.GetExecutingAssembly().GetName().Version.ToString(3)}"
    }
};      

建立一個下載下傳服務:

var downloader = new DownloadService(downloadOpt);      

配置事件處理器(該步驟可以省略):

// Provide `FileName` and `TotalBytesToReceive` at the start of each downloads
// 在每次下載下傳開始時提供 "檔案名 "和 "要接收的總位元組數"。
downloader.DownloadStarted += OnDownloadStarted;

// Provide any information about chunker downloads, like progress percentage per chunk, speed, total received bytes and received bytes array to live streaming.
// 提供有關分塊下載下傳的資訊,如每個分塊的進度百分比、速度、收到的總位元組數和收到的位元組數組,以實作實時流。
downloader.ChunkDownloadProgressChanged += OnChunkDownloadProgressChanged;

// Provide any information about download progress, like progress percentage of sum of chunks, total speed, average speed, total received bytes and received bytes array to live streaming.
// 提供任何關于下載下傳進度的資訊,如進度百分比的塊數總和、總速度、平均速度、總接收位元組數和接收位元組數組的實時流。
downloader.DownloadProgressChanged += OnDownloadProgressChanged;

// Download completed event that can include occurred errors or cancelled or download completed successfully.
// 下載下傳完成的事件,可以包括發生錯誤或被取消或下載下傳成功。
downloader.DownloadFileCompleted += OnDownloadFileCompleted;      

接着就可以下載下傳檔案了:

string file = @"D:\1.html";
string url = @"https://www.coderbusy.com";
await downloader.DownloadFileTaskAsync(url, file);      

下載下傳非 HTTP 協定的檔案

除了 WebClient 可以下載下傳 FTP 協定的檔案之外,上文所示的其他方法隻能下載下傳 HTTP 協定的檔案。

aria2 是一個輕量級的多協定和多源指令行下載下傳工具。它支援 HTTP/HTTPS、FTP、SFTP、BitTorrent 和 Metalink。aria2 可以通過内置的 JSON-RPC 和 XML-RPC 接口進行操作。

我們可以調用 aria2 實作檔案下載下傳功能。

GitHub 位址:

https://github.com/aria2/aria2

下載下傳位址:

https://github.com/aria2/aria2/releases

将下載下傳好的 aria2c.exe 複制到應用程式目錄,如果是其他系統則可以下載下傳對應的二進制檔案。

public static async Task Download(string url, string fn)
{
    var exe = "aria2c";
    var dir = Path.GetDirectoryName(fn);
    var name = Path.GetFileName(fn);


    void Output(object sender, DataReceivedEventArgs args)
    {
        if (string.IsNullOrWhiteSpace(args.Data))
        {
            return;
        }
        Console.WriteLine("Aria:{0}", args.Data?.Trim());
    }

    var args = $"-x 8 -s 8 --dir={dir} --out={name} {url}";
    var info = new ProcessStartInfo(exe, args)
    {
        UseShellExecute = false,
        CreateNoWindow = true,
        RedirectStandardOutput = true,
        RedirectStandardError = true,
    };
    if (File.Exists(fn))
    {
        File.Delete(fn);
    }

    Console.WriteLine("啟動 aria2c: {0}", args);
    using (var p = new Process { StartInfo = info, EnableRaisingEvents = true })
    {
        if (!p.Start())
        {
            throw new Exception("aria 啟動失敗");
        }
        p.ErrorDataReceived += Output;
        p.OutputDataReceived += Output;
        p.BeginOutputReadLine();
        p.BeginErrorReadLine();
        await p.WaitForExitAsync();
        p.OutputDataReceived -= Output;
        p.ErrorDataReceived -= Output;
    }

    var fi = new FileInfo(fn);
    if (!fi.Exists || fi.Length == 0)
    {
        throw new FileNotFoundException("檔案下載下傳失敗", fn);
    }
}      

以上代碼通過指令行參數啟動了一個新的 aria2c 下載下傳程序,并對下載下傳進度資訊輸出在了控制台。調用方式如下:

var url = "https://www.coderbusy.com";
var save = @"D:\1.html";
await Download(url, save);      

繼續閱讀