第 13 章:配置、持久化与可移植性 —— 让一次运行跨越重启和平台
长期运行的软件不能只在内存里正确。Transmission 需要把 Session 设置、Torrent 身份、进度、队列、DHT 节点和统计分别持久化,还要在不同 OS 的路径、权限和文件 API 上保持语义一致。
一、配置不是一个文件
| 数据 | 典型位置 | 格式 | 生命周期 |
|---|---|---|---|
| Session 设置 | settings.json | JSON | 每个内核实例 |
| Torrent 元数据 | torrents/*.torrent | bencode | 每个完整任务 |
| Magnet 暂态 | torrents/*.magnet | 文本 | metadata 补齐前 |
| Torrent 恢复 | resume/*.resume | bencode | 每个任务可变状态 |
| 队列位置 | queue 相关存储 | 序列化状态 | Session 内任务顺序 |
| DHT 节点 | dht.dat / bootstrap | bencode/文本 | 网络路由缓存 |
| Session 统计 | stats 文件 | bencode | 跨重启累计 |
| Blocklist | blocklists/ | 编译后二进制 | 地址过滤 |
分开保存可以让不同更新频率和失败策略互不干扰。Torrent metadata 很少改,resume 几分钟就可能更新;把它们放一个大 JSON 会放大写入与损坏范围。
二、SessionSettings 是强类型配置模型
session-settings.h 将几十个字段定义为 C++ 成员,并用一个 Fields tuple 映射到 TR_KEY_*:
Field<&SessionSettings::dht_enabled>{ TR_KEY_dht_enabled }
Field<&SessionSettings::peer_port>{ TR_KEY_peer_port }
Field<&SessionSettings::download_dir>{ TR_KEY_download_dir }通用 serializer 遍历 tuple,把 tr_variant Map 加载到强类型对象,或反向保存。RpcServerSettings、SessionAltSpeedSettings 使用同一模式。
这比到处写 dictFindBool() 更有优势:默认值集中、字段映射可编译检查、测试能做 round-trip、RPC 与 settings 可复用相同 key。
三、设置来源的优先级
以 daemon 为例:
libtransmission defaults
→ daemon app-specific defaults(例如 rpc_enabled=true)
→ settings.json
→ 命令行参数
→ Session 内部 fixup/normalize命令行覆盖只影响本次启动输入,启动后 daemon 会保存实际 settings。GUI 还有自己的窗口尺寸、显示模式等 UI prefs,它们不属于 tr_session。
Torrent 添加时又有另一条优先级:ctor FORCE → .resume → ctor FALLBACK。不要把 Session settings 与 Torrent resume 混为一类配置。
四、兼容字段如何迁移
preferred_transports 是新的有序传输偏好,旧配置可能只有 tcp_enabled/utp_enabled。SessionSettings::load() 检查新字段是否存在:
- 有:从 preferred list 修正旧布尔字段;
- 无:从旧布尔字段生成 preferred list。
这种双向 fixup 允许旧客户端/旧文件继续工作,又给新实现一个更有表达力的内部模型。兼容代码应集中在加载边界,而不是让 Peer Manager 同时理解三套设置。
五、设置应用是 diff,不是全量重建
tr_session::setSettings() 比较旧/新值,对不同字段采取不同动作:
- 纯内存字段直接更新;
- 带宽值更新 bandwidth 根或 group;
- Peer port/bind address 变化需要重建监听与 DHT/port forwarding;
- RPC port/address/enabled 交给
tr_rpc_server::load(); - blocklist/LPD/DHT/µTP 可能创建、销毁或重配子系统;
- 默认 tracker 改变会 reset 所有 public torrents 的 Announcer。
把副作用集中在 Session,前端只提交设置值,不负责知道重启哪个子系统。
六、写入失败必须留在领域状态里
保存 .torrent、.resume、移动文件或创建目录失败时,源码使用 tr_error 保存 code/message。Torrent 的 local error 会进入 tr_stat/RPC/UI,不能只写日志然后继续假装成功。
.resume 序列化失败尤其危险:本次下载仍可继续,但下次启动会丢进度缓存。实现会标记错误并保留内存状态,下一次周期保存仍可重试。
七、文件迁移与命名兼容
不同平台历史上使用 Resume/Torrents 或小写目录;metainfo helper 根据 torrent name 与 info hash 迁移旧文件名。info hash 是稳定锚点,显示名可被用户重命名。
路径逻辑集中在 platform.cc、torrent-metainfo、torrent-files,避免 UI 拼接配置目录。用户重命名 torrent 子路径时,代码先验证不会与现有路径冲突,再移动磁盘项并更新 metainfo 中的 subpath。
八、Watch directory 的可移植策略
统一 Watchdir 接口下有:
- Linux inotify;
- BSD/macOS kqueue;
- Windows directory API;
- generic timer polling fallback。
CMake 探测能力,运行时 factory 选择实现;daemon 还允许强制 generic。原生 API 低延迟低开销,generic 保证冷门平台仍可工作。
“有 fallback”很重要,但 fallback 也必须复用同一个 handler 和稳定文件判定,否则平台行为会分叉。
九、网络可移植性不止 IPv4/IPv6
net.h 定义 tr_address、tr_port、tr_socket_address 等值类型,显式区分 host/network byte order。Session 同时管理 IPv4/IPv6 bind address、TCP/UDP socket;DHT announce times 也分别记录两个地址族。
Peer compact 编码按 IPv4 6 字节、IPv6 18 字节解析,不能用裸 sockaddr 大小猜协议。Windows errno/socket error 通过适配层归一。
十、加密后端为何也是编译期策略
同一 crypto-utils.h 可由 CommonCrypto、OpenSSL、mbedTLS、wolfSSL 或 fallback 实现。CMake 的 WITH_CRYPTO 选择后端,并暴露统一 SHA-1、random、base64、SSHA1、DH 所需能力。
协议代码依赖“计算 hash/随机数”的语义,不依赖 EVP 或 CommonCrypto 句柄。后端差异留在 crypto-utils-* 文件。
十一、配置排障的正确层次
- 输入层:settings.json 是否合法,CLI 是否覆盖;
- 强类型层:serializer 是否成功、默认值是什么、fixup 后是什么;
- 应用层:Session 是否检测到变化并执行副作用;
- 持久化层:保存后的文件是否反映实际值;
- 产品层:UI 是本地 Session 还是远端 RPC,路径语义属于哪台机器。
尤其在 Qt remote 模式,下载目录是 daemon 所在机器的路径,本地文件选择器没有直接意义。
十二、设计启示
- 按生命周期拆分状态文件,限制写放大与损坏半径;
- 先进入强类型模型,再应用副作用,避免 parser 与运行时耦合;
- 兼容逻辑放边界,内部只保留一种首选表示;
- 平台实现以窄接口替换,业务层不散落
#ifdef; - 持久化是缓存与事实的组合,resume 加速恢复但不能替代 hash 校验。
源码锚点
libtransmission/session-settings.h:强类型字段与序列化 tuplelibtransmission/serializer.h、serializer.cc:通用转换器libtransmission/platform.cc:默认目录与平台环境libtransmission/watchdir.cc:watchdir factorylibtransmission/file-posix.cc、file-win32.cc:文件 API 适配libtransmission/crypto-utils.cc:加密统一入口docs/Configuration-Files.md:官方配置目录说明