< 返回版块

zerx-lab 发表于 2026-07-14 09:09

Tags:rust, flutter, tokio, 开源项目, 下载器

大家好,分享一个我断断续续写了挺久的个人项目:FluxDown,一个下载管理器。初衷很朴素:IDM 收费,免费的那几个要么界面停在十年前,要么核心是 aria2 套壳,我想要的"运行时动态拆分慢速分段"这种行为只能自己写引擎。

引擎全部是 Rust(tokio),UI 用 Flutter,中间靠 Rinf 做信号通信。支持 HTTP/HTTPS、FTP、BitTorrent(librqbit)、HLS/DASH,Windows/macOS/Linux/Android 都有包,配套有 Chrome/Firefox 扩展做下载接管。

多段下载没有想象中简单

一开始我以为多段下载就是"文件切 N 份,N 个 Range 请求,写同一个文件"。真跑到各种奇怪的服务器上之后,这套朴素模型碎了一地。现在的引擎大概长这样:

分段数只是上限,不是启动并发。 segment_advisor 按文件大小和 CPU 核数算一个连接数上限(≤2MB 不分段,硬上限 64 段),但 coordinator 实际从 2 条连接起步,每 2 秒测一次窗口吞吐:吞吐还在涨就把额度倍增,涨不动就冻结——相当于在运行时探服务器和链路的并发甜点。一次性开满 32 连接在很多 CDN 上只会立刻吃 429。

扩容还可能是负收益。 有些服务器发现你多连接,会按连接数均分带宽再惩罚性压低单连接速率,实测见过 59 MB/s 直接掉到 2 MB/s。所以扩容后窗口吞吐跌破扩容前一半就判定"崩塌":回滚到扩容前规模(多余 worker 下完当前段自然退休,绝不掐进行中的连接),并把这个域名学到的连接上限写进 config 表,带 24 小时 TTL,同域名下次任务直接以保守值起步。切代理时整个缓存清空重学——连接上限本质是服务器对"某个出口 IP"的策略,换了出口旧观察就不可信了。

动态拆分要讲原子性。 worker 下完自己的段会找 coordinator 要活干:有 Pending 段就拿,没有就把剩余字节最多的活跃段从中点劈开(IDM 的 in-half division)。拆分落库时子段插入和父段 end_byte 缩小必须在同一个事务里,不然断电重启后段表出现重叠,续传数据直接对不上。拆分阈值也是随实时吞吐自适应的:低速连接 512KB 就拆,高速 2MB 才拆;下载尾声还有个 64KB 的微拆分兜底,专治"99% 之后一个慢段拖着全场干等"。

fsync 也能写出 bug。 64 个 worker 各持一个指向同一文件的 fd,如果各自周期性 fdatasync,等于每个周期把整个文件的脏页重复刷 N 遍。后来做了一个全局合并闸:整文件级别每 2 秒至多一次 fdatasync,且只有 fsync 的起始时刻晚于自己 flush 时刻,才允许把进度偏移写进 DB——维持"DB 记录的偏移 ≤ 已持久化字节"这个不变式,崩溃后续传才不会拼出洞。

最离谱的是配额型端点。 某 NAS 系统的下载接口,一个 token 只允许固定次数成功 GET,发 Range 请求不但 400 还直接作废 token——一轮常规的多段探测等于亲手判死这个任务。现在的处理是:没验证过 Range 能力的 URL,首连接发不带 Range 的 plain GET,从首响应学习(206 或 Accept-Ranges: bytes 才开多段);一旦检测到"新连接必被拒",把首段流右侧所有零进度段全部吸收合并进这条流,然后把它当独苗供着——停滞容忍从常规的 5 秒放宽到 60 秒(对齐 aria2 的默认读超时),因为掐了这条流就再也连不上了。

其他几个印象深的坑

  • librqbit 内部会 block_on,而我的事件循环跑在 current_thread runtime 上,接进来的时候 actor 直接死锁。现在所有 BT 调用都隔离在专用 runtime / spawn_blocking 里。FTP 用的 suppaftp 是同步 API(它的 async 版依赖 async-std),同样 spawn_blocking + channel 桥出来。
  • 浏览器拦截下载比想象中脏。 Chrome MV3 的 downloads.onDeterminingFilename 可以干净取消,Firefox 没有这个 API,只能 onCreated 后等元数据填充再取消,用户会看到下载条闪一下。最后做成三层防线互为兜底。
  • Native Messaging 单帧上限 1MB,网页批量多选几百个链接一条消息塞不下,按 700KB + 1000 条双上限分块。分块送达一半失败怎么办?定的语义是部分成功就部分成功,绝不全量重发——重复任务比缺任务更伤用户。
  • Rust 2024 edition 去掉了 #[macro_use],日志宏得每个文件显式 use,迁移时改了几十个文件。

工程取舍

引擎 crate 零 FFI 依赖,和宿主之间只靠 EventSink / HostSelection 两个 trait 解耦。这个决定后来赚到了:同一个引擎现在同时跑在桌面 App(Rinf 适配层)、headless 服务器(axum + WebSocket)和 CLI(fluxdown add <url> --local 不依赖任何服务直接下)里,协议代码一行没改。

另外做了 aria2 JSON-RPC 兼容层(36 个方法全覆盖),AriaNg 这类现役客户端可以直接指过来用。

Clippy 开了 deny(unwrap_used, expect_used),非测试代码里一个 unwrap 都过不了编译。刚开始嫌烦,写到后面发现被迫想清楚每条错误路径之后,诡异崩溃基本绝迹,值。

现状

  • 桌面端:Windows(x64/ARM64,安装包 + 便携版)、macOS(Intel/Apple Silicon,DMG)、Linux
  • 移动端:Android APK(arm64/armv7/x86_64 + 通用包)
  • headless 服务器:同一个引擎 + axum + Web UI,六平台静态二进制(Linux 是 musl 全静态,扔 NAS 上就能跑)、Docker 镜像,还打了 OpenWrt ipk / 群晖 spk / QNAP qpkg
  • CLI:六平台独立二进制,--local 模式不依赖任何服务直接下
  • 完全免费,无广告无追踪无账号,数据全在本地 SQLite
  • 官网: https://fluxdown.zerx.dev

代码里肯定还有写得不好的地方,欢迎来提 issue 或者直接喷架构。


Ext Link: https://fluxdown.zerx.dev

评论区

写评论

还没有评论

1 共 0 条评论, 1 页