41K Star 的 aria2,凭什么一个命令行下载工具活了 20 年
- 多源分片并行下载,支持 HTTP、FTP、BT 等协议混合加速
- BitfieldMan 位图追踪 16KB 粒度 block,SegmentMan 动态分配并发分片
- SocketPool 复用 TCP 连接,减少高延迟链路握手开销
- JSON-RPC 接口使 aria2 成为可编程守护进程,支撑图形界面生态
- 引擎-协议正交架构通过 Piece 和 BitfieldMan 统一抽象,协议扩展不影响调度层
2026 年了,你在终端里敲 wget 下载一个大文件,盯着那个孤独的进度条慢慢爬,心里可能会冒出一个念头,为什么不能用满我的千兆带宽?
这个疑问,日本开发者 Tatsuhiro Tsujikawa 在 2006 年就想过了。他写了 aria2,一个支持多源、多协议、分片并行的命令行下载工具。GitHub 上 41,408 个 Star,3,870 个 Fork,到今天仍然是 Linux 服务器上跑批量下载的事实标准。
说真的,一个下载工具能活 20 年,还不被淘汰,这事本身就值得拆一拆。
aria2 到底在解决什么问题
你想想看,传统的下载工具,不管是 wget 还是 curl,核心逻辑都是一条线,建立一个连接,从头到尾顺序拉数据。带宽够不够,取决于这一个连接的 TCP 窗口大小和服务器限速。
aria2 换了个思路。它把一个文件切成很多片(Segment),每一片可以走不同的连接,甚至不同的协议。一个 1GB 的文件,aria2 可以同时从 HTTP 镜像站 A、FTP 镜像站 B 和 BitTorrent swarm 里拉数据,HTTP 下来的分片还能喂回 BT 网络做上传。这叫多源下载(multi-source download)。
这不是什么新概念,2000 年代初的 FlashGet、NetAnts 就在做。但 aria2 把这件事搬到了命令行,做成了一个可编程的守护进程,带 JSON-RPC 接口,能被任何语言调用。这才是它 20 年不被淘汰的原因。
分片引擎的核心,BitfieldMan 和 SegmentMan
我花时间读了 aria2 的源码,955 个 C++ 文件,核心的分片逻辑集中在几个关键类里。
最底层是 BitfieldMan,定义在 src/BitfieldMan.h。这个名字直译就是「位图管理器」,它维护三块位图,bitfield_ 记录哪些 block 已经下完,useBitfield_ 记录哪些 block 正在下载,filterBitfield_ 用于 BT 的选择性下载(只下种子里的部分文件)。每一个 bit 对应一个固定大小的 block,这个粒度由 Piece.h 里的 BLOCK_LENGTH = 16_k 定义,也就是 16KB。
你想想看,一个 1GB 的文件,16KB 一个 block,位图大小是 65536 个 bit = 8KB。这个数据结构设计得极其紧凑,整个下载进度用 8KB 内存就能追踪。而且位运算查进度是 O(1) 的,性能不会随文件增大而退化。
但光有位图不够。aria2 的分片是并发的,多个连接各自认领不同的分片,谁来分配?这就是 SegmentMan(src/SegmentMan.h)的职责。它持有一个 SegmentEntries 双端队列,每个 SegmentEntry 绑定一个 cuid(Connection UID,连接唯一标识)和一个 Segment 对象。当一个新的下载连接建立时,SegmentMan 会从 PieceStorage 里找一个还没下完的 piece,切出一个 segment 分给它,连接断开了再回收。
这套机制让 aria2 可以在运行时动态调整并发度。某个连接慢了,它的 segment 不会无限占着,超时后会被回收给其他连接。这就是为什么 aria2 在网络条件不均匀时,实际吞吐量比固定分片的工具更稳。
连接复用和 SocketPool
还有个细节我觉得很巧。aria2 在 DownloadEngine 里内置了一个连接池,src/DownloadEngine.h 里定义了 SocketPoolEntry,以 IP地址:端口 为 key 存在一个 std::multimap 里。一个连接下载完一个分片后,不会被立刻销毁,而是放回池子里。下一个分片如果走同一个服务器,直接复用这个 TCP 连接。
这件事的意义在于,HTTP 下载的分片切换不需要反复 TCP 握手。对于高延迟链路,省掉一个 RTT 的握手时间,多开几个并发分片,实际加速效果很明显。
我翻 issue 区的时候看到一个有意思的讨论。issue #1039,53 个赞,标题是「为什么 max-connections-per-server 被硬编码限制在 16?」用户 trudnorx 的评论挺有代表性,说他的网络环境里 16 个连接根本喂不满带宽,aria2 自称「ultra fast download utility」但实际跑不满。还有人提到微软的 azcopy 默认开 128 个连接,内网能跑到 1300Mbps,aria2 只有 70Mbps。
我去翻了源码。在 src/OptionHandlerFactory.cc 里,这行代码写得很直白,
OptionHandler* op(new NumberOptionHandler(PREF_MAX_CONNECTION_PER_SERVER,
TEXT_MAX_CONNECTION_PER_SERVER,
"1", 1, 16, 'x'));
第五个参数 16 就是上限。你可以在命令行用 -x 16 指定最大连接数,但超过 16 直接报错。想突破?自己改源码重新编译。
维护者的态度也很有意思。有人反驳说「16 已经够多了,再多就是 DoS 攻击」。但用户说得也有道理,服务器侧限流是服务器的事,客户端硬编码一个上限,保护的是谁呢?这个 issue 从 2016 年开到现在没关,也没改。这件事挺 aria2 的,固执,但诚实。
RPC 接口,让 aria2 变成可编程引擎
aria2 真正出圈,不是靠命令行参数,而是它的 RPC 接口。
README 里列了一长串特性,其中一条是「JSON-RPC (over HTTP and WebSocket)/XML-RPC interface」。这个接口让 aria2 可以作为一个守护进程常驻后台,前端通过 RPC 调用它的所有能力。添加任务、查询进度、暂停恢复、修改配置,全部远程可控。
在 src/RpcMethodImpl.h 和 src/RpcMethodFactory.cc 里,aria2 把每个 RPC 方法注册成一个 RpcMethod 对象。aria2.addUri 添加下载任务,aria2.tellActive 查询活跃任务,aria2.changeGlobalOption 动态修改全局配置,aria2.shutdown 优雅关闭。方法名以 aria2. 为前缀,通过 system.multicall 还能批量调用。
这套 RPC 设计是 aria2 生态繁荣的基础。几乎所有主流的 aria2 图形界面,Motrix、AriaNg、Persepolis,底层都是连 aria2 的 JSON-RPC 接口。你甚至可以用 WebSocket 长连接订阅下载事件通知,aria2 会在任务状态变化时主动推消息过来。
一个 C++ 命令行工具,把自己设计成一个带 WebSocket 推送的可编程引擎,这是 2006 年的项目。你不得不佩服 Tsujikawa 的前瞻性。
BitTorrent 和 Metalink,协议层的统一
aria2 另一个被低估的能力是协议统一。它同时支持 HTTP(S)、FTP、SFTP、BitTorrent 和 Metalink,而且这些协议可以混合使用。
BitTorrent 部分的代码量最大,src/ 下以 Bt 开头的文件有几十个,从 BtHandshakeMessage 到 BtPieceMessage 到 BtLeecherStateChoke,覆盖了完整的 BT 协议栈。DHT(分布式哈希表)的实现也很完整,DHTBucket、DHTBucketTree、DHTAutoSaveCommand,支持 Kademlia 算法的节点查找和路由表持久化。IPv4 和 IPv6 的 DHT 路由表分别存在 dht.dat 和 dht6.dat 里。
Metalink 是个更小众的协议,RFC 5854。它做的事情是,用一个 XML 文件描述一个文件的多个镜像源和校验信息。aria2 读到 Metalink 文件后,会自动从多个镜像并行下载,还能用 chunk checksum 在下载过程中逐块校验。这个能力在 Linux 发行版分发场景里很有用,一个 ISO 文件挂在 10 个镜像站上,aria2 全部用上。
坦白讲,把这些协议塞进一个二进制文件里,还能保持代码结构清晰,这件事的工程难度比看起来大得多。HTTP 和 BT 的数据模型完全不同,一个是顺序字节流,一个是随机分片的 piece。aria2 的解法是用 Piece 和 BitfieldMan 做统一抽象,不管数据来自 HTTP 还是 BT,最终都落到 block 级别的位图追踪上。底层协议差异被封装在各自的 Command 类里,上层的 SegmentMan 和 PieceStorage 不需要关心。
诚实的维护现状
aria2 不是没有问题。
我看了一下 release 节奏,1.37.0 发布于 2023 年 11 月,1.36.0 是 2021 年 8 月。两次正式 release 之间隔了两年多。对于一个还活着的项目来说,这个节奏偏慢。当然 README 里写了「每月 15 号发 MINOR 版本,没改动就跳过」,但实际执行上,这两年显然跳过了很多次。
贡献者方面,GitHub 显示 30 个贡献者(API 分页限制,实际可能更多),但核心维护者基本就是 Tsujikawa 一个人。bus factor 约等于 1。这也是 16 连接限制那个 issue 拖了 8 年没动的原因之一,人手不够,优先级排不过来。
协议是 GPL-2.0,对商业集成有约束。如果你想把它静态编译进闭源产品,需要仔细处理许可问题。不过作为独立工具使用或服务端部署,GPL-2.0 没什么影响。
还有几个实际使用中容易踩的坑。aria2 默认不配置端口转发,BT 下载的 listen 端口 6881-6999 需要你自己在路由器上开端口,否则只能连到主动连你的 peer,做种效率很低。DHT 路由表第一次启动是空的,需要跑一段时间才能积累足够节点,冷启动下载冷门资源可能找不到 peer。issue #362 就有人反馈「aria2 看不到 peer 但 uTorrent 下载正常」,这通常是 DHT 路由表还没暖起来的问题。
一个值得带走的设计模式
拆完 aria2 的源码,我一直在想它到底做对了什么。
不是多线程下载,axel 也做了。不是支持 BT,Transmission 也做了。aria2 真正的差异化在于一个设计决策,把下载引擎和协议实现彻底解耦,通过统一的分片抽象层对接所有协议。
这个模式我称之为「引擎-协议正交架构」。下载引擎(DownloadEngine + SegmentMan + PieceStorage)只管分片调度、连接复用、进度追踪,完全不关心数据从哪来。每个协议(HTTP、FTP、BT)实现自己的 Command 类,负责建连接、收数据、把数据喂给 piece。两层之间通过 Piece 和 BitfieldMan 这个统一接口通信。
好处是显而易见的。加一个新协议,只需要写新的 Command 子类,引擎层一行代码都不用改。调度的优化(比如连接池、分片回收)对所有协议同时生效。这也是为什么 aria2 能在 955 个源文件里保持可控的复杂度,协议扩展是正交的,不会互相纠缠。
你想想看,这个模式是不是只适用于下载工具?不是的。任何需要「多种数据源 → 统一处理管道」的场景都可以借鉴,爬虫框架的数据源抽象、日志收集器的 input 插件、ETL 工具的 source adapter,底层都是同一个思路。把变的部分(协议/数据源)和不变的部分(调度/处理)正交拆开,用一层薄薄的抽象隔开。
aria2 20 年不被淘汰,不是因为它多快,而是因为这个架构让它在 HTTP 时代、BT 时代、Metalink 时代、RPC 驱动时代,都能不伤筋动骨地接上。一个 2006 年的 C++ 项目,到 2026 年还是下载工具的标杆,靠的不是代码量,是架构正交带来的进化能力。
评论互动