BT握手与TCP的连接与释放
定位了DHT爬虫的一个问题,在这梳理一下BT握手流程
BitTorrent 客户端(假设为 A)与另一个 Peer(假设为 B)从建立 TCP 连接开始,经过标准握手、扩展握手,最终到通过 ut_metadata
扩展获取种子元数据的完整流程
0.1 前提:
- 客户端 A 知道 Peer B 的 IP 地址和监听端口 (IP_B, Port_B)。
- 客户端 A 知道它感兴趣的种子的
info_hash
。 - 客户端 A 没有这个种子的元数据通过磁力连接向B请求这个种子的信息
- 我们假设 Peer B 拥有这个种子的元数据,并且支持 BEP-10 扩展协议和
ut_metadata
扩展。 - 环境是我本地的环境这里A的地址为192.168.30.125:59806, B的地址是192.168.30.230:63219
0.2 流程步骤:
整体的流程如图,除了最后一次挥手没等停止抓包了,
图中长度66的都是没有其他信息的, dMAC(6)+sMAC(6)+type(2)(ipv4 0x0800) = 14 IP 头 5 * 4 = 20 TCP 头 5 * 4 = 20 + 可选 12 字节(主要是默认开的时间戳10B) 共66B
0.3 阶段一:建立 TCP 连接
0.3.0.1 1. A 发起连接:
- 客户端 A 的操作系统向 Peer B 的 (IP_B, Port_B) 发送一个 TCP SYN 包。
0.3.0.2 2. B 响应连接:
- Peer B 的操作系统接收到 SYN 包,如果端口正在监听且允许连接,则发送一个 TCP SYN-ACK 包 回给 A。
0.3.0.3 3. A 确认连接:
- 客户端 A 的操作系统接收到 SYN-ACK 包,发送一个 TCP ACK 包 给 B。
- 结果: TCP 三次握手完成。A 和 B 之间建立了一条可靠的、面向连接的 TCP 通道。现在可以互相发送应用层数据了。
0.4 阶段二:BitTorrent 标准握手 (BEP-3)
0.4.0.4 4. A 发送握手信息(第106号):
- TCP 连接建立后,客户端 A 立即 构造标准的 68 字节 BitTorrent 握手消息:
<"BitTorrent protocol"><8 reserved bytes><info_hash><peer_id_A>
- 其中
reserved
字节的第 5 字节的位 4 设置为 1,表示 A 支持 BEP-10 扩展协议如果双方都在这个位上设置为1
,它们就可以在标准握手之后立即交换扩展握手消息,从而协商使用如ut_metadata
(元数据交换),ut_pex
(Peer Exchange) 等多种现代 BitTorrent 扩展功能。 - 字节 7 (索引为 7), 位 3 (第 3 位)。掩码 (Mask)
0x04
(十六进制) 或00000100
(二进制) - 设置此位置表明该 Peer 支持 BEP-4 DHT Protocol (DHT 协议)。表明客户端理解并能够参与 DHT 网络。如果设置了此位,意味着该 Peer 可能运行一个 DHT 节点,并且(根据 BEP-4)理论上可以在与 BitTorrent P2P 流量相同的端口上响应 DHT 查询,不过我们KRPC监听端口和tcp端口不一样所以没有设置,设置此位是加入 Mainline DHT 网络的前提。
- A 将这 68 字节数据交给其 TCP 协议栈,发送 TCP 数据包 给 B。
0.4.0.5 5. B 接收并验证握手信息:
- Peer B 的 TCP 协议栈 接收 TCP 数据包,并将数据递交给 BitTorrent 应用层。
- B 检查协议字符串是否为 “BitTorrent protocol”,检查
info_hash
是否是它关心的种子。假设匹配。 - B 记录下 A 的
peer_id_A
。 - B 检查 A 发来的
reserved
字节,发现 A 支持 BEP-10。
0.4.0.6 6. B 发送握手信息(108):
- Peer B 构造自己的 68 字节标准握手消息:
<"BitTorrent protocol"><8 reserved bytes><info_hash><peer_id_B>
(B 也在reserved
字节中表明自己支持 BEP-10)。 - B 发送 TCP 数据包 给 A。
0.4.0.7 7. A 接收并验证握手信息(只读取68, 可见108大小为333):
- 客户端 A 的 TCP 协议栈 接收 TCP 数据包。
- A 验证协议字符串和
info_hash
。 - A 记录下 B 的
peer_id_B
。 - A 检查 B 发来的
reserved
字节,确认 B 也支持 BEP-10。 - 结果: 标准握手完成。双方确认了对方身份和意图(针对同一个
info_hash
),并且都知晓对方支持扩展协议。 - 这里有点不一样的是,BT客户端的实现互不相同,有的会等A发送扩展握手后在回应,有的直接在握手包后面回复扩展握手了,这里注意一下,前面读取握手包只读取68字节,剩下的,扩展握手时候在处理,qbt就是这种。
0.5 阶段三:扩展协议握手 (BEP-10) - 条件性
- 因为 A 和 B 都支持 BEP-10,所以此阶段会进行。_
0.5.0.8 8. A 发送扩展握手(110):
- 扩展握手很简单,不过要注意ID, 双方后续通信需要使用对方握手时候使用的ut_metadata ID 而且不能取0
- 客户端 A 构造 BEP-10 扩展握手消息 (消息 ID 固定为
0
):<长度前缀><Bencoded 载荷>
载荷是一个 Bencode 字典,至少包含m
字典,例如{"m": {"ut_metadata": 1}}
(表示 A 希望 B 在发送ut_metadata
消息给 A 时使用 ID1
)。可能还包含v
(版本),metadata_size
(如果 A 碰巧有元数据的话,但在此场景下 A 没有,所以不含或为 0) 等。 - A 发送 TCP 数据包 给 B。
0.5.0.9 9. B 接收并处理扩展握手:
- Peer B 接收 TCP 数据包。
- B 解析载荷,记录下 A 要求使用的扩展 ID 映射 (例如,知道发送
ut_metadata
给 A 要用 ID1
)。记录 A 的版本等信息。
0.5.0.10 10. B 发送扩展握手:
- Peer B 构造自己的 BEP-10 扩展握手消息 (消息 ID 固定为
0
):<长度前缀><Bencoded 载荷>
载荷包含 B 的m
字典,例如{"m": {"ut_metadata": 3}, "metadata_size": 9308}
(表示 B 希望 A 在发送ut_metadata
给 B 时使用 ID3
,并且 B 宣告它拥有的元数据大小为 12345 字节)。 - B 发送 TCP 数据包 给 A。
- A 接收并处理扩展握手(108后半部分):
- 客户端 A 接收 TCP 数据包。
- A 解析载荷,记录下 B 要求使用的扩展 ID 映射 (知道发送
ut_metadata
给 B 要用 ID2
)。 - A 从 B 的握手消息中获知了元数据的确切大小 (
metadata_size: 9308
)。 - 扩展协议握手完成。双方知道了如何互相发送扩展消息(特别是
ut_metadata
),并且 A 知道了元数据的总大小。 - 解释一下其余字段,ut_pex 是节点交换协议标识位,reqq是说明piece分片请求中,由于可以流水线处理,最大支持的处理数,v 可选是版本, your ip 是对方看到的IP
0.6 阶段四:元数据交换 (ut_metadata
, BEP-9) - 条件性
- 因为 A 需要元数据,且 B 支持
ut_metadata
并拥有元数据,所以此阶段会进行。A 知道需要向 B 发送 ID 为3
的消息来请求元数据。_
0.6.0.11 12. A 请求元数据块(1002):
-
客户端 A 根据获知的
metadata_size
(9308 字节) 和标准块大小 (16 KB = 16384 字节),计算出需要请求多少个元数据块 (我们这个就1快儿)。假设元数据大小是 30000 字节,则需要请求块 0 和块 1。 -
A 开始请求第一个元数据块 (索引
0
):-
构造
ut_metadata
请求载荷 (Bencode 字典):{"msg_type": 0, "piece": 0}
-
构造 BEP-10 消息:
<长度前缀><上述 Bencoded 载荷>
(注意消息 ID 是3
) -
A 发送 TCP 数据包 给 B。
-
(如果需要更多块,A 可以连续发送对块 1, 2… 的请求,不必等待响应,这称为流水线 Pipelining)。
-
0.6.0.12 13. B 接收请求并发送数据块:
- Peer B 接收 TCP 数据包。
- B 解析消息,识别出是 ID 为
2
(对应ut_metadata
) 的消息,再解析内部载荷,看到是msg_type: 0
(请求),请求的是piece: 0
。 - B 获取元数据的第 0 块数据(从字节 0 到 16383,但在此例中只有 9308 字节)。
- B 构造
ut_metadata
数据载荷 (Bencode 字典 + 原始数据):{"msg_type": 1, "piece": 0, "total_size": 9308}
后紧跟 9308 字节的元数据块。 - B 构造 BEP-10 消息:
<长度前缀><上述 Bencode 载荷 + 原始数据块>
(注意消息 ID 是1
,这是 A 在握手中指定的 B->A 的ut_metadata
ID)。 - B 发送 TCP 数据包 给 A。
- 如下图显然我们这里tcp分包了,然后在119重组
0.6.0.13 14. A 接收并存储数据块:
- 客户端 A 接收 TCP 数据包。
- A 解析消息,识别出是 ID 为
1
(对应ut_metadata
),解析内部载荷看到是msg_type: 1
(数据),对应piece: 0
,总大小total_size: 9308
。 - A 读取随后的 9308 字节原始数据。
- A 将这块数据存储在内存中,作为元数据的第 0 块。
0.6.0.14 15. A 请求并接收后续块 (如果需要):
- 如果元数据大于 16 KiB,A 会继续发送对后续块 (
piece: 1
,piece: 2
…) 的请求,B 也会类似地响应数据块。A 持续接收并组装。
0.6.0.15 16. A 完成接收并校验:
-
当 A 接收完所有预期的块(根据
total_size
判断)后,它将所有块按顺序拼接起来,得到完整的 Bencode 编码的info
字典数据。 -
A 计算这段完整数据的 SHA-1 哈希值。
-
A 将计算出的哈希与它最初从 Magnet 链接得到的
info_hash
进行比较。 -
结果:
-
匹配: A 成功获取并验证了元数据!现在它可以解析这个
info
字典,了解文件结构、大小、分块哈希等,并开始请求实际的文件数据块。 -
不匹配: 获取元数据失败,A 可能会丢弃数据并尝试从其他 Peer 获取。
大功告成了
0.6.0.16 17. 手动关闭了连接
实际连接可能不会这么快关闭,毕竟还要传输数据
0.7 其他
实际下载中,会有些其他和拓展协议并列的消息,需要注意过滤掉,比如MsgTypeExt: 5的bitfield,用于表示B已经下载的分块。
还有就是,可见我们连续发送了多个分片元数据请求,这个请求后B开始发送元数据到A,注意超时时间不要设置的太短,如果网络情况差,可能不会很快。