并发文件下载器
使用 Tokio + reqwest 构建一个支持多线程下载、断点续传、进度显示的下载工具
你将学到
- 使用 Tokio 实现并发任务
- 使用 reqwest 发送 HTTP 请求
- 实现分片下载和合并
- 添加进度条显示
前置知识
架构设计
downloader/
├── src/
│ ├── main.rs # 入口
│ ├── downloader.rs # 下载核心
│ ├── chunk.rs # 分片管理
│ └── progress.rs # 进度显示
└── Cargo.toml
实现步骤
第一步:项目初始化
cargo new downloader
cd downloader
# Cargo.toml
[package]
name = "downloader"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.11", features = ["stream"] }
futures = "0.3"
indicatif = "0.17"
第二步:下载核心
// src/downloader.rs
use reqwest::Client;
use std::path::Path;
use tokio::fs::File;
use tokio::io::AsyncWriteExt;
pub struct Downloader {
client: Client,
chunks: usize,
}
impl Downloader {
pub fn new(chunks: usize) -> Self {
Downloader {
client: Client::new(),
chunks,
}
}
pub async fn download(&self, url: &str, output: &str) -> Result<(), Box<dyn std::error::Error>> {
// 获取文件大小
let resp = self.client.head(url).send().await?;
let total_size = resp.headers()
.get("content-length")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse::<u64>().ok())
.unwrap_or(0);
if total_size == 0 {
// 不支持分片下载,直接下载
return self.download_single(url, output).await;
}
// 分片下载
let chunk_size = total_size / self.chunks as u64;
let mut handles = vec![];
for i in 0..self.chunks {
let start = i as u64 * chunk_size;
let end = if i == self.chunks - 1 {
total_size - 1
} else {
start + chunk_size - 1
};
let url = url.to_string();
let client = self.client.clone();
let output = format!("{}.part{}", output, i);
handles.push(tokio::spawn(async move {
Self::download_chunk(&client, &url, start, end, &output).await
}));
}
// 等待所有分片完成
for handle in handles {
handle.await??;
}
// 合并分片
self.merge_chunks(output, self.chunks).await?;
Ok(())
}
async fn download_chunk(
client: &Client,
url: &str,
start: u64,
end: u64,
output: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let resp = client
.get(url)
.header("Range", format!("bytes={}-{}", start, end))
.send()
.await?;
let bytes = resp.bytes().await?;
let mut file = File::create(output).await?;
file.write_all(&bytes).await?;
Ok(())
}
async fn download_single(&self, url: &str, output: &str) -> Result<(), Box<dyn std::error::Error>> {
let resp = self.client.get(url).send().await?;
let bytes = resp.bytes().await?;
let mut file = File::create(output).await?;
file.write_all(&bytes).await?;
Ok(())
}
async fn merge_chunks(&self, output: &str, chunks: usize) -> Result<(), Box<dyn std::error::Error>> {
let mut file = File::create(output).await?;
for i in 0..chunks {
let part = format!("{}.part{}", output, i);
let data = tokio::fs::read(&part).await?;
file.write_all(&data).await?;
tokio::fs::remove_file(&part).await?;
}
Ok(())
}
}
第三步:主程序
// src/main.rs
mod downloader;
use downloader::Downloader;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let url = std::env::args().nth(1).expect("Usage: downloader <URL>");
let output = std::env::args().nth(2).unwrap_or_else(|| "output.bin".to_string());
let downloader = Downloader::new(4);
downloader.download(&url, &output).await?;
println!("✅ 下载完成: {}", output);
Ok(())
}
完整项目结构
downloader/
├── src/
│ ├── main.rs # 程序入口
│ ├── downloader.rs # 并发下载核心
│ ├── chunk.rs # 分片管理
│ └── progress.rs # 进度条显示
└── Cargo.toml
最佳实践
- 分片下载:大文件分成多个小块并行下载
- 断点续传:记录已下载的分片,支持中断后继续
- 进度显示:使用 indicatif 显示下载进度
- 错误重试:网络失败时自动重试
常见问题
Q: 如何支持断点续传? A:记录已下载的分片信息到文件,下次启动时检查并跳过已完成的分片。
Q: 如何限制下载速度? A: 使用 tokio::time::interval 控制下载速率。
Q: 如何处理大文件? A: 使用流式写入,避免一次性加载到内存。
扩展挑战
- 添加下载速度限制
- 支持多文件批量下载
- 实现下载队列
相关课程
| 课程 | 相关章节 |
|---|---|
| Rust | 异步编程:async/await |
| Rust | Tokio 运行时基础 |
| Rust | 并发编程基础 |
| Rust | 常用集合:Vec, String, HashMap |