1、引言
Rust 近年来在系统编程与服务端领域迅速被采用。它将"高性能"与"内存安全"结合,并通过所有权/借用系统在编译期避免大量常见错误。本文希望通过一个实战案例,带你体验 Rust 在性能与安全之间取得的完美平衡。
2、为什么选择 Rust?
Rust 是一种兼具性能与安全性的系统编程语言,它在没有垃圾回收(GC)的前提下,通过"所有权系统"和"借用检查器",在编译期保障了内存安全 与线程安全。
Rust 的三大核心优势:
| 特性 | Rust 优势说明 |
|---|---|
| 高性能 | 零成本抽象、无运行时开销、接近 C/C++ 的执行效率 |
| 内存安全 | 编译期防止悬垂指针、空指针等内存问题 |
| 并发可靠 | 所有权系统确保多线程资源访问安全 |
在实际工程中,这意味着更少的崩溃、更高的吞吐、更强的稳定性。
3、项目目标
目标:实现一个在 Windows 上可运行的高性能并发下载器,支持多线程分块下载、可视化进度条、平均速度显示、简单的断点续传提示(保存进度信息以便后续手工/自动恢复)、以及优雅中断(Ctrl-C)处理。
4、高层设计与流程图

设计要点:避免在运行时出现数据竞争(使用
Arc+Mutex或原子变量);通过Range请求实现断点下载;在 Windows 下注意文件句柄与路径的差异。
5、项目创建
本项目使用 VScode 运行代码。
5.1 前置准备
环境搭建可以参考这位大佬的从零开始的vscode配置及安装rust教程_vscode rust-CSDN博客
5.2 项目创建
5.2.1 通过 VS Code 终端创建
- 打开 VS Code,新建终端(点击顶部菜单栏「终端 → 新建终端」,或按 `Ctrl+``)。

- 在终端中切换到你想存放项目的目录(例如存放在「D:\code\Rust\projects」文件夹):
rust
cd D:\code\Rust\projects
- 执行
<font style="color:rgb(0, 0, 0);">cargo new 项目名</font>命令创建项目(项目名只能包含字母、数字、下划线,且不能以数字开头):
rust
cargo new downloaded_file
- 执行成功后,终端会输出提示:
Creating binary (application)
downloaded_filepackage

此时项目目录结构已自动生成,如下:

6、完整代码
说明:代码已尽量兼容 Windows。我们在代码中实现:
- 多线程分块下载(
THREADS可配置)- 进度条与下载速度显示(
indicatif)- Ctrl-C 优雅中断(保存当前位置到一个小的
.progress文件)- 简单的限速(通过在循环中 sleep 控制)
rust
// 定义全局错误类型
type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync + 'static>>;
use std::{
fs::OpenOptions,
io::{Seek, SeekFrom, Write},
path::Path,
sync::{
atomic::{AtomicU64, Ordering},
Arc,
},
time::{Duration, Instant},
};
use futures_util::StreamExt;
use indicatif::{ProgressBar, ProgressStyle};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use tokio::signal;
use tokio::sync::Mutex;
const THREADS: usize = 4; // 可根据需要调整
const PROGRESS_FILE: &str = "downloaded_file.progress.json";
#[derive(Serialize, Deserialize, Debug, Default)]
struct ProgressInfo {
// 已下载的总字节数
downloaded: u64,
total: u64,
}
#[tokio::main]
async fn main() -> Result<()> {
// ====== 配置(可改) ======
let url = "https://registry.npmmirror.com/-/binary/git-for-windows/v2.25.0.windows.2/mingw-w64-git-2.25.0.windows.2-1.src.tar.gz"
.to_string();
let filename = "downloaded_file.bin".to_string();
let max_speed_bytes_per_sec: Option<u64> = None; // Some(2_000_000) 限速示例
// ==========================
let client = Client::new();
// 1) HEAD 获取文件大小
let resp = 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);
println!("📦 文件总大小: {} bytes", total_size);
// 2) 尝试读取进度文件(如果存在)
let mut resume_downloaded: u64 = 0;
if Path::new(PROGRESS_FILE).exists() {
if let Ok(text) = std::fs::read_to_string(PROGRESS_FILE) {
if let Ok(pi) = serde_json::from_str::<ProgressInfo>(&text) {
if pi.total == total_size {
resume_downloaded = pi.downloaded;
println!(
"🔁 发现进度文件,已记录已下载 {} bytes,可尝试继续下载",
resume_downloaded
);
} else {
println!("⚠️ 进度文件与当前资源大小不一致,忽略进度文件。");
}
}
}
}
// 3) 准备目标文件
let mut file = OpenOptions::new()
.create(true)
.read(true)
.write(true)
.open(&filename)?;
let current_len = file.metadata()?.len();
if current_len < total_size {
file.set_len(total_size)?; // 预分配文件大小
}
let file = Arc::new(Mutex::new(file));
// 4) 进度条
let pb = ProgressBar::new(total_size);
pb.set_style(
ProgressStyle::default_bar()
.template("{msg} [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta}) {bytes_per_sec}")
.unwrap(),
);
pb.set_message("下载中");
pb.set_position(resume_downloaded);
// 5) 原子计数器(跨线程累计已下载)
let downloaded = Arc::new(AtomicU64::new(resume_downloaded));
// 6) Ctrl-C 处理器:优雅保存进度并退出
let downloaded_for_signal = downloaded.clone();
let total_size_clone = total_size; // 克隆用于信号处理线程(避免生命周期问题)
tokio::spawn(async move {
signal::ctrl_c().await.expect("failed to listen for ctrl-c");
let v = downloaded_for_signal.load(Ordering::Relaxed);
let pi = ProgressInfo {
downloaded: v,
total: total_size_clone,
};
let _ = std::fs::write(
PROGRESS_FILE,
serde_json::to_string_pretty(&pi).unwrap(),
);
println!(
"\n🛑 已捕获 Ctrl-C,进度已保存到 {} ({} bytes)",
PROGRESS_FILE, v
);
std::process::exit(0);
});
let start_time = Instant::now();
let mut handles = vec![];
for i in 0..THREADS {
let client = client.clone();
let file = file.clone();
let pb = pb.clone();
let downloaded = downloaded.clone();
let url = url.clone();
let total_size = total_size;
let max_speed = max_speed_bytes_per_sec;
// 每个线程的字节范围
let start = (total_size / THREADS as u64) * i as u64;
let end = if i == THREADS - 1 {
total_size - 1
} else {
(total_size / THREADS as u64) * (i as u64 + 1) - 1
};
let handle: tokio::task::JoinHandle<Result<()>> = tokio::spawn(async move {
let range_header = format!("bytes={}-{}", start, end);
let mut resp = client
.get(&url)
.header("Range", range_header)
.send()
.await
.map_err(|e| format!("reqwest error: {}", e))?;
let mut stream = resp.bytes_stream();
let mut pos = start;
// 简单限速实现:统计本线程已读字节,然后 sleep
let mut last_instant = Instant::now();
let mut bytes_in_period: u64 = 0;
let period = Duration::from_millis(200);
while let Some(item) = stream.next().await {
let chunk = item.map_err(|e| format!("stream error: {}", e))?;
{
let mut file = file.lock().await;
file.seek(SeekFrom::Start(pos))?;
file.write_all(&chunk)?;
}
let len = chunk.len() as u64;
pos += len;
downloaded.fetch_add(len, Ordering::Relaxed);
pb.inc(len);
// 限速(粗略):如果设置了 max_speed,则在短周期内控制速率
if let Some(max_bps) = max_speed {
bytes_in_period += len;
let elapsed = last_instant.elapsed();
if elapsed >= period {
let cur_bps = (bytes_in_period as f64) / elapsed.as_secs_f64();
if cur_bps > (max_bps as f64) {
// 计算需要 sleep 的时间
let need_sleep = Duration::from_secs_f64(
(bytes_in_period as f64 / max_bps as f64) - elapsed.as_secs_f64(),
);
tokio::time::sleep(need_sleep).await;
}
bytes_in_period = 0;
last_instant = Instant::now();
}
}
}
Ok(())
});
handles.push(handle);
}
// 等待所有线程完成
for h in handles {
h.await??;
}
pb.finish_with_message("✅ 下载完成");
// 删除进度文件(下载完全)
if Path::new(PROGRESS_FILE).exists() {
let _ = std::fs::remove_file(PROGRESS_FILE);
}
let elapsed = start_time.elapsed();
let total_downloaded = downloaded.load(Ordering::Relaxed);
let speed = (total_downloaded as f64 / elapsed.as_secs_f64()) / 1024.0 / 1024.0;
println!(
"\n📥 下载完成: {}\n⏱️ 总耗时: {:.2}s\n🚀 平均速度: {:.2} MB/s",
filename,
elapsed.as_secs_f64(),
speed
);
Ok(())
}
6.1 添加依赖
代码用到了 6 个外部库,执行以下命令一键添加(确保终端在项目根目录 <font style="color:rgba(0, 0, 0, 0.85);">downloaded_file/</font> 下):
rust
cargo add tokio@1.0 --features full # 异步运行时(核心,支持 async/await)
cargo add reqwest@0.12 --features stream # HTTP 客户端(用于下载文件)
cargo add futures-util@0.3 # 异步流处理(StreamExt trait 所在)
cargo add indicatif@0.17 # 进度条显示(ProgressBar/ProgressStyle)
cargo add serde@1.0 --features derive # 序列化/反序列化(Deserialize/Serialize)
cargo add serde_json@1.0 # JSON 解析(from_str/to_string_pretty)

命令说明:
<font style="color:rgb(0, 0, 0);">cargo add 库名@版本</font>:自动将依赖添加到<font style="color:rgb(0, 0, 0);">Cargo.toml</font>,并下载对应版本(版本号用<font style="color:rgb(0, 0, 0);">@x.y</font>指定,确保兼容性)。<font style="color:rgb(0, 0, 0);">--features</font>:启用库的必要功能(比如<font style="color:rgb(0, 0, 0);">tokio</font>需要<font style="color:rgb(0, 0, 0);">full</font>特性支持信号处理、线程池;<font style="color:rgb(0, 0, 0);">reqwest</font>需要<font style="color:rgb(0, 0, 0);">stream</font>支持流式下载)。<font style="color:rgb(0, 0, 0);">serde</font>的<font style="color:rgb(0, 0, 0);">derive</font>特性:允许使用<font style="color:rgb(0, 0, 0);">#[derive(Serialize, Deserialize)]</font>自动生成序列化代码。
7、逐段代码讲解
7.1 HEAD 请求与文件大小
通过 client.head(url).send().await 获取 content-length。如果服务器不支持 content-length(例如 Transfer-Encoding: chunked),本示例会把 total_size 置为 0------这时分块下载就不适用,需要采用流式下载。
7.2 进度持久化(简单策略)
使用了 serde 用于保存一个非常简单的进度文件(存储已下载总字节数和资源总大小),这主要用于:当用户按 Ctrl-C 手动中断时,程序能把当前总体进度保存到磁盘(.progress.json),下次运行时程序会检查该文件并打印提示(自动精确恢复每个分块需要更复杂的策略,本示例采取了较简单且通用的提示/记录策略)。
我们保存了一个 JSON 文件,结构:
{
"downloaded": 123456,
"total": 104857600
}
这不是精确的分块恢复信息,但在用户中断时可以作为恢复提示 。如果要完全自动化的断点续传 ,需在磁盘上记录每个分块是否完成(或使用临时分块文件)。那会稍微复杂一些,但原理相同。
7.3 文件预分配
file.set_len(total_size) 在 Windows 上会创建一个具有指定大小的稀疏文件(如果文件系统支持),避免运行时频繁扩容带来的性能波动。
7.4 并发与写入
我们采用 Arc<Mutex<File>> 简单地让多协程串行化文件写入。这个策略在 I/O 较快时可能成为瓶颈。如果你想进一步优化:可以为每个分块打开独立的文件句柄(Windows 下也支持),或者将写入任务放到单独的写线程,通过通道接收已下载块再写磁盘。
7.5 限速实现说明
限速使用了一个非常粗糙的周期统计(200ms),根据短周期内的数据量和预期速率决定是否 sleep。它不是非常精确,但对于大多数用途已经足够。如果你需要更精确的速率控制,建议使用令牌桶(token bucket)算法。
7.6 Ctrl-C 捕获
使用 tokio::signal::ctrl_c() 监听中断信号,保存当前已下载字节数到 .progress.json 并优雅退出。Windows 下该方法同样可用。
8、结果展示
方式一:先编译再运行 **<font style="color:rgb(0, 0, 0);">cargo build</font>**+ 执行产物
先编译生成可执行文件,再手动运行(适合需要重复运行、分发程序给他人,或测试编译产物时)。
- 编译代码
D:\code\Rust\projects\downloaded_file> cargo build --release

- 运行时进度条终端截图

- 若中断并恢复。

- 下载完成后,显示文件属性的截图

方式二:****开发调试(最常用)------ **<font style="color:rgb(0, 0, 0);">cargo run</font>**
直接编译 + 运行,自动处理依赖和编译,适合开发时快速测试代码(支持热修改后重新运行)。
D:\code\Rust\projects\downloaded_file> cargo run --release


9、实验与性能对比(Rust vs Python 简单测试)
我们可以设计一个和 Rust 下载器功能类似的 Python 并发下载脚本 ,用于性能对比测试。
为了公平对比,我们采用:
- 相同的下载文件(例如:https://registry.npmmirror.com/-/binary/git-for-windows/v2.25.0.windows.2/mingw-w64-git-2.25.0.windows.2-1.src.tar.gz)(最好是大于 100MB 的大文件,这样才能体现并发下载的优势)
- 相同的线程数(例如:4)
- 统计总耗时与平均下载速度(MB/s)
9.1 Python 对照实验代码
⚠️ 说明:此代码能在 Windows 上直接运行(需安装 requests 库)。
Python 版本建议:3.8+
python
import os
import threading
import time
import requests
URL = "https://registry.npmmirror.com/-/binary/git-for-windows/v2.25.0.windows.2/mingw-w64-git-2.25.0.windows.2-1.src.tar.gz"
FILENAME = "download_py.bin"
THREADS = 4
def get_file_size(url):
try:
r = requests.head(url, allow_redirects=True)
if "content-length" in r.headers:
return int(r.headers["content-length"])
else:
print("⚠️ HEAD 请求未返回 content-length,尝试 GET...")
r = requests.get(url, stream=True)
return int(r.headers.get("content-length", 0))
except Exception as e:
print("⚠️ 无法通过 HEAD 获取文件大小:", e)
return 0
def download_range(start, end, index):
headers = {'Range': f'bytes={start}-{end}'}
response = requests.get(URL, headers=headers, stream=True)
with open(FILENAME, "r+b") as f:
f.seek(start)
f.write(response.content)
print(f"线程 {index} 下载完成")
def main():
total_size = get_file_size(URL)
print(f"📦 文件总大小: {total_size} bytes")
if total_size < 1024 * 1024:
print("⚠️ 可能未正确获取文件大小,请检查 URL 或代理设置。")
return
with open(FILENAME, "wb") as f:
f.truncate(total_size)
part_size = total_size // THREADS
threads = []
start_time = time.time()
for i in range(THREADS):
start = part_size * i
end = total_size - 1 if i == THREADS - 1 else (start + part_size - 1)
t = threading.Thread(target=download_range, args=(start, end, i))
threads.append(t)
t.start()
for t in threads:
t.join()
elapsed = time.time() - start_time
speed = total_size / (1024 * 1024) / elapsed
print(f"\n📥 下载完成: {FILENAME}")
print(f"⏱️ 总耗时: {elapsed:.2f}s")
print(f"🚀 平均速度: {speed:.2f} MB/s")
if __name__ == "__main__":
main()
9.2 结果展示

这里给出一个非常简化的对比思路,真实对比请在相同机器、相同网络环境下多次取平均。
- Python 测试用例:
requests+threading,由于 GIL,CPU 密集型或大量小请求并发性能差。对于 I/O 密集型任务,Python 仍然能完成任务,但并发吞吐通常低于 Rust。 - Rust:并发真实利用多核(runtime + OS 线程),无 GC 干扰,网络与写入延迟在高负载时表现更稳定。
| 指标 | Rust(本示例) | Python (requests+threading) |
|---|---|---|
| 平均下载速度(示例) | ~15--40 MB/s(依网络) | ~5--12 MB/s |
| 稳定性 | 高(编译期保证) | 依赖运行时与库 |
| 复杂性 | 代码稍复杂但可维护 | 更简单但风险在运行时 |
9.3 总结
Rust 的性能优势主要来自:
- 原生异步 I/O + 无 GIL 限制;
- 零成本抽象 + 无解释器;
- 多线程真正并行(Python 的 GIL 会限制 CPU 并发)。
10、结语: Rust,让系统编程再次充满魅力
Rust 的出现,并不仅仅是多了一门"语法新潮"的语言,而是重新定义了 系统级开发的安全与效率平衡点 。
通过本文的实践案例,我们从多个角度验证了 Rust 的核心价值:
- 🧠 内存安全:借助所有权与生命周期机制,Rust 在无 GC 的前提下彻底消除了悬垂指针与内存泄漏问题。
- ⚙️ 高性能:编译为原生代码、零成本抽象,使其在 CPU 密集或网络密集型任务中接近 C/C++ 的性能。
- 🔒 并发可靠:语言层面对数据竞争进行静态检测,让多线程不再是"悬崖边起舞"。
- 🌐 生态日益完善:Cargo、Crates.io 以及 Tokio、Reqwest 等优秀库,使现代系统开发的门槛大幅降低。
相比之下,Python、Go、C++ 各有优势,但在综合安全性与性能的权衡中,Rust 的设计理念显得更具未来感。
尤其在高并发、云原生、区块链、嵌入式等领域,Rust 正逐步成为"安全高性能"的代名词。