第 7 章:Peer Wire 消息泵 —— 一条连接如何持续交换状态与数据
握手只解决“你是谁、支持什么”。长期下载由
tr_peerMsgsImpl驱动:它解析长度前缀消息,维护 choke/interest/request 状态,按需发送数据,并通过事件把结果交给 Torrent 与 Peer Manager。
一、消息层持有哪些状态
一个 Peer 连接至少同时维护两组对称状态:
| 视角 | 状态 | 含义 |
|---|---|---|
| 对方对我 | peer_is_choked / peer_is_interested | 我是否允许对方下载、对方是否想要我的数据 |
| 我对对方 | client_is_choked / client_is_interested | 对方是否允许我下载、我是否需要它的数据 |
再加上:
- 对端拥有的 piece bitfield;
- 我发出的 active requests 与超时队列;
- 对端向我发出的
peer_requested_队列; - LTEP message id、metadata 请求、PEX 历史;
- keepalive、request、PEX 等 timers;
- 上传/下载速度与统计。
因此它更像一个小型双向协议 actor,而不只是 parser。
二、基础帧格式
握手之后的消息采用:
4-byte big-endian length
length = 0 → keepalive
length > 0:
1-byte message id
(length - 1) payload解析器先等 4 字节长度,再验证是否超过允许范围;然后等 message id 和完整 payload。网络分片不会改变状态机,因为不足就返回 ReadState::Later。
三、核心消息族
| 消息 | 方向 | 作用 |
|---|---|---|
| CHOKE / UNCHOKE | 双向 | 关闭/开放对方请求窗口 |
| INTERESTED / NOT_INTERESTED | 双向 | 声明是否需要对方数据 |
| HAVE / BITFIELD / HAVE_ALL / HAVE_NONE | 双向 | 通知拥有的 piece 集合 |
| REQUEST | 下载方 → 上传方 | 请求 (piece, offset, length) |
| PIECE | 上传方 → 下载方 | 返回块数据 |
| CANCEL / REJECT | 双向 | 取消或拒绝请求 |
| PORT | Peer → Peer | 告知 DHT UDP 端口 |
| SUGGEST / ALLOWED_FAST | Fast Extension | choke 状态下的有限请求能力 |
| EXTENDED | 双向 | LTEP 扩展握手、PEX、metadata、holepunch |
四、读消息不是“switch 后直接改 Torrent”
消息层把长期策略交给事件订阅者。tr_peer_event 可以表达 GotBlock、GotHave、GotBitfield、GotChoke、SentRequest、SentCancel、GotError 等事件。Peer Manager/Swarm 订阅后更新:
- piece replication;
- active request 归属;
- Peer blame/strike;
- 下载/上传统计;
- Wishlist 缓存;
- torrent completion。
这种 publish/subscribe 让协议 parser 不必直接了解选块排序和 Torrent 所有状态,但事件是同步的,处理顺序仍处于同一个 Session 线程。
五、收到 PIECE 的完整路径
解析器会拒绝越界 piece、错误 offset/length、非活动请求或明显 unwanted 数据。写入成功后清除 active request,发布事件,然后重新计算 desired request count 并填充窗口。
六、上传不是收到 REQUEST 就立刻同步读盘
对端 REQUEST 先进入 peer_requested_ 队列,前提是:
- 当前没有 choke 对端,或 Fast Extension 明确允许;
- 队列未超过
reqq; - piece/offset/length 合法;
- 本地拥有该 piece。
消息 pump 在有输出空间和带宽时从队列取请求,必要时 ensure_piece_is_checked(),从磁盘读取后构造 PIECE;不满足条件则发送 REJECT(对端支持时)。
这把协议接收速度与磁盘/上传带宽解耦,避免一个 Peer 的请求洪水直接触发无界 I/O。
七、请求窗口是动态的
desired_request_count_ 不是固定常量。它参考 Peer 下载速度、block 大小、期望管线时间和对端声明的 reqq,让高速 Peer 保持更多 in-flight requests,低速 Peer 不占用太多块。
maybe_send_block_requests() 的门槛是:
torrent 允许下载
AND 我对 Peer interested
AND Peer 没有 choke 我
AND active_request_count < desired_request_count缺口数量传给 Peer Manager 的 Wishlist,返回 block spans 后批量发送 REQUEST。
八、超时、取消和迟到数据
每次 REQUEST 会进入超时队列。周期检查时:
- 新请求可以 supersede 同一 block 的旧超时记录;
- 已经不 active 的记录直接删除;
- 超时仍 active 的请求调用 cancel,并把 block 还给 Wishlist。
当前 Wishlist 主路径避免同时向多个 Peer 请求同一 block。超时、reject、choke 或断连发生后,原请求先被取消,block 再回到 Wishlist,之后才可能交给另一个 Peer。旧 Peer 仍可能在取消后迟到地发送 PIECE,因此接收侧必须识别重复或迟到 block,不能重复增加完成度。
九、LTEP:一个扩展协议容器
扩展消息 id 只是外层 EXTENDED,真正子协议 id 在扩展握手的 bencode map 中协商。Transmission 主要使用:
ut_metadata:magnet metadata 分片请求与响应;ut_pex:Peer 增量交换;- upload-only/metadata_size/reqq 等能力字段;
- holepunch 等可选扩展。
本地不能假设自己的扩展 id 与远端相同;发送时必须使用对端握手里分配的 id。
十、Pump:读写与定时动作汇合
消息对象的周期 pulse()/pump 会做:
- 检查 request timeout;
- 更新请求窗口并补发 block requests;
- 请求 magnet metadata;
- 发送待处理 metadata piece;
- 服务对端上传请求;
- 必要时发送 keepalive;
- 刷出 protocol messages。
真正的字节数仍由 peerIo 的 write buffer space 和 bandwidth clamp 控制。消息层决定“想发什么”,I/O/带宽层决定“现在能发多少”。
十一、不可信输入的防线
协议层最值得学习的不是 happy path,而是大量边界检查:
- message length 与 payload 长度;
- piece index、offset、length 与总大小;
- bitfield 长度;
- bencode 扩展消息深度与字段类型;
- metadata piece 数与大小;
- PEX 最大 Peer 数;
- request queue 上限;
- 对不符合当前 choke/interest 状态的消息做拒绝或忽略。
网络 parser 的正确目标不是“能解析正常客户端”,而是“任何字节序列都只能让状态机前进、等待或安全失败”。
源码锚点
libtransmission/peer-msgs.h:消息对象接口libtransmission/peer-msgs.cc:基础消息、LTEP、请求窗口与 pumplibtransmission/peer-common.h:tr_peer_event与 Peer 抽象libtransmission/peer-io.cc:读写 buffer 驱动libtransmission/torrent-magnet.cc:ut_metadata的 Torrent 侧状态