BitTorrent Go 版本重构小记
(BitTorrent 协议实现小记, Part 5)
前言
这篇文章先把最重要的事情说清楚:这次 zhongzi-go 的 v2 重构,代码基本都是 AI 生成的;这篇文章本身也是 AI 根据我的要求生成的。
我在这个过程中做的事情,更像是产品经理和代码评审者的混合体:规划几个新的需求,指出重构方向,在实现有问题时提醒 AI 哪里不对,要求它继续测试和修正。比如我会说“下载管理不要基于 piece,要改成 block”,“TUI 应该独立成一个工具”,“支持 magnet”,“peer 管理太初级”,“这个速度低了”,“慢 peer 不能简单踢掉”。AI 负责读代码、改代码、写测试、构建、上传远端机器测试,再根据结果继续改。
这听起来有点偷懒,但我觉得反而值得记录。因为这个过程很清楚地暴露了 AI 编程的两面:它能快速把一个想法落成代码,也很容易做出看起来合理、实际一跑就有问题的设计。
为什么要重构
旧版本的问题不只是“代码写得不够好”,而是方向上已经很难继续加功能。
之前的实现更接近一个实验性质的客户端:能解析 torrent,能连 peer,能按 piece 下载。但真的要做成一个可以后台运行、可以观察状态、可以调参的下载工具,就会遇到几类问题:
- 下载和 UI 混在一起,没法自然地后台运行。
- 下载调度以 piece 为中心,一个 piece 慢了会拖住整体进度。
- peer 发现和 peer 健康管理比较粗糙,公网环境下速度很不稳定。
- magnet、DHT、限速、状态持久化这些功能继续往旧结构里塞,会越来越别扭。
- 测试不够系统,很多问题只能靠手动跑公网 torrent 才发现。
所以 v2 基本上是一次推倒重来,而不是在原来的代码上修修补补。
这里顺便提一下 Go module 的版本管理。v2 以后模块路径需要带 /v2,所以模块路径也改成了:
github.com/lyyyuna/zhongzi-go/v2
这不是单纯为了形式正确。既然已经是一次破坏兼容性的重构,把 module path 一起切到 v2,后续代码结构和 API 才不用背着 v1 的包袱。
v2 的目标
这次重构一开始定了几个方向:
| 需求 | v2 的处理方式 |
|---|---|
| 支持 magnet | 解析 btih、tr、x.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 支持主要依赖两个东西:
- magnet URI 里的
xt=urn:btih:<infohash>。 - 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 很不可靠:
- 有的连不上。
- 有的握手成功但不给 bitfield。
- 有的长期 choked。
- 有的能传,但极慢。
- 有的某些 block 一直超时。
- tracker 偶尔还会返回
503。
如果只在启动时发现一批 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 掉,下载就永远完不成。
于是后面又改了一版:
BlockTimedOut不再直接 ban。- timeout 后 peer 降级为
slow。 - slow peer 仍保留在 provider pool 里。
- 有 fast peer 时,slow peer 只少量混入调度,作为 fallback。
- 没有 fast peer 时,才使用全部 slow peer。
- 如果 slow peer 后续成功下载 block,就恢复成 connected。
- bad data、连接失败、握手失败才继续走 ban/cooldown 路径。
这就是一个典型的 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:
- metainfo 解析。
- magnet 解析。
- tracker compact 和非 compact peer list。
- scheduler 的 block 调度、timeout、stale block 忽略。
- storage 跨文件 piece 写入。
- peer session、keepalive、metadata。
- daemon task lifecycle。
- fake tracker + fake peer 的本地下载 e2e。
公网测试用的是 Ubuntu 官方 torrent,在 vultr2 上跑。
比较有代表性的几轮结果:
| 阶段 | 参数/策略 | 结果 |
|---|---|---|
| 早期 v2 | peer 管理还很粗 | 5 分钟约 61 MB,速度很低 |
| 持续 peer 管理后 | max-peers=80,block-workers=64 |
5 分钟约 487 MB,约 1.9 MB/s |
| 调高默认并发后 | 默认 max-peers=120,peer-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 下永远不会暴露,比如:
- tracker 返回非 compact peer list。
- peer 长期 choked。
- block request 一直挂住。
- in-flight 数上不去。
- 慢 peer 被错误 ban。
这些都是跑真实公网 torrent 才发现的。
AI 协作的感受
这次最让我感到意外的不是 AI 能写代码,而是它能持续工作。它可以读代码、改代码、跑测试、构建、上传远端机器、分析日志,再继续改。这个循环如果人工来做,体力成本很高。
但 AI 的问题也很明显。
它很容易把某个局部指标优化过头。比如“速度低”,它就会倾向于更激进地踢掉慢 peer;但下载器不是只服务热门资源,也要考虑冷门资源的可完成性。
它也容易在第一版实现中漏掉工程边界。比如 peer manager 一开始没有并发锁,但 downloader worker 和 refill goroutine 会同时更新它;这类问题不是代码能编译就能发现的,需要从系统运行角度去审。
还有一点,AI 对“测试通过”的理解有时太乐观。单元测试通过不代表公网行为正常,公网测试速度高也不代表冷门 torrent 没问题。这里仍然需要人来定义验收标准。
所以这次我的角色更像这样:
- 提需求:magnet、daemon/TUI、限速、block 调度、持续 peer 管理。
- 定方向:彻底重构,不强行复用 v1。
- 看结果:测试是否真的跑了,远端速度是否合理。
- 指出问题:速度低、peer 管理粗、slow peer 不能直接 ban。
- 要求继续修:一轮一轮把策略补完整。
代码是 AI 生成的,但判断标准不能完全交给 AI。
小结
这次 Go v2 重构大致完成了一个更像下载器的骨架:
- 以 block 为下载调度单元,piece 作为校验和写盘边界。
- daemon 和 TUI 分离,支持后台任务。
- magnet、tracker、DHT 都进入主流程。
- rate limit 从 app 层统一控制。
- peer 管理从一次性连接变成持续 refill。
- 慢 peer 作为 fallback 保留,坏 peer 才 ban。
- 默认并发调高到更适合公网 torrent 的配置。
它离成熟客户端还很远。比如 PEX、LSD、endgame mode、文件选择、持久化更细的 piece 状态、DHT 长期路由表维护,都还没认真做。
不过这次重构证明了一件事:如果把问题切成清楚的需求和验收标准,AI 已经可以承担相当一部分工程执行工作。人不一定要逐行写代码,但仍然要负责方向、边界和判断。