rusty-cat 分片并发上传完全指南(Rust 小白版)
本文面向 刚接触 Rust、想快速把「大文件分片并发上传」跑起来 的开发者。 每段代码都可以直接复制到你的项目里编译运行,遇到需要替换的地方都写了中文注释。 配套阅读:《rusty-cat 分片并发下载完全指南》。
目录
- rusty-cat 是什么,能帮你解决什么问题
- 第一步:在 Cargo.toml 里加依赖
- 10 分钟跑通第一个上传(完整可运行程序)
- 核心概念:两个「并发」参数千万别搞混
- 开启「单文件分片并发」——
with_max_parts_in_flight - 四种云存储上传配方(各有独立实战文章)
- 从内存字节上传 / 自建服务器上传
- 进度、暂停、恢复、取消
- 断点续传:进程重启后接着传
- 内存占用与参数怎么选
- 错误处理与自动重试
- 常见问题排查表(FAQ)
- 附录:配置项速查表
1. rusty-cat 是什么,能帮你解决什么问题
rusty-cat 是一个 异步(async)的 Rust 文件传输 SDK,专门解决「大文件的可断点续传上传 / 下载」。它帮你处理这些又琐碎又容易出 bug 的事:
- 分片(chunk):把一个大文件切成很多小块分别上传;
- 并发(concurrent):同一个文件的多个分片可以同时上传,把带宽跑满;
- 重试 / 退避:网络抖动时自动重试,且用指数退避 + 抖动,不会「重试风暴」;
- 暂停 / 恢复 / 取消:随时控制任务;
- 进度回调:实时把进度百分比推给你的 UI;
- 断点续传:进程被杀掉、崩溃、重启后能接着传,而不是从头再来。
它 不帮你做 的事(这是刻意的设计,让 SDK 保持小巧、适配任意后端):
- 不内置数据库(要不要持久化任务记录、用什么库,你自己决定);
- 不管理你的云密钥 / 权限(你从自己的后端提供)。
支持的上传后端:普通 HTTP、阿里云 OSS(直传 / 预签名)、Azure Blob(直传 / SAS)。
一句话记住它的用法:创建
MeowClient→ 用 Builder 构建一个「上传任务」→ 提交给客户端 → 收进度回调 → 用完close()。
2. 第一步:在 Cargo.toml 里加依赖
rusty-cat 用「feature(特性开关)」来控制引入哪些云厂商的代码,你只需要开启你用到的那一个,包体积和依赖都最小。
[dependencies]
# 按需选一个(或多个)云厂商特性:
rusty-cat = { version = "0.2.4", features = ["aliyun-oss-direct"] }
# rusty-cat = { version = "0.2.4", features = ["aliyun-oss-presigned"] }
# rusty-cat = { version = "0.2.4", features = ["azure-blob-direct"] }
# rusty-cat = { version = "0.2.4", features = ["azure-blob-sas"] }
# rusty-cat 是 async 库,需要一个异步运行时,这里用 tokio:
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
各个 feature 的含义:
| Feature | 用途 | 谁持有密钥 |
|---|---|---|
aliyun-oss-direct | 阿里云 OSS 直传,客户端直接用 AccessKey 签名 | 客户端(受信任的环境,如你自己的服务器) |
aliyun-oss-presigned | 阿里云 OSS 预签名,后端签好短期 URL,客户端只负责传 | 后端 |
azure-blob-direct | Azure Blob 直传,用账号 Shared Key 签名 | 客户端(受信任环境) |
azure-blob-sas | Azure Blob SAS,后端签好短期 SAS URL | 后端 |
怎么选? 如果你的程序跑在不完全受信任的环境(比如用户的手机、桌面 App、浏览器端 WASM),优先用预签名 / SAS:密钥永远留在后端,客户端只拿到几分钟有效的临时 URL。只有当程序跑在你完全掌控的服务器上时,才考虑直传(direct)。
3. 10 分钟跑通第一个上传(完整可运行程序)
下面是一个完整的 main.rs,演示把本地文件分片并发上传到阿里云 OSS。把顶部四个常量换成你自己的,cargo run 即可。
需要在 Cargo.toml 里开启
features = ["aliyun-oss-direct"]。
use std::sync::Arc;
use std::time::Duration;
use rusty_cat::aliyun_oss_direct::AliOssDirectUpload;
use rusty_cat::api::{FileTransferRecord, MeowClient, MeowConfig, UploadPounceBuilder};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// ===== 改成你自己的信息 =====
let bucket = "my-bucket"; // OSS Bucket 名
let access_key_id = "AK_ID"; // AccessKeyId
let access_key_secret = "AK_SECRET"; // AccessKeySecret
let region = "cn-beijing"; // 地域,例如 cn-beijing / cn-hangzhou
let local_file = "./big-object.bin"; // 要上传的本地文件
let object_url = // 上传到 OSS 后的对象完整 URL
"https://my-bucket.oss-cn-beijing.aliyuncs.com/test/big-object.bin";
// ============================
// 1) 创建配置和客户端
let config = MeowConfig::builder()
.max_upload_concurrency(3) // 同时最多传 3 个「文件」
.http_timeout(Duration::from_secs(30))
.build()?;
let client = MeowClient::new(config);
// 2) 准备上传协议(阿里云 OSS 直传)
let protocol =
AliOssDirectUpload::new(bucket, access_key_id, access_key_secret, region);
// 3) 构建上传任务
let task = UploadPounceBuilder::new(
"big-object.bin", // 展示用的文件名(回调 / 日志里能看到)
local_file, // 本地文件路径
1024 * 1024, // 分片大小 = 1 MiB
)
.with_url(object_url)
.with_breakpoint_upload(Arc::new(protocol)) // 协议对象必须放进 Arc
.with_max_parts_in_flight(4) // 同一个文件最多 4 片并发上传
.with_max_chunk_retries(3) // 每片失败最多再重试 3 次
.build()?;
// 4) 提交任务并等待它跑完(enqueue_and_wait 是最省事的方式)
let outcome = client
.enqueue_and_wait(task, |record: FileTransferRecord| {
println!(
"上传进度 {:.1}% 状态 {:?}",
record.progress() * 100.0,
record.status()
);
})
.await?;
println!(
"上传完成!task_id={} 服务端返回={:?}",
outcome.task_id, outcome.payload
);
// 5) 关闭客户端(这一步是必须的!见下方说明)
client.close().await?;
Ok(())
}
几个新手最容易忽略的点:
enqueue_and_wait(task, progress_cb)是最简单的提交方式:它提交任务、在每次进度更新时调用你的回调、并在任务到达「终态」时返回。成功返回TaskOutcome,失败 / 被取消会返回Err(MeowError),所以你直接用?就能处理错误。- 协议对象要放进
Arc(Arc::new(protocol)):因为并发上传时,同一个协议对象&self会被多个异步任务共享。 with_max_parts_in_flight(4)就是「单文件分片并发」开关,第 5 节详细讲。- 一定要调用
client.close().await:它会取消在途任务、把缓冲的回调发完、并优雅地关闭后台调度线程。不要依赖Drop自动清理——那只是兜底,会丢事件。
进度是个 0.0~1.0 的比例。
record.progress()返回f32,乘 100 就是百分比。没有「已传字节数」的直接读取方法,如需字节数请自己算:(f64::from(record.progress()) * record.total_size() as f64) as u64。
4. 核心概念:两个「并发」参数千万别搞混
这是新手 第一大坑。rusty-cat 有 两个完全独立 的并发旋钮:
| 旋钮 | 作用范围 | 默认值 | 控制什么 |
|---|---|---|---|
MeowConfig::builder().max_upload_concurrency(k) | 整个客户端 | 2 | 同时上传 几个文件 |
UploadPounceBuilder::with_max_parts_in_flight(n) | 单个文件内部 | 1 | 一个文件的 几个分片 同时传 |
max_upload_concurrency(k):客户端级别,控制多少个文件同时在传。上传和下载各自独立计数、互不占用。with_max_parts_in_flight(n):任务级别,控制同一个文件的多少个分片同时在传(这才是本文的主角)。
两者会相乘。如果 max_upload_concurrency(3),且每个文件都 with_max_parts_in_flight(4),那么客户端最多会有 3 × 4 = 12 个上传请求同时在飞。没有全局上限帮你兜底,请自己把乘积控制在合理范围。
🖼️ 【配图占位 U2|两个并发旋钮的层级关系】 建议配图:外层一个大框「客户端」,标
max_upload_concurrency(3),里面并排 3 个「文件」框;每个文件框内再画 4 个并行的「分片」块,标with_max_parts_in_flight(4)。图注:外层控制「几个文件」、内层控制「一个文件几个分片」,两者相乘 = 最多 12 个请求在飞。 临时 ASCII 草图:┌─ 文件A ─ 分片1 分片2 分片3 分片4 ← with_max_parts_in_flight(4) 客户端 ──────┼─ 文件B ─ 分片1 分片2 分片3 分片4 └─ 文件C ─ 分片1 分片2 分片3 分片4 ↑ max_upload_concurrency(3):最多 3 个文件同时传
5. 开启「单文件分片并发」——with_max_parts_in_flight
默认值是 1,也就是严格串行:一片传完再传下一片,和最经典的单流上传逐字节一致。传入大于 1 的值即开启并发。
什么时候该用?
- ✅ 适合:单个大文件(几十 MiB 以上)、走一条快网(单流跑不满带宽,比如服务器往对象存储推大对象)。
- ❌ 没必要:小文件、单流已经跑满带宽、或者你想省内存的场景,保持默认
1即可。
加速效果和你的文件大小、网络状况强相关,SDK 没有承诺任何固定的倍数。把它当作一个「可以试、要实测」的工具。
三个前置条件(并发闸门)
设了 with_max_parts_in_flight(8) 并不保证 真的并发。执行器只有在 同时满足以下三条 时才走并发路径:
n > 1(0会被归一化成1);- 文件总大小已知 —— 上传时总大小由本地文件 / 内存字节自动得到,所以上传天然满足这条;
- 上传协议是「并发安全」的(下表)。
如果条件 1 或 3 不满足,任务会静默回退到串行:不报错、不告警,照样成功,只是一片一片传。
🖼️ 【配图占位 U3|并发闸门决策流程图】 建议画一张流程图(判断框依次串联,任一为「否」就走串行):
n > 1?→否→ 串行;→是→总大小已知?→否→ 串行(上传天然满足);→是→协议并发安全?→否→ 串行;→是→ 并发。 临时 ASCII 草图:n>1? ──否──▶ 串行 │是 总大小已知? ──否──▶ 串行 │是 协议并发安全? ──否──▶ 串行 │是 ▼ 并发 ✅
哪些上传协议支持并发?
并发要求协议是「乱序安全」的:分片 N 不依赖分片 N-1 完成,且最后的「合并」步骤能按正确顺序拼装。
| 上传协议 | Feature | 并发安全? | 说明 |
|---|---|---|---|
AliOssDirectUpload | aliyun-oss-direct | ✅ 是 | OSS Multipart,分片号由偏移量推导,重传幂等,Complete 按序合并 |
PresignedMultipartUpload(阿里云预签名) | aliyun-oss-presigned | ✅ 是 | 预签名分片,按偏移记账,完成清单按分片号重排 |
AzureBlobDirectUpload | azure-blob-direct | ✅ 是 | Block Blob,block id 由分片下标决定,Put Block List 固定提交顺序 |
PresignedMultipartUpload(Azure SAS) | azure-blob-sas | ✅ 是 | 同样的 Block Blob 模型,走 SAS URL |
内置默认 HTTP 上传(DefaultStyleUpload) | 无 | ❌ 否 | 依赖服务端单一 nextByte 游标,必须严格顺序 |
你自己实现的 BreakpointUpload | 自定义 | ❌ 否(默认) | 除非你显式声明支持,否则默认串行 |
划重点:想要并发上传,就 给任务挂上上面四种 provider 上传协议之一,并设
n > 1。内置的默认 HTTP 上传永远串行。
6. 四种云存储上传配方(各有独立实战文章)
每种云存储的上传配方(含完整可运行程序、内部数据流讲解、断点续传要点)都拆成了独立的实战文章,方便你按需只看用到的那一个:
| 实战文章 | Feature | 谁持密钥 | 一句话 |
|---|---|---|---|
| 阿里云 OSS 直传直下 | aliyun-oss-direct | 客户端 | 可信服务器用 AccessKey 直连 OSS |
| 阿里云 OSS 预签名 | aliyun-oss-presigned | 后端 | 后端签短期 URL,客户端只传字节 |
| Azure Blob 直传直下 | azure-blob-direct | 客户端 | 可信服务器用 Shared Key 直连 Blob |
| Azure Blob SAS | azure-blob-sas | 后端 | 后端签短期 SAS URL |
四种配方共用同一套「构建协议 → with_breakpoint_upload(Arc::new(...)) → 设 with_max_parts_in_flight(4) → enqueue_and_wait」骨架,只有「怎么构建协议」不同。最小上手片段(以 OSS 直传为例):
let protocol = AliOssDirectUpload::new("my-bucket", "AK_ID", "AK_SECRET", "cn-beijing");
let task = UploadPounceBuilder::new("big-object.bin", "./big-object.bin", 1024 * 1024)
.with_url("https://my-bucket.oss-cn-beijing.aliyuncs.com/test/big-object.bin")
.with_breakpoint_upload(Arc::new(protocol))
.with_max_parts_in_flight(4) // ← 单文件并发开关
.build()?;
client.enqueue_and_wait(task, |r| println!("{:.1}%", r.progress() * 100.0)).await?;
🖼️ 【配图占位 U1|四种上传配方的密钥模型对比】 建议配图:左右两栏对比。左「direct(直连)」:客户端图标里有钥匙,直接连云;标注「仅限可信服务器」。右「presigned/SAS」:钥匙在后端图标里,客户端只从后端拿短期 URL、再连云;标注「适合手机/浏览器等不可信端」。 临时 ASCII 草图:
direct [可信服务器 🔑] ─────────▶ [云存储] presigned/SAS [客户端] ─要URL→ [后端 🔑] ─签URL→ [客户端] ─传字节→ [云存储]
并发上传时 SDK 的保证
在并发路径上,没有任何单个分片会去「结束」整个上传。每个分片只管把自己的字节传上去;Complete / Put Block List 这一步被提升到调度器层,只执行一次,且只在所有分片都落地、拼成连续前缀之后才执行。这就是乱序分片安全的原因——先传完的分片不会触发提前完成。
7. 从内存字节上传 / 自建服务器上传
7.1 上传内存里的字节(而不是本地文件)
如果你的数据在内存里(比如刚生成的一段 buffer),用 from_bytes 代替 new:
use std::sync::Arc;
use rusty_cat::aliyun_oss_direct::AliOssDirectUpload;
use rusty_cat::api::{MeowClient, MeowConfig, UploadPounceBuilder};
async fn upload_from_memory(
data: Vec<u8>, // 你内存里的数据
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let client = MeowClient::new(MeowConfig::default());
let protocol = AliOssDirectUpload::new("my-bucket", "AK_ID", "AK_SECRET", "cn-beijing");
// from_bytes(展示文件名, 字节, 分片大小)
let task = UploadPounceBuilder::from_bytes("in-memory.bin", data, 1024 * 1024)
.with_url("https://my-bucket.oss-cn-beijing.aliyuncs.com/test/in-memory.bin")
.with_breakpoint_upload(Arc::new(protocol))
.with_max_parts_in_flight(4)
.build()?;
client.enqueue_and_wait(task, |_r| {}).await?;
client.close().await?;
Ok(())
}
Vec<u8> 会被零拷贝地转成 bytes::Bytes,后续切片和克隆都只是引用计数 +1,不会复制大块内存。
7.2 上传到你自己的 HTTP 服务器
如果不用云厂商、而是传到你自己的服务器,有两种方式:
- 用内置默认协议:不挂任何 provider 协议,直接
build()。它走一套约定好的 multipart/form-data 分片请求格式(默认协议串行,不支持并发)。 - 自己实现
BreakpointUpload:完全控制每片的请求方法、URL、Header、Body。下面是一个最小示例,把每片用PUT发到某个 URL,并带上x-offset头告诉服务器写入位置:
use async_trait::async_trait;
use reqwest::header::{HeaderMap, HeaderValue};
use rusty_cat::api::{
BreakpointUpload, InnerErrorCode, MeowError, UploadChunkCtx, UploadPrepareCtx,
UploadResumeInfo,
};
#[derive(Clone)]
struct MyBinaryUpload {
url: String,
}
#[async_trait]
impl BreakpointUpload for MyBinaryUpload {
// prepare 在上传开始前调用一次,返回「从第几个字节续传」
async fn prepare(&self, _ctx: UploadPrepareCtx<'_>) -> Result<UploadResumeInfo, MeowError> {
Ok(UploadResumeInfo {
completed_file_id: None,
next_byte: Some(0), // 0 表示从头开始
provider_upload_id: None,
})
}
// 每个分片调用一次
async fn upload_chunk(&self, ctx: UploadChunkCtx<'_>) -> Result<UploadResumeInfo, MeowError> {
let mut headers = HeaderMap::new();
headers.insert(
"x-offset",
HeaderValue::from_str(&ctx.offset.to_string()).unwrap(),
);
let resp = ctx
.client // SDK 提供的、已按配置构建好的 reqwest::Client
.put(&self.url)
.headers(headers)
.body(reqwest::Body::from(ctx.chunk.clone()))
.send()
.await
.map_err(|e| {
MeowError::from_source(InnerErrorCode::HttpError, e.to_string(), e)
})?;
if !resp.status().is_success() {
return Err(MeowError::from_code(
InnerErrorCode::ResponseStatusError,
format!("上传返回状态 {}", resp.status()),
));
}
Ok(UploadResumeInfo {
completed_file_id: None,
next_byte: Some(ctx.offset + ctx.chunk.len() as u64),
provider_upload_id: None,
})
}
}
然后像 provider 协议一样挂上去(注意自定义协议默认串行):
let task = UploadPounceBuilder::new("http-upload.bin", "./http-upload.bin", 1024 * 1024)
.with_url("http://127.0.0.1:8080/upload")
.with_method(reqwest::Method::PUT)
.with_breakpoint_upload(Arc::new(MyBinaryUpload { url: "http://127.0.0.1:8080/upload".into() }))
.build()?;
8. 进度、暂停、恢复、取消
8.1 用 try_enqueue 拿到更细的回调(进度 + 完成)
enqueue_and_wait 适合「提交后一直等到结束」。如果你要构建 UI、需要在后台异步跑,用底层的 try_enqueue,它返回 TaskId,并让你分别处理进度回调和完成回调:
use std::sync::Arc;
use rusty_cat::aliyun_oss_direct::AliOssDirectUpload;
use rusty_cat::api::{
FileTransferRecord, MeowClient, MeowConfig, TaskId, TransferStatus, UploadPounceBuilder,
};
async fn upload_with_callbacks() -> Result<TaskId, Box<dyn std::error::Error + Send + Sync>> {
let client = MeowClient::new(MeowConfig::default());
let protocol = AliOssDirectUpload::new("my-bucket", "AK_ID", "AK_SECRET", "cn-beijing");
let task = UploadPounceBuilder::new("f.bin", "./f.bin", 1024 * 1024)
.with_url("https://my-bucket.oss-cn-beijing.aliyuncs.com/test/f.bin")
.with_breakpoint_upload(Arc::new(protocol))
.with_max_parts_in_flight(4)
.build()?;
let task_id = client
.try_enqueue(
task,
// 进度回调:每次进度更新都会调用
move |record: FileTransferRecord| match record.status() {
TransferStatus::Failed(err) => eprintln!("上传失败:{err}"),
TransferStatus::Canceled => println!("已取消"),
_ => println!("进度 {:.1}%", record.progress() * 100.0),
},
// 完成回调:任务成功到达 Complete 时调用一次
move |task_id, payload| {
println!("任务 {task_id} 完成,服务端返回 {payload:?}");
},
)
.await?;
// 保存好 task_id,后面用它来 暂停/恢复/取消
Ok(task_id)
}
record.status()返回的是&TransferStatus,可以直接match。TransferStatus的取值有:Pending(排队)、Transmission(传输中)、Paused(暂停)、Complete(完成)、Failed(err)(失败)、Canceled(取消)。
8.2 暂停 / 恢复 / 取消
拿到 TaskId 后就能控制任务:
client.pause(task_id).await?; // 暂停
client.resume(task_id).await?; // 从断点继续(TaskId 不变)
client.cancel(task_id).await?; // 取消(会跑 provider 的清理,比如 OSS 的 AbortMultipartUpload)
- 取消上传会触发协议的
abort_upload(例如 OSS 的AbortMultipartUpload),让未提交的分片停止占用存储、不再计费。 - 取消是尽力而为的、且不可逆——取消后想再传,就重新建一个任务。
8.3 并发下的进度行为(重要且反直觉)
即使分片是乱序完成的,SDK 只上报「最长连续前缀」的进度(业界叫 watermark,水位线):
- 进度是单调递增的,绝不回退;
- 如果靠后的分片先传完、但前面还有「空洞」,进度会原地不动,直到空洞被填上——这是正常现象,不是卡死;
- 每次水位线推进,你只会收到一次合并后的进度事件(不是每片一个),终态只有一次。
🖼️ 【配图占位 U4|水位线(watermark)进度模型】 建议配图:一条按分片切格的进度条。分片 1、2、4 已完成(涂实),分片 3 还是空洞。用一条竖线标出「水位线」停在分片 2 结尾处——尽管分片 4 已完成,进度仍只算到连续前缀的末尾。图注:进度 = 连续前缀 / 总大小,单调递增;空洞填上后水位线才前跳。 临时 ASCII 草图:
分片: [#1✅][#2✅][#3 ⬜空洞][#4✅] ▲ 水位线(进度只算到这里,即使 #4 已完成)
9. 断点续传:进程重启后接着传
rusty-cat 刻意不内置数据库,续传要靠你在自己的库里持久化足够的信息,重启后重建同一个逻辑任务再提交。要点:
- 建一张自己的传输表,字段至少包含:业务文件 ID、本地路径、远端 URL / 对象 key、方向(上传/下载)、分片大小、provider 类型、状态、进度等;
- 在进度回调里把
FileTransferRecord落库(不要在回调里做慢速 DB 写,最好丢给你自己的 worker 队列批量写); - 进程重启后,查出未完成的行,重建等价的
PounceTask; - 再次调用
try_enqueue(...),provider 协议会从本地 / 远端的断点信息继续。
各 provider 的续传细节:
- 阿里云 OSS 预签名:用
PresignedMultipartUpload::with_resumed_parts(...)把上次持久化的分片重新注入,prepare会跳过已验证的最长连续前缀、重传其余部分。 - 阿里云 OSS 直传:续传时 SDK 通过
ListMultipartUploads接管已有的 multipart 会话。建议持久化 provider 的UploadId(AliOssDirectUpload::current_upload_id()),并在 Bucket 上配一条AbortMultipartUpload生命周期规则,让废弃会话自动清理、不再计费。 - Azure 直传 / SAS:没有 upload id,续传靠恢复服务端的「未提交 block 列表」。未提交的 block 会计费到 Azure 垃圾回收(约 7 天),所以也要配生命周期规则。
还有一个「先导入为暂停态、再让用户选择性开始」的高级用法:用
try_enqueue_paused(...)把任务导入成Paused状态,不发任何网络 / 文件 IO,之后对用户选中的任务调resume(task_id)即可。适合「重启后恢复一堆任务,但只启动用户勾选的那几个」。
10. 内存占用与参数怎么选
每个在途分片都需要一个缓冲区,所以:
单个文件的峰值传输内存 ≈
n × 分片大小(n=max_parts_in_flight)
放到整个客户端,内存要对同时在传的文件求和:
客户端峰值内存 ≈ max_upload_concurrency × 上传 n × 上传分片大小
+ max_download_concurrency × 下载 n × 下载分片大小
SDK 没有全局内存上限,n 无限大内存就无限大,请务必控制。实用建议:
- 分片大小保持在 1–8 MiB(对象存储常见区间)。太小则请求开销大;太大则每次重试要重发更多数据、且抬高内存下限。
n从 4 开始试,大文件 + 快网时用。再往上调,等带宽跑满后收益很小,内存却翻倍。- 记得乘以文件级并发:
4个文件 ×n=8×4 MiB= 最多128 MiB缓冲。
11. 错误处理与自动重试
大多数 SDK 调用返回 Result<_, MeowError>。MeowError 上有两个关键方法:
MeowError::code():稳定的数字错误码(i32,适合结构化日志 / FFI);MeowError::msg():给人看的错误描述。
分支判断请用 code(),不要去匹配错误文本。常见错误码:
| 码 | 名称 | 什么时候发生 |
|---|---|---|
102 | ParameterEmpty | 入队时必填项(URL、文件名、非零大小)为空 |
103 | DuplicateTaskError | 同一个文件 / 任务已经在排队、运行或暂停 |
107 | ClientClosed | 客户端已关闭,请新建一个 MeowClient |
111 | CommandSendFailed | 命令队列满了(快速失败),退避重试或调大 command_queue_capacity |
120 | TaskCanceled | 任务在完成前被取消 |
121 | DiskFull | 写下载文件时磁盘满 |
122 | LocalFileRemoved | 传输过程中本地源 / 目标文件被删了 |
自动重试是怎么工作的
两个旋钮控制重试次数:
with_max_chunk_retries(n):单个分片首次失败后额外重试的次数(默认3,0关闭);with_max_upload_prepare_retries(n):上传 prepare 阶段首次失败后额外重试的次数(默认3)。
在这个预算内,SDK 只重试「瞬时(transient)传输错误」——连接被重置、超时、消息不完整、以及 HTTP 408/429/5xx——并在两次尝试之间用指数退避(基准 200ms、上限 5000ms)+ ±20% 抖动等待,避免重试风暴。非瞬时错误(如 403、格式错误的响应、无效 Range)快速失败,不消耗重试预算,因为重试也没用。
12. 常见问题排查表(FAQ)
| 现象 | 可能原因 | 解决 |
|---|---|---|
设了 n > 1 但还是串行 | 三个前置条件没满足 | 确认 n > 1、协议并发安全、且总大小已知(上传天然满足总大小) |
| 上传没并发 | 用的是内置默认 HTTP 上传 / 自定义协议 | 挂上四种 provider 上传协议之一(direct / presigned / SAS) |
| 进度卡住不动然后突然跳 | 靠后的分片先传完、前面有空洞 | 正常现象,进度按连续前缀单调推进,不是卡死 |
入队大量任务时报 CommandSendFailed | 命令队列满了(try_enqueue 快速失败) | 调大 command_queue_capacity,或自己退避重试 |
报 DuplicateTaskError | 同一个文件 / URL 的任务已经存在 | 别重复提交;或先取消旧任务 |
关闭后再提交报 ClientClosed | 客户端 close() 之后不可复用 | 新建一个 MeowClient |
忘了调 close(),事件丢失 / 线程不退 | 依赖了 Drop 兜底 | 关机流程里显式 client.close().await |
13. 附录:配置项速查表
MeowConfig(客户端级配置)
用 MeowConfig::default() 拿安全默认值,或用 MeowConfig::builder() 定制后 build()?。配置在客户端创建后不可变。
| 参数 | 默认 | 约束 | 说明 |
|---|---|---|---|
max_upload_concurrency | 2 | >= 1 | 同时上传的文件数 |
max_download_concurrency | 2 | >= 1 | 同时下载的文件数 |
http_timeout | 5s | 正数 | 内部构建 HTTP 客户端的单请求超时 |
tcp_keepalive | 30s | 正数 | TCP keepalive |
command_queue_capacity | 128 | >= 1 | 入队 / 暂停 / 恢复 / 取消 等命令队列容量 |
worker_event_queue_capacity | 256 | >= 1 | 进度 / 状态事件队列容量 |
http_client | 无 | reqwest::Client | 可注入自定义 HTTP 客户端(代理、TLS、默认头、埋点等) |
UploadPounceBuilder(上传任务)
| 方法 | 必填? | 说明 |
|---|---|---|
new(file_name, file_path, chunk_size) | 是 | 文件来源的上传任务,chunk_size == 0 归一化为默认值 |
from_bytes(file_name, bytes, chunk_size) | 二选一 | 内存字节来源 |
with_url(url) | 通常要 | 上传目标 URL |
with_breakpoint_upload(Arc::new(...)) | 云上传要 | 挂上 provider / 自定义上传协议 |
with_max_parts_in_flight(n) | 可选 | 单文件分片并发数,默认 1;仅对并发安全协议生效 |
with_max_chunk_retries(n) | 可选 | 单片重试次数,默认 3 |
with_max_upload_prepare_retries(n) | 可选 | prepare 阶段重试次数,默认 3 |
with_method(method) | 可选 | 默认 / 自定义上传请求的 HTTP 方法(默认 POST) |
with_headers(headers) | 可选 | 替换基础请求头 |
build() | 是 | 读文件元数据并返回 PounceTask,可能返回 std::io::Error |
MeowClient(客户端)常用方法
| 方法 | 用途 |
|---|---|
MeowClient::new(config) | 创建客户端(执行器懒启动) |
enqueue_and_wait(task, progress_cb).await | 提交并等待到终态,返回 TaskOutcome |
try_enqueue(task, progress_cb, complete_cb).await | 提交任务,返回 TaskId(快速失败,不等完成) |
try_enqueue_paused(task, ..).await | 以暂停态导入,不做任何 IO,之后 resume 才启动 |
pause / resume / cancel (task_id).await | 暂停 / 恢复 / 取消 |
register_global_progress_listener(cb) | 注册观测「所有任务」进度的全局监听器 |
snapshot().await | 查看排队 / 活跃的任务组数量 |
close().await | 关机必调:优雅关闭 |
相关阅读
- 《rusty-cat 分片并发下载完全指南》 —— 并发下载、
with_total_size、.rcdl续传等。 - 官方英文文档:
rusty-cat/docs/concurrent-chunk-transfer.md、rusty-cat/README.md。
本文基于
rusty-cat0.2.4 编写,所有代码片段均经过cargo check编译验证。