高级3-4 小时·系统编程

并发文件下载器

使用 Tokio + reqwest 构建一个支持多线程下载、断点续传、进度显示的下载工具

Rusttokioreqwestconcurrencydownload

并发文件下载器

使用 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

最佳实践

  1. 分片下载:大文件分成多个小块并行下载
  2. 断点续传:记录已下载的分片,支持中断后继续
  3. 进度显示:使用 indicatif 显示下载进度
  4. 错误重试:网络失败时自动重试

常见问题

Q: 如何支持断点续传? A:记录已下载的分片信息到文件,下次启动时检查并跳过已完成的分片。

Q: 如何限制下载速度? A: 使用 tokio::time::interval 控制下载速率。

Q: 如何处理大文件? A: 使用流式写入,避免一次性加载到内存。

扩展挑战

  1. 添加下载速度限制
  2. 支持多文件批量下载
  3. 实现下载队列

相关课程

课程相关章节
Rust异步编程:async/await
RustTokio 运行时基础
Rust并发编程基础
Rust常用集合:Vec, String, HashMap