凌峰创科服务平台

winform 从服务器下载文件

核心思路

无论使用哪种方法,核心思路都是一样的:

winform 从服务器下载文件-图1
(图片来源网络,侵删)
  1. 建立连接:客户端(WinForms 应用)向服务器发起一个 HTTP 或 FTP 请求。
  2. 获取数据流:服务器响应请求,将文件内容以数据流的形式返回。
  3. 写入本地:客户端接收这个数据流,并将其写入到本地的文件系统中。
  4. 更新 UI:在下载过程中,更新进度条、状态标签等控件,给用户反馈。

使用 HttpClient (推荐,现代、灵活)

HttpClient 是 .NET Framework 4.5 及以上版本推荐使用的类,它功能强大,支持异步操作,能更好地避免界面卡顿。

步骤 1: 创建 WinForms 项目

  1. 打开 Visual Studio,创建一个新的 "Windows Forms App (.NET Framework)" 项目。
  2. 在窗体上拖放以下控件:
    • Button (命名为 btnDownload)
    • ProgressBar (命名为 progressBar)
    • Label (命名为 lblStatus)
    • TextBox (用于输入服务器文件 URL,命名为 txtUrl)

步骤 2: 编写下载代码

双击 btnDownload 按钮,生成其 Click 事件处理方法,然后编写以下代码:

using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WinFormsDownloader
{
    public partial class MainForm : Form
    {
        // 使用 static HttpClient 是一个最佳实践,避免重复创建和销毁
        private static readonly HttpClient client = new HttpClient();
        public MainForm()
        {
            InitializeComponent();
        }
        private async void btnDownload_Click(object sender, EventArgs e)
        {
            // 1. 检查 URL 是否为空
            if (string.IsNullOrWhiteSpace(txtUrl.Text))
            {
                MessageBox.Show("请输入有效的文件下载地址。", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                return;
            }
            string downloadUrl = txtUrl.Text;
            string localPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), Path.GetFileName(downloadUrl));
            try
            {
                // 2. 禁用下载按钮,防止重复点击
                btnDownload.Enabled = false;
                lblStatus.Text = "准备下载...";
                progressBar.Value = 0;
                progressBar.Style = ProgressBarStyle.Marquee; // 开始时显示为忙碌状态
                // 3. 发起异步 GET 请求
                using (HttpResponseMessage response = await client.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead))
                {
                    // 确保请求成功 (状态码 200-299)
                    response.EnsureSuccessStatusCode();
                    // 4. 获取文件总大小 (用于计算进度)
                    long totalBytes = response.Content.Headers.ContentLength ?? -1L; // -1 表示大小未知
                    long totalBytesRead = 0L;
                    // 5. 打开文件流和内容流
                    using (var contentStream = await response.Content.ReadAsStreamAsync())
                    using (var fileStream = new FileStream(localPath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true)) // true 表示异步文件操作
                    {
                        var buffer = new byte[8192]; // 8KB 缓冲区
                        int bytesRead;
                        // 6. 循环读取流内容并写入本地文件
                        while ((bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
                        {
                            await fileStream.WriteAsync(buffer, 0, bytesRead);
                            totalBytesRead += bytesRead;
                            // 7. 更新进度条和标签
                            if (totalBytes > 0)
                            {
                                double progressPercentage = (double)totalBytesRead / totalBytes * 100;
                                // 使用 Invoke 确保在 UI 线程上更新控件
                                this.Invoke((MethodInvoker)delegate
                                {
                                    progressBar.Value = (int)progressPercentage;
                                    lblStatus.Text = $"下载中... {progressPercentage:F2}% ({totalBytesRead / 1024.0 / 1024:F2} MB / {totalBytes / 1024.0 / 1024:F2} MB)";
                                });
                            }
                            else
                            {
                                // 如果文件大小未知,只能显示已下载的字节数
                                this.Invoke((MethodInvoker)delegate
                                {
                                    lblStatus.Text = $"下载中... 已下载 {totalBytesRead / 1024.0 / 1024:F2} MB";
                                });
                            }
                        }
                    }
                }
                // 8. 下载完成
                this.Invoke((MethodInvoker)delegate
                {
                    lblStatus.Text = "下载完成!";
                    MessageBox.Show($"文件已成功下载到:\n{localPath}", "下载成功", MessageBoxButtons.OK, MessageBoxIcon.Information);
                });
            }
            catch (Exception ex)
            {
                // 9. 错误处理
                this.Invoke((MethodInvoker)delegate
                {
                    lblStatus.Text = "下载失败!";
                    MessageBox.Show($"下载过程中发生错误: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
                });
            }
            finally
            {
                // 10. 恢复按钮状态
                this.Invoke((MethodInvoker)delegate
                {
                    btnDownload.Enabled = true;
                });
            }
        }
    }
}

代码解析

  1. static readonly HttpClient: 将 HttpClient 声明为 staticreadonly 是一个非常重要的性能优化,它可以在整个应用程序生命周期内被复用,避免了每次请求都创建新实例的开销。
  2. async/await: 使用异步编程模型,当网络请求或文件 I/O 发生时,线程不会阻塞,而是会释放出来,让 UI 线程可以继续响应用户操作(移动窗口、点击其他按钮),从而避免了界面“卡死”。
  3. HttpCompletionOption.ResponseHeadersRead: 这是一个优化选项,它告诉 HttpClient 一旦收到响应头就立即返回,而不需要等待整个响应体下载完成,这对于大文件下载尤其有用,可以更快地开始写入本地文件。
  4. ReadAsStreamAsync()FileStream: 直接操作流,而不是将整个文件读入内存,极大地降低了内存消耗,特别是对于 GB 级别的大文件。
  5. Invoke: 由于下载逻辑运行在一个后台线程(由 async/await 自动管理),而 UI 控件(如 ProgressBarLabel)只能在 UI 线程上访问。Invoke 方法确保了更新 UI 的代码在正确的线程上执行,防止了跨线程访问 UI 控件时可能引发的异常。
  6. try...catch...finally: 结构化的错误处理和资源清理。finally 块确保无论成功还是失败,下载按钮都会被重新启用。

使用 WebClient (简单、旧版)

WebClient 是 .NET Framework 中更早的类,使用起来非常简单,但功能相对 HttpClient 较弱,且已被标记为“过时”(obsolete),对于新项目,建议优先使用 HttpClient

using System;
using System.Net;
using System.Windows.Forms;
// 在窗体上添加一个 BackgroundWorker 控件 (backgroundWorker1)
private void btnDownload_Click(object sender, EventArgs e)
{
    string downloadUrl = txtUrl.Text;
    string localPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), Path.GetFileName(downloadUrl));
    // 设置 BackgroundWorker 事件
    backgroundWorker1.DoWork += (s, args) =>
    {
        using (WebClient client = new WebClient())
        {
            // 为 DownloadProgressChanged 事件绑定处理程序
            client.DownloadProgressChanged += (sender2, e2) =>
            {
                // 这里的代码会在 UI 线程上自动执行,无需 Invoke
                progressBar.Value = e2.ProgressPercentage;
                lblStatus.Text = $"下载中... {e2.ProgressPercentage}% ({e2.BytesReceived / 1024.0 / 1024:F2} MB)";
            };
            // 执行下载
            client.DownloadFileAsync(new Uri(downloadUrl), localPath);
        }
    };
    backgroundWorker1.RunWorkerCompleted += (s, args) =>
    {
        if (args.Error != null)
        {
            lblStatus.Text = "下载失败!";
            MessageBox.Show($"下载失败: {args.Error.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
        }
        else
        {
            lblStatus.Text = "下载完成!";
            MessageBox.Show("文件下载成功!", "成功", MessageBoxButtons.OK, MessageBoxIcon.Information);
        }
        btnDownload.Enabled = true;
    };
    btnDownload.Enabled = false;
    lblStatus.Text = "准备下载...";
    backgroundWorker1.RunWorkerAsync();
}

WebClient 的优缺点:

winform 从服务器下载文件-图2
(图片来源网络,侵删)
  • 优点: 代码量少,内置了进度更新事件 (DownloadProgressChanged),使用起来非常方便。
  • 缺点:
    • 已被微软标记为过时,未来版本可能会被移除。
    • 灵活性不如 HttpClient
    • 默认是同步的,需要结合 async/awaitBackgroundWorker 来避免 UI 卡顿。

高级功能:断点续传

断点续传允许在下载中断后,从上次停止的地方继续下载,而不是重新开始,这需要服务器支持 Range 请求头。

实现原理

  1. 客户端先检查本地是否存在一个未下载完的临时文件。
  2. 如果存在,获取该文件的大小。
  3. 在向服务器请求文件时,添加 Range 请求头,Range: bytes=102400-,表示从第 102400 字节开始下载。
  4. 服务器如果支持,会返回 206 Partial Content 状态码和文件剩余部分的内容。
  5. 客户端将接收到的内容以追加模式写入临时文件。

以下是使用 HttpClient 实现断点续传的关键代码片段:

// 在 MainForm 类中添加一个字段来存储临时文件路径
private string _tempFilePath;
// 在 btnDownload_Click 方法中修改下载逻辑
string downloadUrl = txtUrl.Text;
_tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetFileName(downloadUrl) + ".tmp");
string finalFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), Path.GetFileName(downloadUrl));
long initialPosition = 0;
if (File.Exists(_tempFilePath))
{
    initialPosition = new FileInfo(_tempFilePath).Length;
}
// 创建 HttpRequestMessage
var request = new HttpRequestMessage(HttpMethod.Get, downloadUrl);
if (initialPosition > 0)
{
    request.Headers.Range = new System.Net.Http.Headers.RangeHeaderValue(initialPosition, null); // 从 initialPosition 开始到末尾
}
using (HttpResponseMessage response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead))
{
    if (initialPosition > 0 && response.StatusCode != HttpStatusCode.PartialContent)
    {
        // 服务器不支持断点续传,重新开始下载
        lblStatus.Text = "服务器不支持断点续传,将重新开始下载...";
        File.Delete(_tempFilePath);
        // ... 这里可以递归调用自己或者重新开始流程 ...
        return;
    }
    response.EnsureSuccessStatusCode();
    long totalBytes = response.Content.Headers.ContentLength ?? (initialPosition + (response.Content.Headers.ContentLength ?? -1));
    long totalBytesRead = initialPosition;
    using (var contentStream = await response.Content.ReadAsStreamAsync())
    using (var fileStream = new FileStream(_tempFilePath, FileMode.Append, FileAccess.Write, FileShare.None, 8192, true))
    {
        // ... (这里的循环和进度更新逻辑与之前基本相同,只是写入模式是 Append) ...
    }
}
// 下载完成后,将临时文件重命名为最终文件
File.Move(_tempFilePath, finalFilePath);

总结与建议

特性 HttpClient WebClient
推荐度 ⭐⭐⭐⭐⭐ (首选) ⭐⭐ (仅用于简单场景)
易用性 中等,需要自己处理进度和流 简单,内置事件
灵活性 高,可定制请求头、超时等
性能 高,支持流式处理 一般
异步支持 原生支持 async/await 需要配合 async/awaitBackgroundWorker
状态 现代,微软推荐 已过时

给你的建议:

winform 从服务器下载文件-图3
(图片来源网络,侵删)
  • 对于所有新项目,请优先选择 HttpClient,它更强大、更灵活,是 .NET 生态系统的标准。
  • 如果只是需要一个快速、简单的下载功能,且不关心 WebClient 的未来状态,可以使用 WebClient,它能让你用更少的代码实现基本功能。
  • 如果需要下载大文件不希望 UI 卡顿必须使用异步方法 (async/await)
  • 如果网络不稳定,需要可靠的下载体验,请考虑实现断点续传功能。
分享:
扫描分享到社交APP
上一篇
下一篇