-
异步IO操作——C#编写关于大文件多线程下载
第一部分:C#基础入门
5.异步IO操作——大文件多线程下载
实例介绍
大文件(如1GB以上的视频、压缩包)单线程下载存在两个核心问题:速度慢(单线程只能利用vb.net教程C#教程python教程SQL教程access 2010教程
部分带宽)、阻塞UI(同步IO会卡住界面)。异步IO+多块并行下载通过以下方式解决:
分块下载:将大文件拆分为多个固定大小的块(如1MB/块),同时下载多个块;
异步IO:网络请求、文件读写均用异步方法,不阻塞线程,提高CPU利用率;
并行处理:多个块并行下载,充分利用带宽资源。
本节通过大文件分块下载工具的实现,带你掌握异步IO与多块并行的核心逻辑,理解“为什么大文件要这么下载”。
需求分析
设计一个大文件下载工具,核心需求如下:
1.分块逻辑:将文件按固定大小(如1MB)拆分,最后一块自动适配剩余大小;
2.异步IO:网络请求(获取块数据)、文件写入(保存块)均用异步方法;
3.进度报告:实时显示总下载进度、每个块的下载状态;
4.文件合并:所有块下载完成后,按顺序合并为完整文件;
5.异常处理:单个块下载失败自动重试(最多3次);
6.断点续传:记录已下载的块,下次启动时跳过已完成的块(可选但实用)。
代码实现
-
核心模型类
csharp
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
// 下载任务信息
public class DownloadTask
{
public string FileUrl { get; set; } // 文件URL
public string SavePath { get; set; } // 保存路径
public long BlockSize { get; set; } = 1024 * 1024; // 块大小(默认1MB)
public int MaxRetryCount { get; set; } = 3; // 最大重试次数
}
// 分块信息
public class BlockInfo
{
public int Index { get; set; } // 块索引(从0开始)
public long StartByte { get; set; } // 块开始字节
public long EndByte { get; set; } // 块结束字节
public string TempFilePath { get; set; } // 临时块文件路径
public bool IsCompleted { get; set; } = false; // 是否完成
}
-
大文件下载服务类
csharp
public class LargeFileDownloader
{
private readonly HttpClient _httpClient;
private List<BlockInfo> _blocks;
private DownloadTask _task;
private long _totalFileSize; // 文件总大小
public LargeFileDownloader()
{
// 复用HttpClient实例(避免频繁创建销毁)
_httpClient = new HttpClient { Timeout = TimeSpan.FromMinutes(5) };
}
// 启动下载(入口方法)
public async Task StartDownloadAsync(DownloadTask task)
{
_task = task;
Console.WriteLine($"=== 开始下载:{task.FileUrl} ===");
// 1. 获取文件总大小(通过HEAD请求)
_totalFileSize = await GetFileSizeAsync(task.FileUrl);
Console.WriteLine($"文件总大小:{_totalFileSize / (1024*1024)} MB");
// 2. 计算分块
_blocks = CalculateBlocks(_totalFileSize, task.BlockSize);
Console.WriteLine($"分块数量:{_blocks.Count}");
// 3. 异步下载所有块(并行)
var blockTasks = new List<Task>();
foreach (var block in _blocks)
{
blockTasks.Add(DownloadBlockAsync(block));
}
await Task.WhenAll(blockTasks);
// 4. 合并块文件
await MergeBlocksAsync();
Console.WriteLine($"=== 下载完成:{task.SavePath} ===");
}
// 步骤1:获取文件总大小(HEAD请求,不下载内容)
private async Task<long> GetFileSizeAsync(string url)
{
var request = new HttpRequestMessage(HttpMethod.Head, url);
var response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode(); // 确保请求成功
// 从响应头获取文件大小(Content-Length)
if (long.TryParse(response.Content.Headers.ContentLength.ToString(), out long size))
{
return size;
}
throw new Exception("无法获取文件大小");
}
// 步骤2:计算分块
private List<BlockInfo> CalculateBlocks(long totalSize, long blockSize)
{
var blocks = new List<BlockInfo>();
long currentStart = 0;
int index = 0;
while (currentStart < totalSize)
{
long currentEnd = Math.Min(currentStart + blockSize -1, totalSize -1);
var block = new BlockInfo
{
Index = index,
StartByte = currentStart,
EndByte = currentEnd,
TempFilePath = Path.Combine(Path.GetDirectoryName(_task.SavePath), $"temp_{index}.block")
};
blocks.Add(block);
currentStart = currentEnd + 1;
index++;
}
return blocks;
}
// 步骤3:异步下载单个块
private async Task DownloadBlockAsync(BlockInfo block)
{
int retryCount = 0;
while (retryCount < _task.MaxRetryCount)
{
try
{
Console.WriteLine($"下载块 {block.Index}:{block.StartByte}-{block.EndByte}");
var request = new HttpRequestMessage(HttpMethod.Get, _task.FileUrl);
// 设置Range请求头:获取指定范围的字节(HTTP协议支持)
request.Headers.Range = new System.Net.Http.Headers.RangeHeaderValue(block.StartByte, block.EndByte);
var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode();
// 异步写入块文件(IO密集型,用异步方法)
using var stream = await response.Content.ReadAsStreamAsync();
using var fileStream = new FileStream(block.TempFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 4096, true); // 最后一个参数为true:异步文件流
await stream.CopyToAsync(fileStream); // 异步复制流(不阻塞线程)
block.IsCompleted = true;
Console.WriteLine($"块 {block.Index} 下载完成");
return;
}
catch (Exception ex)
{
retryCount++;
Console.WriteLine($"块 {block.Index} 下载失败(第{retryCount}次重试):{ex.Message}");
if (retryCount >= _task.MaxRetryCount)
{
throw new Exception($"块 {block.Index} 重试次数耗尽");
}
await Task.Delay(1000); // 重试前等待1秒
}
}
}
// 步骤4:合并块文件
private async Task MergeBlocksAsync()
{
Console.WriteLine("=== 开始合并块文件 ===");
using var outputStream = new FileStream(_task.SavePath, FileMode.Create, FileAccess.Write, FileShare.None, 4096, true);
foreach (var block in _blocks.OrderBy(b => b.Index)) // 按索引顺序合并
{
if (!block.IsCompleted)
{
throw new Exception($"块 {block.Index} 未完成,无法合并");
}
using var blockStream = new FileStream(block.TempFilePath, FileMode.Open, FileAccess.Read, FileShare.None,4096,true);
await blockStream.CopyToAsync(outputStream); // 异步复制到输出流
File.Delete(block.TempFilePath); // 合并后删除临时块
}
Console.WriteLine("=== 块文件合并完成 ===");
}
}
-
测试程序
csharp
class Program
{
static async Task Main(string[] args)
{
var downloader = new LargeFileDownloader();
var task = new DownloadTask
{
FileUrl = "https://example.com/largefile.zip", // 替换为实际大文件URL
SavePath = @"C:Downloadslargefile.zip",
BlockSize = 2*1024*1024, // 2MB per block
MaxRetryCount =3
};
try
{
await downloader.StartDownloadAsync(task);
}
catch (Exception ex)
{
Console.WriteLine($"下载失败:{ex.Message}");
}
}
}
逐行讲解
-
模型类
DownloadTask:封装下载任务的核心参数,如URL、保存路径、块大小、重试次数,让代码更整洁;
BlockInfo:记录每个分块的索引、字节范围、临时文件路径、完成状态,方便管理每个块的下载进度。 -
下载服务类(LargeFileDownloader)
构造函数:创建复用的HttpClient实例(HttpClient是线程安全的,复用能减少资源消耗);
StartDownloadAsync:入口方法,串联获取文件大小、分块、下载块、合并文件四个步骤;
GetFileSizeAsync:用HEAD请求获取文件大小(HEAD请求只返回响应头,不下载内容,效率高);
CalculateBlocks:按块大小拆分文件,最后一块处理边界情况(避免超出总大小);
DownloadBlockAsync:
用Range请求头获取指定字节范围的内容(HTTP协议支持,是分块下载的核心);
异步读取响应流并写入临时文件(用CopyToAsync异步方法,不阻塞线程);
重试逻辑:失败后重试最多3次,避免网络波动导致下载失败;
MergeBlocksAsync:按索引顺序合并临时块,合并后删除临时文件,节省空间。 -
测试程序
Main方法:创建下载任务,调用StartDownloadAsync,捕获异常并处理,确保程序稳定。
基础知识拓展 -
异步IO的核心原理
IO密集型任务:文件读写、网络请求等操作,CPU等待IO完成的时间远大于计算时间;
异步IO:线程发起IO请求后,不阻塞等待,而是继续处理其他任务,IO完成后通过回调通知线程继续执行;
同步IO vs异步IO:同步IO会阻塞线程,导致线程资源浪费;异步IO能提高线程利用率,适合高并发场景。 -
Range请求头的作用
HTTP Range:允许客户端请求文件的一部分,格式为Range: bytes=start-end;
应用场景:分块下载、断点续传(记录已下载的字节范围,下次继续);
注意事项:服务器必须支持Range请求(响应头包含Accept-Ranges: bytes),否则无法分块。 -
断点续传的实现思路
记录进度?将已完成的块信息保存到本地文件(如JSON);
启动时恢复:下次下载前读取进度文件,跳过已完成的块;
实现步骤:在StartDownloadAsync中检查进度文件,更新_blocks的IsCompleted状态,只下载未完成的块。 -
多块并行的注意事项
块大小选择:块太小会增加请求次数( overhead ),块太大则并行度低;建议1-4MB;
服务器限制:部分服务器限制同时连接数,过多并行请求可能被拒绝;
资源控制:用SemaphoreSlim限制并行请求数量(如最多5个并行块),避免资源耗尽。 -
异常处理的最佳实践
重试策略:对网络请求失败重试,设置合理的重试间隔(如指数退避:1s→2s→4s);
错误日志:记录失败的块信息,方便排查问题;
优雅退出:下载中断时,保存进度文件,下次继续。
总结
大文件异步IO下载的核心是分块+并行+异步:
分块:拆分大任务为小任务,提高并行度;
并行:同时下载多个块,充分利用带宽;
异步:不阻塞线程,提高CPU利用率。
掌握本节内容后,你可以扩展功能:比如断点续传、进度条显示、多服务器镜像选择等,满足更复杂的下载需求。异步IO是C#开发中必备的技能,尤其在处理IO密集型任务时,能大幅提升程序性能。
最新更新
LINQ高级查询——C#进行复杂数据分组与聚
异步IO操作——C#编写关于大文件多线程下
异步编程基础——C#开发Task异步方法设计
事件驱动编程——C#开发实时消息通知系
委托与Lambda——C#简化事件处理逻辑
泛型集合自定义——C#关于类型安全的队
Excel操作——用C#开发学生成绩表生成(
CSV文件处理——C#编写销售数据导入导出
数据验证——用C#开发表单输入合法性检
正则表达式——C#编写邮箱/手机号验证工
SQL SERVER中递归
2个场景实例讲解GaussDB(DWS)基表统计信息估
常用的 SQL Server 关键字及其含义
动手分析SQL Server中的事务中使用的锁
openGauss内核分析:SQL by pass & 经典执行
一招教你如何高效批量导入与更新数据
天天写SQL,这些神奇的特性你知道吗?
openGauss内核分析:执行计划生成
[IM002]Navicat ODBC驱动器管理器 未发现数据
初入Sql Server 之 存储过程的简单使用
uniapp/H5 获取手机桌面壁纸 (静态壁纸)
[前端] DNS解析与优化
为什么在js中需要添加addEventListener()?
JS模块化系统
js通过Object.defineProperty() 定义和控制对象
这是目前我见过最好的跨域解决方案!
减少回流与重绘
减少回流与重绘
如何使用KrpanoToolJS在浏览器切图
performance.now() 与 Date.now() 对比










