五一假期的周末,外面人山人海,不如宅家用 Rust 写个下载器。从零到一,一天时间,支持断点续传、并发下载、进度条。
缘起:假期宅家,不如写点代码
五一假期,朋友圈里全是人山人海的景区照片。我选择宅在家里,刷着手机,翻来覆去不知道干什么。假期第五天,该追的剧追完了,该玩的游戏也玩腻了,总觉得该做点什么但又没有明确的方向。
盯着终端发了一会儿呆,突然冒出一个念头:要不趁假期学一下 Rust 吧,搭个环境,写个小项目练手。
但写什么呢?写个 Hello World 太无聊,写个 To-Do List 太简单。脑子里闪过一个想法:下载器。不是简单地调个 HTTP 请求,而是做成并发的、支持断点续传的、有进度条的------一个真正能用的命令行下载工具。
假期还剩最后两天,说干就干。打开 VS Code,新建文件夹,cargo init,故事就开始了。
架构设计:把下载抽象成状态机
在写代码之前,我先花了点时间想清楚整体设计。一个下载任务的生命周期其实很清晰,天然适合用状态机来建模:
markdown
Pending → Downloading → Paused
↓ ↓
↓ Downloading ←─┘
↓ ↓
└──────→ Completed
↑
Failed ────┘ (重试)
每个下载任务就是一个事件(Event) ,包含 URL、目标路径、字节范围、重试次数和当前状态。事件通过 mpsc channel 分发给 Worker 池,每个 Worker 负责驱动状态机、执行下载、汇报进度。
断点续传的核心思路也很直接:如果下载中断,会留下一个 .part 临时文件。下次启动同一个任务时,读取 .part 的大小,通过 HTTP 的 Range 请求头告诉服务器从哪里继续传,把新数据追加到 .part 文件末尾,下载完成后再重命名为正式文件名。
环境搭建:Windows 上的第一个坑
我用的主力机是 Windows,Rust 环境还没装。第一件事就是去 rustup.rs 下载安装器。
装完兴冲冲地在终端敲下 cargo new,结果被泼了一盆冷水:
go
error: linker `link.exe` not found
Rust 在 Windows 上需要 Microsoft C++ Build Tools 来提供链接器。解决方法是:
- 在 rustup 安装过程中,当它提示缺少工具链时,选择快速安装(输入
1回车) - 或者手动去下载 Visual Studio Build Tools,勾选"使用 C++ 的桌面开发"工作负荷
装好之后 cargo run 跑出 Hello, world! 的那一刻,才算是正式踏上了 Rust 之旅。
核心实现:一步步把下载器写出来
整个项目分了五个模块,每个模块职责清晰:
event.rs:状态机
用 Rust 的 enum 定义下载状态,用 struct 封装事件数据。状态转换方法(start、pause、complete、fail)保证状态流转的正确性。
rust
pub enum DownloadState {
Pending,
Downloading { progress: u64 },
Paused { progress: u64 },
Completed,
Failed { error: String },
}
downloader.rs:HTTP 下载
负责实际的网络请求和文件写入。用 reqwest 发 GET 请求,如果已有部分下载就带上 Range: bytes={已下载字节}- 头。服务器返回 206 Partial Content 时,说明断点续传生效了。
数据以 8KB 缓冲区逐块写入 .part 文件,每写入一块就更新 event.state 里的 progress,同时回调进度更新函数,让外部能实时知道下载了多少。
worker.rs:并发 Worker 池
每个 Worker 是一个独立的线程(后来改成了 tokio 异步任务),从共享的 Arc<Mutex<Receiver>> 里取事件,调用 downloader::download 执行下载,然后根据状态判断是否需要重试。
重试逻辑是整个项目中调整最多的部分。最初我用 Failed 状态来判断是否需要重试,但问题在于:fail() 方法在 retries < max_retries 时会把状态设回 Pending,而不是 Failed。这就导致 Worker 永远看不到 Failed 状态,重试分支根本进不去。
修复方法是把判断条件从"是否是 Failed 状态"改成"是否不是 Completed 状态",再加上 retries < max_retries 的限制。
rust
if !matches!(&event.state, DownloadState::Completed) {
if event.retries < event.max_retries {
// 重试逻辑
}
}
reporter.rs:进度条
用 indicatif 库给每个下载任务创建一个进度条,支持实时更新和完成/失败状态展示。这里踩了一个小坑:Receiver 不能 clone,只能用 Arc<Mutex<Receiver>> 在多个 Worker 间共享。
main.rs:命令行入口
用 clap 的 derive 模式解析命令行参数:
bash
auto-download -t 4 -o downloads https://example.com/file1.iso https://example.com/file2.iso
支持自定义线程数、输出目录,以及多个 URL 同时下载。
踩坑记录:Rust 编译器教会我的事
整个开发过程中遇到了不少问题,每一个都值得记录下来:
1. Receiver 不能 clone
std::sync::mpsc::Receiver 是独占的,不能直接 clone 给多个线程。标准解法是用 Arc<Mutex<Receiver>> 包一层,让多个 Worker 安全地共享。这也意味着每次取事件都要先 lock,会有轻微的性能开销。
2. Cargo.toml 的 edition 版本
我本地装完 Rust 后 cargo new 自动生成的 edition = "2021",但在某个阶段我手动改成了 "2024",结果旧版 Cargo 不认这个版本。改回 "2021" 解决。
3. SSL 证书验证失败
在 Windows 上下载测试文件时遇到了证书错误。原因是系统时间不准或根证书过期。临时解决是在 reqwest 客户端构建时加 .danger_accept_invalid_certs(true),但正式使用时应该去掉并修复系统证书。
4. 测试 URL 的全员 404
我用 Ubuntu ISO 镜像地址测试下载,结果试了三个官方链接都返回 404。后来才意识到这些镜像站的路径里包含具体版本号,而且可能有反爬机制。最后还是用 proof.ovh.net 的测试文件才跑通。
5. fail() 状态和重试判断的 bug
前面提到过,这是整个项目调试最久的逻辑问题。fail() 把状态设成 Pending,但 Worker 却在等 Failed 状态------典型的"我以为你会这样,但你没有"型 bug。
异步改造:从 blocking 到 tokio
最初为了快速跑通,我用的是 reqwest::blocking 同步版本。功能验证完后,决定改成异步的,提升并发效率。
改动点:
Cargo.toml中把reqwest的blockingfeature 去掉,加上tokio和futures-utildownloader.rs的download函数加上async,用response.bytes_stream()代替response.read()逐块读取worker.rs的thread::spawn换成tokio::spawn,thread::sleep换成tokio::time::sleepmain.rs的fn main()加上#[tokio::main]宏
异步改造本身没出什么大问题,Rust 的 async/await 语法和 tokio 生态已经很成熟了。唯一需要注意的是,std::sync::mpsc 仍然用于主线程和 Worker 之间的任务分发,因为它和异步运行时是兼容的。
成品展示
最终的命令行界面:
vbnet
A concurrent downloader with resume support
Usage: auto-download.exe [OPTIONS] <URLS>...
Arguments:
<URLS>... 要下载的 URL 列表(至少一个)
Options:
-t, --threads <THREADS> Worker 线程数(默认 4) [default: 4]
-o, --output-dir <OUTPUT_DIR> 输出目录(默认当前目录) [default: .]
-h, --help Print help
下载过程带进度条:
ruby
Starting 2 worker(s) for 2 URL(s)
https://proof.ovh.net/files/10Mb.dat [==========================> ] 5.2 MB/10.0 MB
https://proof.ovh.net/files/100Mb.dat [=========> ] 23.1 MB/100.0 MB
[✅ COMPLETED] https://proof.ovh.net/files/10Mb.dat -> downloads\10Mb.dat (10.00 MB)
[✅ COMPLETED] https://proof.ovh.net/files/100Mb.dat -> downloads\100Mb.dat (100.00 MB)
--- Done ---
Completed: 2, Failed: 0
失败自动重试:
less
[Worker 0] retrying (1/3) in 1s: https://proof.ovh.net/files/does_not_exist.dat
[Worker 0] retrying (2/3) in 1s: https://proof.ovh.net/files/does_not_exist.dat
[❌ FAILED] https://proof.ovh.net/files/does_not_exist.dat - HTTP error: 404 Not Found
项目仓库:github.com/VaneBlien/a... 👈 欢迎 Star ⭐
总结与展望
从一个无聊的五一假期念头,到产出一个有并发、断点续传、进度条、重试机制的命令行下载器,整个过程花了一天。Rust 的学习曲线确实陡峭,但编译器在编译阶段挡掉的 bug,比我在其他语言里跑出来的运行时 bug 加起来还多。
这个项目还有很多可以继续完善的方向:
- 暂停/恢复的交互控制(
Paused状态已经在状态机里定义好了,只是还没接上控制信号) - 下载速度限制
- 更丰富的进度展示(比如同时显示速度和预计剩余时间)
- 支持 Metalink 或 torrent