Skip to content

第 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。

二、基础帧格式

握手之后的消息采用:

text
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双向取消或拒绝请求
PORTPeer → Peer告知 DHT UDP 端口
SUGGEST / ALLOWED_FASTFast Extensionchoke 状态下的有限请求能力
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() 的门槛是:

text
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 的正确目标不是“能解析正常客户端”,而是“任何字节序列都只能让状态机前进、等待或安全失败”。

源码锚点

文档采用 CC BY-SA 4.0;源码片段保留 Transmission 上游许可。