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

- 建立连接:客户端(WinForms 应用)向服务器发起一个 HTTP 或 FTP 请求。
- 获取数据流:服务器响应请求,将文件内容以数据流的形式返回。
- 写入本地:客户端接收这个数据流,并将其写入到本地的文件系统中。
- 更新 UI:在下载过程中,更新进度条、状态标签等控件,给用户反馈。
使用 HttpClient (推荐,现代、灵活)
HttpClient 是 .NET Framework 4.5 及以上版本推荐使用的类,它功能强大,支持异步操作,能更好地避免界面卡顿。
步骤 1: 创建 WinForms 项目
- 打开 Visual Studio,创建一个新的 "Windows Forms App (.NET Framework)" 项目。
- 在窗体上拖放以下控件:
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;
});
}
}
}
}
代码解析
static readonly HttpClient: 将HttpClient声明为static和readonly是一个非常重要的性能优化,它可以在整个应用程序生命周期内被复用,避免了每次请求都创建新实例的开销。async/await: 使用异步编程模型,当网络请求或文件 I/O 发生时,线程不会阻塞,而是会释放出来,让 UI 线程可以继续响应用户操作(移动窗口、点击其他按钮),从而避免了界面“卡死”。HttpCompletionOption.ResponseHeadersRead: 这是一个优化选项,它告诉HttpClient一旦收到响应头就立即返回,而不需要等待整个响应体下载完成,这对于大文件下载尤其有用,可以更快地开始写入本地文件。ReadAsStreamAsync()和FileStream: 直接操作流,而不是将整个文件读入内存,极大地降低了内存消耗,特别是对于 GB 级别的大文件。Invoke: 由于下载逻辑运行在一个后台线程(由async/await自动管理),而 UI 控件(如ProgressBar和Label)只能在 UI 线程上访问。Invoke方法确保了更新 UI 的代码在正确的线程上执行,防止了跨线程访问 UI 控件时可能引发的异常。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 的优缺点:

- 优点: 代码量少,内置了进度更新事件 (
DownloadProgressChanged),使用起来非常方便。 - 缺点:
- 已被微软标记为过时,未来版本可能会被移除。
- 灵活性不如
HttpClient。 - 默认是同步的,需要结合
async/await或BackgroundWorker来避免 UI 卡顿。
高级功能:断点续传
断点续传允许在下载中断后,从上次停止的地方继续下载,而不是重新开始,这需要服务器支持 Range 请求头。
实现原理:
- 客户端先检查本地是否存在一个未下载完的临时文件。
- 如果存在,获取该文件的大小。
- 在向服务器请求文件时,添加
Range请求头,Range: bytes=102400-,表示从第 102400 字节开始下载。 - 服务器如果支持,会返回
206 Partial Content状态码和文件剩余部分的内容。 - 客户端将接收到的内容以追加模式写入临时文件。
以下是使用 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/await 或 BackgroundWorker |
| 状态 | 现代,微软推荐 | 已过时 |
给你的建议:

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