五一假期无聊?我用 Rust 手搓了一个并发下载器

五一假期的周末,外面人山人海,不如宅家用 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 来提供链接器。解决方法是:

  1. 在 rustup 安装过程中,当它提示缺少工具链时,选择快速安装(输入 1 回车)
  2. 或者手动去下载 Visual Studio Build Tools,勾选"使用 C++ 的桌面开发"工作负荷

装好之后 cargo run 跑出 Hello, world! 的那一刻,才算是正式踏上了 Rust 之旅。


核心实现:一步步把下载器写出来

整个项目分了五个模块,每个模块职责清晰:

event.rs:状态机

用 Rust 的 enum 定义下载状态,用 struct 封装事件数据。状态转换方法(startpausecompletefail)保证状态流转的正确性。

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 中把 reqwestblocking feature 去掉,加上 tokiofutures-util
  • downloader.rsdownload 函数加上 async,用 response.bytes_stream() 代替 response.read() 逐块读取
  • worker.rsthread::spawn 换成 tokio::spawnthread::sleep 换成 tokio::time::sleep
  • main.rsfn 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
相关推荐
该昵称用户已存在3 小时前
从边缘计量到碳足迹追踪:MyEMS 开源一体化架构的全栈拆解
架构·开源
xmdy58664 小时前
Flutter+开源鸿蒙实战|智安盾电商溯源平台Day1 项目搭建与整体方案拆解
flutter·开源·harmonyos
该昵称用户已存在4 小时前
以开源筑基,架构先行——深度拆解 MyEMS 微服务能源管理系统的技术内核
微服务·架构·开源
Hommy886 小时前
【开源剪映小助手】字幕接口
开源·github·aigc·剪映小助手·视频剪辑自动化
乱世刀疤8 小时前
cc-witch-web,已开源!实现OpenClaw、Claude Code等Agent的大模型便捷快速切换
人工智能·开源
Python私教9 小时前
Pure-Admin-Thin 深度解析:完整版和精简版到底怎么选?
vue.js·人工智能·开源
辭七七10 小时前
2026年4款热门龙虾工具实测:ToDesk AI、WorkBuddy等深度横评
开源
xmdy586610 小时前
Flutter+开源鸿蒙实战|智联邻里Day6 引入GetX全局架构+升级版下拉刷新+Toast弹窗+网络状态监听
flutter·开源·harmonyos