lyyyuna 的小花园

动静中之动, by

RSS

BitTorrent Go 版本重构小记

发表于 2026-06
记录一次由 AI 完成的 BitTorrent 下载器 Go v2 重构。

前言

这篇文章先把最重要的事情说清楚:这次 zhongzi-go 的 v2 重构,代码基本都是 AI 生成的;这篇文章本身也是 AI 根据我的要求生成的。

我在这个过程中做的事情,更像是产品经理和代码评审者的混合体:规划几个新的需求,指出重构方向,在实现有问题时提醒 AI 哪里不对,要求它继续测试和修正。比如我会说“下载管理不要基于 piece,要改成 block”,“TUI 应该独立成一个工具”,“支持 magnet”,“peer 管理太初级”,“这个速度低了”,“慢 peer 不能简单踢掉”。AI 负责读代码、改代码、写测试、构建、上传远端机器测试,再根据结果继续改。

这听起来有点偷懒,但我觉得反而值得记录。因为这个过程很清楚地暴露了 AI 编程的两面:它能快速把一个想法落成代码,也很容易做出看起来合理、实际一跑就有问题的设计。

为什么要重构

旧版本的问题不只是“代码写得不够好”,而是方向上已经很难继续加功能。

之前的实现更接近一个实验性质的客户端:能解析 torrent,能连 peer,能按 piece 下载。但真的要做成一个可以后台运行、可以观察状态、可以调参的下载工具,就会遇到几类问题:

  1. 下载和 UI 混在一起,没法自然地后台运行。
  2. 下载调度以 piece 为中心,一个 piece 慢了会拖住整体进度。
  3. peer 发现和 peer 健康管理比较粗糙,公网环境下速度很不稳定。
  4. magnet、DHT、限速、状态持久化这些功能继续往旧结构里塞,会越来越别扭。
  5. 测试不够系统,很多问题只能靠手动跑公网 torrent 才发现。

所以 v2 基本上是一次推倒重来,而不是在原来的代码上修修补补。

这里顺便提一下 Go module 的版本管理。v2 以后模块路径需要带 /v2,所以模块路径也改成了:

github.com/lyyyuna/zhongzi-go/v2

这不是单纯为了形式正确。既然已经是一次破坏兼容性的重构,把 module path 一起切到 v2,后续代码结构和 API 才不用背着 v1 的包袱。

v2 的目标

这次重构一开始定了几个方向:

需求 v2 的处理方式
支持 magnet 解析 btihtrx.pe,通过 peer metadata extension 获取 torrent metainfo
支持后台运行 downloader 作为 daemon task,TUI 作为独立进程连接 daemon
下载调度更细 piece 作为校验边界,block 作为下载调度单元
支持限速 全局和任务级 rate limit,block request 前等待 token
peer 管理更强 候选池、连接态、健康评分、refill、slow fallback
可测试 单元测试 + 本地 fake peer e2e + 公网 torrent 手动验证

这几个需求看似分散,其实都指向一个核心:下载器不能只是“从几个 peer 那里拉 piece”,而应该是一个持续运行的系统。系统里有任务,有状态,有外部输入,有不可靠 peer,有网络波动,有限速,有 UI 观察。

从 piece 到 block

BitTorrent 里有两个容易混在一起的概念:piece 和 block。

piece 是 torrent 文件里的校验单元。torrent metainfo 里保存的是每个 piece 的 SHA-1,所以只有整个 piece 拼完后才能校验。

block 是协议下载时的请求单元。一个 piece 通常会拆成多个 block,常见大小是 16 KiB。

旧思路如果以 piece 为单位管理下载,就会遇到一个问题:一个 piece 很大,某个 peer 很慢,整个 piece 就会卡住。即使其他 peer 手里也有这个 piece,调度器也不容易细粒度地把剩余部分分出去。

v2 改成:

torrent
  └── piece 0
        ├── block 0
        ├── block 1
        └── block 2

调度器分配的是 block,完成后暂存在内存里;当一个 piece 的所有 block 都完成,才拼起来做 SHA-1 校验,通过后写入文件。

这样做的好处是并发粒度更细。慢 peer 最多拖住一个或几个 block,不会长期占住一个完整 piece。后来公网测试速度能提升,block 级 timeout 也是关键之一。

daemon 和 TUI 分开

另一个重要变化是把 UI 从下载主流程中拆出去。

下载器应该能后台运行。否则一个 TUI 退出,下载任务也跟着退出,这就不像一个正经下载工具。

v2 里大致是这样的结构:

zhongzi daemon
  ├── task manager
  ├── downloader
  ├── rate limiter
  ├── state store
  └── unix socket

zhongzi tui
  └── client -> unix socket -> daemon

daemon 负责真正的任务生命周期:add、pause、resume、remove、status、stats、limit。TUI 只是一个客户端,通过 Unix socket 连接 daemon,订阅事件流并做低频轮询兜底。

这个设计没有什么特别新奇的地方,但它把边界划清楚了。下载逻辑不需要知道屏幕怎么刷新,TUI 也不需要知道 block 怎么调度。

magnet 和 metadata

magnet 支持主要依赖两个东西:

  1. magnet URI 里的 xt=urn:btih:<infohash>
  2. peer extension 里的 ut_metadata

普通 torrent 文件一开始就有完整 metainfo;magnet 只有 infohash,所以必须先找到 peer,再从 peer 那里把 metadata 分片拉下来,拼成完整 torrent metainfo,验证 infohash 后才能进入正常下载流程。

这块 AI 生成代码时完成得还可以,但也有一个典型问题:它容易只实现“快乐路径”。例如有显式 peer 的 magnet 比较容易测,真正公网 tracker/DHT 找 peer 时,会遇到连接失败、metadata extension 不支持、metadata size 拿不到等各种情况。

所以后面补了本地 e2e:用 fake peer 提供 ut_metadata,确保 magnet 能走完整链路。

peer 管理:这才是下载速度的核心

一开始我以为速度低主要是 block 调度问题。后来远端测试发现,问题更偏向 peer 管理。

公网 peer 很不可靠:

如果只在启动时发现一批 peer,然后固定使用它们,速度很快就会掉下去。

v2 后来加了持续 peer 管理:

discover peers -> candidate pool -> connect -> active pool
                                  -> health update
                                  -> refill

下载过程中会周期性 refill。closed、choked、非 connected 的 provider 会被过滤;连接池不足时重新从 tracker/DHT 拿候选 peer,再补进来。

同时每个 peer 有健康信息:连接失败、握手失败、block timeout、bad block、完成块数、下载速度、choke 时长等。

这一步带来的变化很明显。早期测试里,下载 5 分钟只有几十 MB 到一百多 MB;持续 peer 管理和 block timeout 修完后,Ubuntu 官方 torrent 的 5 分钟下载量提高到了几百 MB。

慢 peer 不能等同于坏 peer

这里有个很有意思的小插曲。

为了提高热门 torrent 的速度,AI 一开始把 block timeout 扣分设得很重。结果策略变成:一个 peer 慢,就很快 ban 掉。

对热门资源这很有效,因为有很多替代 peer。但我指出了一个问题:如果一个冷门资源只有一个 peer 有数据,而它就是慢呢?

这时慢 peer 不是坏 peer。它传得慢,但它可能是唯一可用来源。把它 ban 掉,下载就永远完不成。

于是后面又改了一版:

这就是一个典型的 AI 不足:它会沿着“提高速度”这个局部目标走得很远,但不一定意识到另一个目标“保证冷门资源可完成性”也同样重要。

限速

限速的设计相对简单。v2 里有一个 rate limiter,支持全局和任务级下载限速。

真正下载 block 前先等待 token:

scheduler 分配 block
  -> rate limiter 等待下载配额
  -> peer request
  -> block result

metadata 下载也经过 limiter。严格说 metadata 不大,不限也没什么问题,但统一处理更简单。

这里需要注意,限速不应该放在 peer socket read 那一层,否则每个 peer 都要理解任务级别的限速状态。放在 app/coordinator 层更合适,因为那里知道 task id、block size,也能看到调度器状态。

测试和公网验证

这次 AI 生成了不少单元测试和本地 e2e:

公网测试用的是 Ubuntu 官方 torrent,在 vultr2 上跑。

比较有代表性的几轮结果:

阶段 参数/策略 结果
早期 v2 peer 管理还很粗 5 分钟约 61 MB,速度很低
持续 peer 管理后 max-peers=80block-workers=64 5 分钟约 487 MB,约 1.9 MB/s
调高默认并发后 默认 max-peers=120peer-pipeline=2 3 分钟约 457 MB,约 3.06 MB/s
slow fallback 初版 slow peer 全量参与 3 分钟约 303 MB,约 2.1 MB/s
slow fallback 限量后 slow peer 少量混入 3 分钟约 310 MB,约 2.0 MB/s,本轮 active peer 质量较差

公网测试有随机性。比如有一次 tracker 返回 503,DHT 也没找到可用 peer,17 秒就退出 app: no block providers。这不是代码一定坏了,而是外部发现质量本身不稳定。

但公网测试仍然很有价值。因为很多问题在本地 fake peer 下永远不会暴露,比如:

这些都是跑真实公网 torrent 才发现的。

AI 协作的感受

这次最让我感到意外的不是 AI 能写代码,而是它能持续工作。它可以读代码、改代码、跑测试、构建、上传远端机器、分析日志,再继续改。这个循环如果人工来做,体力成本很高。

但 AI 的问题也很明显。

它很容易把某个局部指标优化过头。比如“速度低”,它就会倾向于更激进地踢掉慢 peer;但下载器不是只服务热门资源,也要考虑冷门资源的可完成性。

它也容易在第一版实现中漏掉工程边界。比如 peer manager 一开始没有并发锁,但 downloader worker 和 refill goroutine 会同时更新它;这类问题不是代码能编译就能发现的,需要从系统运行角度去审。

还有一点,AI 对“测试通过”的理解有时太乐观。单元测试通过不代表公网行为正常,公网测试速度高也不代表冷门 torrent 没问题。这里仍然需要人来定义验收标准。

所以这次我的角色更像这样:

  1. 提需求:magnet、daemon/TUI、限速、block 调度、持续 peer 管理。
  2. 定方向:彻底重构,不强行复用 v1。
  3. 看结果:测试是否真的跑了,远端速度是否合理。
  4. 指出问题:速度低、peer 管理粗、slow peer 不能直接 ban。
  5. 要求继续修:一轮一轮把策略补完整。

代码是 AI 生成的,但判断标准不能完全交给 AI。

小结

这次 Go v2 重构大致完成了一个更像下载器的骨架:

  1. 以 block 为下载调度单元,piece 作为校验和写盘边界。
  2. daemon 和 TUI 分离,支持后台任务。
  3. magnet、tracker、DHT 都进入主流程。
  4. rate limit 从 app 层统一控制。
  5. peer 管理从一次性连接变成持续 refill。
  6. 慢 peer 作为 fallback 保留,坏 peer 才 ban。
  7. 默认并发调高到更适合公网 torrent 的配置。

它离成熟客户端还很远。比如 PEX、LSD、endgame mode、文件选择、持久化更细的 piece 状态、DHT 长期路由表维护,都还没认真做。

不过这次重构证明了一件事:如果把问题切成清楚的需求和验收标准,AI 已经可以承担相当一部分工程执行工作。人不一定要逐行写代码,但仍然要负责方向、边界和判断。

lyyyuna 沪ICP备2025110782号-1