1. 项目概述
在文件传输领域,断点续传技术一直是提升下载可靠性的关键解决方案。我最近用C#实现了一个支持断点续传的文件下载工具,这个项目源于实际工作中频繁遇到的大文件下载中断问题。当网络不稳定或系统意外重启时,传统下载方式往往需要从头开始,既浪费时间又消耗带宽。
这个工具的核心价值在于:
- 自动记录下载进度,中断后可从中断点继续
- 支持多线程分块下载,提升传输效率
- 内置完整性校验机制,确保文件正确性
- 提供友好的进度显示和日志记录
2. 核心技术解析
2.1 HTTP范围请求机制
断点续传的基础是HTTP协议的Range头部字段。通过发送类似这样的请求:
http复制GET /largefile.zip HTTP/1.1
Host: example.com
Range: bytes=102400-
服务器会返回从100KB开始的文件内容,并附带206 Partial Content状态码。在C#中,我们可以通过HttpClient的AddRange方法实现:
csharp复制var request = new HttpRequestMessage(HttpMethod.Get, fileUrl);
request.Headers.Range = new RangeHeaderValue(startByte, endByte);
2.2 分块下载策略
为提高下载效率,工具采用了分块并行下载策略:
- 获取文件总大小(HEAD请求)
- 根据配置的线程数计算分块大小
- 为每个分块创建独立下载任务
- 合并下载完成的分块
典型的分块计算逻辑:
csharp复制long chunkSize = fileSize / threadCount;
for(int i=0; i<threadCount; i++){
long start = i * chunkSize;
long end = (i == threadCount-1) ? fileSize-1 : start+chunkSize-1;
// 创建下载任务
}
2.3 断点记录与恢复
关键数据结构设计:
csharp复制class DownloadCheckpoint {
public string FileUrl { get; set; }
public string LocalPath { get; set; }
public long FileSize { get; set; }
public List<ChunkInfo> Chunks { get; set; }
}
class ChunkInfo {
public int Index { get; set; }
public long Start { get; set; }
public long End { get; set; }
public bool Completed { get; set; }
}
恢复流程:
- 检查是否存在断点记录文件
- 验证源文件是否变更(通过ETag/LastModified)
- 重新下载未完成的分块
- 合并所有分块后删除记录文件
3. 完整实现方案
3.1 核心类设计
csharp复制public class ResumableDownloader
{
private readonly HttpClient _httpClient;
private readonly int _maxParallel;
private readonly string _tempDir;
public event Action<ProgressInfo> ProgressChanged;
public ResumableDownloader(int maxParallel = 4)
{
_httpClient = new HttpClient();
_maxParallel = maxParallel;
_tempDir = Path.Combine(Path.GetTempPath(), "DownloadCache");
Directory.CreateDirectory(_tempDir);
}
}
3.2 下载流程实现
主下载方法逻辑:
csharp复制public async Task DownloadAsync(string url, string savePath, CancellationToken token)
{
// 1. 获取文件元数据
var fileInfo = await GetFileInfoAsync(url);
// 2. 检查/创建断点记录
var checkpoint = LoadCheckpoint(url, savePath) ?? CreateNewCheckpoint(url, savePath, fileInfo);
// 3. 并行下载分块
await DownloadChunksAsync(checkpoint, token);
// 4. 合并文件
MergeFiles(checkpoint);
}
3.3 关键优化点
- 缓冲区管理:
csharp复制const int BUFFER_SIZE = 81920; // 80KB
using var buffer = ArrayPool<byte>.Shared.Rent(BUFFER_SIZE);
- 进度计算:
csharp复制long totalDownloaded = checkpoint.Chunks
.Where(c => c.Completed)
.Sum(c => c.End - c.Start + 1);
double progress = (double)totalDownloaded / checkpoint.FileSize;
- 错误重试机制:
csharp复制int retryCount = 0;
while(retryCount < MAX_RETRY)
{
try {
await DownloadChunkAsync(chunk);
break;
}
catch {
retryCount++;
await Task.Delay(1000 * retryCount);
}
}
4. 实战问题与解决方案
4.1 常见问题排查
- 进度不更新问题
- 检查是否跨线程更新UI(需通过Dispatcher.Invoke)
- 验证进度计算分母是否为0
- 文件合并失败
- 确保所有分块文件存在且大小正确
- 检查文件写入权限
- 下载速度慢
- 适当增加线程数(建议不超过8)
- 检查是否启用了压缩传输:
csharp复制_httpClient.DefaultRequestHeaders.AcceptEncoding.Add(
new StringWithQualityHeaderValue("gzip"));
4.2 性能优化技巧
- 内存优化:
csharp复制// 使用FileStream的异步API
using var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, 4096, true);
await fs.WriteAsync(buffer, 0, bytesRead);
- 连接复用:
csharp复制// 在构造函数中配置
var handler = new SocketsHttpHandler {
PooledConnectionLifetime = TimeSpan.FromMinutes(5)
};
_httpClient = new HttpClient(handler);
- 智能分块:
csharp复制// 根据文件大小动态调整分块
int CalculateChunkCount(long fileSize)
{
if(fileSize < 10 * 1024 * 1024) return 1;
if(fileSize < 100 * 1024 * 1024) return 4;
return _maxParallel;
}
5. 扩展功能实现
5.1 下载限速控制
csharp复制private async Task ThrottledCopyAsync(Stream source, Stream destination,
int maxBytesPerSecond)
{
var buffer = new byte[81920];
int bytesRead;
var stopwatch = new Stopwatch();
while ((bytesRead = await source.ReadAsync(buffer)) > 0)
{
stopwatch.Start();
await destination.WriteAsync(buffer, 0, bytesRead);
var elapsed = stopwatch.ElapsedMilliseconds;
var expected = (bytesRead * 1000L) / maxBytesPerSecond;
if (expected > elapsed)
{
await Task.Delay(TimeSpan.FromMilliseconds(expected - elapsed));
}
stopwatch.Reset();
}
}
5.2 代理服务器支持
csharp复制var proxy = new WebProxy("proxy.example.com", 8080)
{
Credentials = new NetworkCredential("user", "pass")
};
var handler = new HttpClientHandler { Proxy = proxy };
_httpClient = new HttpClient(handler);
5.3 文件校验机制
csharp复制async Task<string> CalculateFileHash(string path)
{
using var md5 = MD5.Create();
using var stream = File.OpenRead(path);
var hash = await md5.ComputeHashAsync(stream);
return BitConverter.ToString(hash).Replace("-", "");
}
// 与服务器提供的ETag或Content-MD5比对
6. 完整使用示例
6.1 基础调用方式
csharp复制var downloader = new ResumableDownloader();
downloader.ProgressChanged += progress => {
Console.WriteLine($"进度: {progress.Percentage:P1} 速度: {progress.Speed/1024}KB/s");
};
try {
await downloader.DownloadAsync(
"https://example.com/largefile.zip",
"C:\\Downloads\\largefile.zip",
CancellationToken.None);
}
catch (Exception ex) {
Console.WriteLine($"下载失败: {ex.Message}");
}
6.2 高级配置示例
csharp复制var settings = new DownloaderSettings {
MaxParallel = 8,
MaxRetry = 5,
Timeout = TimeSpan.FromMinutes(5),
TempDirectory = "D:\\Temp\\Downloads"
};
var downloader = new ResumableDownloader(settings);
// 自定义验证逻辑
downloader.FileValidation = async (original, downloaded) => {
return await CalculateFileHash(downloaded) == original.ETag;
};
6.3 实际应用场景
- 软件自动更新:
csharp复制// 检查新版本
if(newVersionAvailable)
{
await downloader.DownloadAsync(updateUrl, tempPath);
VerifySignature(tempPath);
InstallUpdate(tempPath);
}
- 媒体文件下载:
csharp复制// 支持暂停/继续
var cts = new CancellationTokenSource();
// 暂停按钮点击
pauseButton.Click += () => cts.Cancel();
// 继续下载时使用相同的保存路径
// 会自动检测并恢复下载
这个工具在实际项目中已经稳定运行超过6个月,累计下载文件超过5TB。最关键的收获是:对于大文件传输,完善的错误恢复机制比单纯的传输速度更重要。建议在实现时特别注意边界条件的处理,比如磁盘空间不足、网络闪断等情况。
