0%

Kademlia_DHT_KRPC_BitTorrent协议(二)

4. BitTorrent协议

BitTorrent 使用”分布式哈希表”(DHT)来为无 tracker 的种子(torrents)存储 peer 之间的联系信息。这样每个 peer 都成了 tracker。这个协议基于 Kademila 网络并且在 UDP 上实现。

1
2
3
1. "peer" 是在一个 TCP 端口上监听的客户端/服务器,它实现了 BitTorrent 协议 
2. "节点" 是在一个 UDP 端口上监听的客户端/服务器,它实现了 DHT(分布式哈希表) 协议
DHT 由节点组成,它存储了 peer 的位置。BitTorrent 客户端包含一个 DHT 节点,这个节点用来联系 DHT 中其他节点,从而得到 peer 的位置,进而通过 BitTorrent 协议下载

每个节点有一个全局唯一的标识符,作为 “node ID”。节点 ID 是一个随机选择的 160bit(20字节) 空间,BitTorrent infohash 也使用这样的 160bit 空间。”距离”用来比较两个节点 ID 之间或者节点 ID 和 infohash 之间的”远近”(节点和节点、节点和文件之间的距离)。节点必须维护一个路由表,路由表中含有一部分其它节点的联系信息。其它节点距离自己越近时,路由表信息越详细。因此每个节点都知道 DHT 中离自己很”近”的节点的联系信息,而离自己非常远的 ID 的联系信息却知道的很少
在 Kademlia 网络中,距离是通过异或(XOR)计算的,结果为无符号整数。distance(A, B) = |A xor B|,值越小表示越近

  1. 当节点要为 torrent(种子文件) 寻找 peer(保存了目标资源的IP) 时,它将自己路由表中的节点 ID 和 torrent 的 infohash(资源HASH) 进行”距离对比”(节点和目标文件的距离),然后向路由表中离 infohash 最近的节点发送请求,问它们正在下载这个 torrent 的 peer 的联系信息

  2. 因为资源HASH和节点HASH都共用一套20bytes的命名空间,所以DHT节点充当了peer节点的”代理”的工作,我们不能直接向peer节点发起资源获取请求(即使这个peer节点确实存储了我们的目标资源),因为peer节点本身不具备处理P2P request/response能力的,我们需要借助DHT的能力,让DHT告诉我们哪个peer节点保存了我们想要的资源或者哪个DHT节点可能知道从而递归地继续去问那个DHT网络

  3. 如果一个被联系的节点知道下载这个 torrent 的 peer 信息,那个 peer 的联系信息将被回复给当前节点。否则,那个被联系的节点则必须回复在它的路由表中离该 torrent 的 infohash 最近的节点的联系信息,(get_peers)

  4. 最初的节点重复地请求比目标 infohash 更近的节点,直到不能再找到更近的节点为止

  5. 查询完了之后,客户端把自己作为一个 peer 插入到所有回复节点中离种子最近的那个节点中,这一步背后的含义是: 我之前是请求这个资源的人,我们现在获取到资源了,我在下载这个文件的同时,我也要充当一个新的peer来向其他的客户端贡献自己的文件共享,这样,当另外的其他客户端在发起新的请求的时候,DHT节点就有可能把当前客户端对应的peer返回给新的请求方,这样不断发展下去,这个资源的热度就越来越热,下载速度也越来越快(announce_peer)

  6. 请求 peer 的返回值包含一个不透明的值,称之为”令牌(token)”

  7. 如果一个节点宣布它所控制的 peer 正在下载一个种子(即该节点拥有该文件资源),它必须在回复请求节点的同时,附加上对方向我们发送的最近的”令牌(token)”。这样当一个节点试图”宣布”正在下载一个种子时,被请求的节点核对令牌和发出请求的节点的 IP 地址。这是为了防止恶意的主机登记其它主机的种子。由于令牌仅仅由请求节点返回给收到令牌的同一个节点,所以没有规定他的具体实现。但是令牌必须在一个规定的时间内被接受,超时后令牌则失效。在 BitTorrent 的实现中,token 是在 IP 地址后面连接一个 secret(通常是一个随机数),这个 secret 每五分钟改变一次,其中 token 在十分钟以内是可接受的

这种握手验证的原理是:

请求方生成一个随机值,跟着我的请求发给被请求方,被请求方回复的时候要带上这个随机值,那请求方就知道,你是我刚才想请求的那个人

0x1: 路由表 Routing Table

  1. 每个节点维护一个路由表保存已知的好节点。路由表中的节点是用来作为在 DHT 中请求的起始点。路由表中的节点是在不断的向其他节点请求过程中,对方节点回复的。即DHT中的K桶中的节点,当我们请求一个目标资源的时候,我们根据HASH XOR从自己的K桶中选择最有可能知道该资源的节点发起请求,而被请求的节点也不一定知道目标资源所在的peer,这个时候被请求方会返回一个新的”它认为可能知道这个peer的节点”,请求方收到这个新的节点后,会把这个节点保存进自己的K桶内,然后继续发起请求,直到找到目标资源所在的peer为止

  2. 并不是我们在请求过程中收到的节点都是平等的,有的节点是好的,而另一些则不是。许多使用 DHT 协议的节点都可以发送请求并接收回复,但是不能主动回复其他节点的请求,这种节点被称之为”坏节点”

  3. 节点的路由表只包含已知的好节点,这很重要。好节点是指在过去的 15 分钟以内,曾经对我们的某一个请求给出过回复的节点(存活好节点),或者曾经对我们的请求给出过一个回复(不用在15分钟以内),并且在过去的 15 分钟给我们发送过请求。上述两种情况都可将节点视为好节点。在 15 分钟之后,对方没有上述 2种情况发生,这个节点将变为可疑的。当节点不能给我们的一系列请求给出回复时,这个节点将变为坏的。相比那些未知状态的节点,已知的好节点会被给于更高的优先级。(看源码确实是这样的)

    这就反过来告诉我们,如果我们要做DHT嗅探,我们的嗅探器除了要能够发出FIND_NODE请求及接收返回之外,还需要能够响应其他节点发来的请求(get_peers/announce_peer),这样才不会被其他节点列入”可疑”甚至”坏节点”列表中

  4. 路由表覆盖从 0 到 2^160 全部的节点 ID 空间。路由表又被划分为桶(bucket),每个桶包含一部分的 ID 空间。空的路由表只有一个桶,它的 ID 范围从 min=0 到 max=2^160。当 ID 为 N 的节点插入到表中时,它将被放到 ID 范围在 min <= N < max 的 桶 中

  5. 空的路由表只有一个桶,所以所有的节点都将被放到这个桶中。每个桶最多只能保存 K 个节点,当前 K=8。当一个桶放满了好节点之后,将不再允许新的节点加入,除非我们自身的节点 ID 在这个桶的范围内。在这样的情况下,这个桶将被分裂为 2 个新的桶,每个新桶的范围都是原来旧桶的一半。原来旧桶中的节点将被重新分配到这两个新的桶中。如果一个新表只有一个桶,这个包含整个范围的桶将总被分裂为 2 个新的桶,每个桶的覆盖范围从 0..2^159 和 2^159..2^160 以log2N的方式不断分裂,类似于Kademlia中的K桶机制

  6. 当桶装满了好节点,新的节点会被丢弃。一旦桶中的某个节点变为了坏的节点,那么我们就用新的节点来替换这个坏的节点。如果桶中有在 15 分钟内都没有活跃过的节点,我们将这样的节点视为可疑的节点,这时我们向最久没有联系的节点发送 ping。如果被 ping 的节点给出了回复,那么我们向下一个可疑的节点发送 ping,不断这样循环下去,直到有某一个节点没有给出 ping 的回复,或者当前桶中的所有节点都是好的(也就是所有节点都不是可疑节点,他们在过去 15 分钟内都有活动)。如果桶中的某个节点没有对我们的 ping 给出回复,我们最好再试一次(再发送一次 ping,因为这个节点也许仍然是活跃的,但由于网络拥塞,所以发生了丢包现象,注意 DHT 的包都是 UDP 的),而不是立即丢弃这个节点或者直接用新节点来替代它。这样,我们得路由表将充满稳定的长时间在线的节点

  7. 每个桶都应该维持一个 lastchange 字段来表明桶中节点的”新鲜”度。当桶中的节点被 ping 并给出了回复,或者一个节点被加入到了桶,或者一个节点被新的节点所替代,桶的 lastchange 字段都应当被更新。如果一个桶的 lastchange 在过去的 15 分钟内都没有变化,那么我们将更新它。这个更新桶操作是这样完成的

    • 从这个桶所覆盖的范围中随机选择一个 ID,并对这个 ID 执行 find_nodes 查找操作。
    • 常常收到请求的节点通常不需要常常更新自己的桶, 反之,不常常收到请求的节点常常需要周期性的执行更新所有桶的操作,这样才能保证当我们用到 DHT 的时候,里面有足够多的好的节点
  8. 在插入第一个节点到路由表并启动服务后,这个节点应试着查找 DHT 中离自己更近的节点,这个查找工作是通过不断的发出 find_node 消息给越来越近的节点来完成的,当不能找到更近的节点时,这个扩散工作就结束了

  9. 路由表应当被启动工作和客户端软件保存(也就是启动的时候从客户端中读取路由表信息,结束的时候客户端软件记录到文件中)

0x2: BitTorrent 协议扩展 BitTorrent Protocol Extension

BitTorrent 协议已经被扩展为可以在通过 tracker 得到的 peer 之间互相交换节点的 UDP 端口号(也就是告诉对方我们的 DHT 服务端口号),在这样的方式下,客户端可以通过下载普通的种子文件来自动扩展 DHT 路由表(我直接知道某个节点有某一个资源)。新安装的客户端第一次试着下载一个无 tracker 的种子时,它的路由表中将没有任何节点,这是它需要在 torrent 文件中找到联系信息

  1. peers 如果支持 DHT 协议就将 BitTorrent 协议握手消息的保留位的第 8 字节的最后一位置为 1
  2. 这时如果 peer 收到一个 handshake 表明对方支持 DHT 协议,就应该发送 PORT 消息。它由字节 0x09 开始,payload 的长度是 2 个字节,包含了这个 peer 的 DHT 服务使用的网络字节序的 UDP 端口号
  3. 当 peer 收到这样的消息时应当向对方的 IP 和消息中指定的端口号的节点发送 ping
  4. 如果收到了 ping 的回复,那么应当使用上述的方法将新节点的联系信息加入到路由表中

0x3: Torrent 文件扩展 Torrent File Extensions(种子文件)

一个无 tracker 的 torrent 文件字典不包含 announce 关键字,而使用 nodes 关键字来替代。这个关键字对应的内容应该设置为 torrent 创建者的路由表中 K 个最接近的节点(可供选择的),这个关键字也可以设置为一个已知的可用节点(这意味着接收到这个种子文件的客户端能够向这些节点发出解析请求,询问资源的所在位置),比如这个 torrent 文件的创建者.

请不要自动加入 router.bittorrent.com 到 torrent 文件中或者自动加入这个节点到客户端路由表中。这里可以仔细思考一下,这么做还有另一个好处,这个对等网络可以保持无中心化,对于外部新加入的新节点来说,它可以不用通过”中心引导节点”来加入网络,隐藏了”中心引导节点”的存在,增强了对等网络的隐蔽性

bt 种子文件是使用 bencode 编码的,整个文件就 dictionary,包含以下键

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
1. info(dictinary): 必选, 表示该bt种子文件的文件信息 
1) 文件信息包括文件的公共部分
1.1) piece length(integer): 必选, 每一数据块的长度
1.2) pieces(string): 必选, 所有数据块的 SHA1 校验值
1.3) publisher(string): 可选, 发布者
1.4) publisher.utf-8(string): 可选, 发布者的 UTF-8 编码
1.5) publisher-url(string): 可选, 发布者的 URL
1.6) publisher-url.utf-8(string): 可选, 发布者的 URL 的 UTF-8 编码
2) 如果 bt 种子包含的是单个文件,包含以下内容
2.1) name(string): 必选, 推荐的文件名称
2.2) name.utf-8(string): 可选, 推荐的文件名称的 UTF-8 编码
2.3) length(int): 必选,文件的长度单位是字节
3) 如果是多文件,则包含以下部分:
3.1) name(string): 必选, 推荐的文件夹名称
3.2) name.utf-8(string): 可选, 推荐的文件名称的 UTF-8 编码
3.3) files(list): 必选, 文件列表,每个文件列表下面是包括每一个文件的信息,文件信息是个字典
4) 文件字典
4.1) length(int): 必选,文件的长度单位是字节
4.2) path(string): 必选,文件名称,包含文件夹在内
4.3) path.utf-8(string): 必选,文件名称 UTF-8 表示,包含文件夹在内
4.4) filehas(string): 可选,文件hash
4.5) ed2k(string): 可选, ed2k 信息

2. announce(string): 必选, tracker 服务器的地址
3. announce-list(list): 可选, 可选的 tracker 服务器地址
4. creation date(interger): 必选, 文件创建时间
5. comment(string): 可选, bt 文件注释
6. created by(string): 可选,文件创建者

pieces是一个字符串,它的长度是20的倍数,每一段20个字符表示对应文件块的sha1 hash值。

这里要特别注意一点:磁力链接的infohash也是根据info字段来计算的,info字段的pieces为每个数据块的校验值,其作用是验证下载下来的文件是否正确,如果下载下来的文件块计算出来的SHA1值和pieces中的SHA1校验值不一致,该数据块要重新下载。 所以,我们可以看出根据磁力链接下载文件是分成两个步骤的

  1. 先根据infohash下载种子文件的info字段,种子文件并不是必须的,但是info字段却必不可少
  2. 然后根据infohash下载源文件,将下载的每一个数据块和info中的对应的SHA1校验码进行比较,不一致重新下载该数据块

需要注意的是

  1. 一般的种子文件会包含announce,也就是tracker服务器的地址(trackerless是BTTorrent的趋势)
  2. 如果没有tracker服务器,文件中可能会包含nodes,nodes是存有种子信息的peer节点,这样的种子文件就是trackerless torrent。如果有nodes客户端直接从nodes获取种子信息
  3. 而从DHT网络中下载下来的种子文件既没有annouce也没有nodes,客户端只能通过info字段计算出hashinfo,再从bootstrap node节点开始在DHT网络中寻找种子信息

BT原生依靠Tracker,后来才加入dht

5. uTP协议

uTP协议是一个基于UDP的开放的BT点对点文件共享协议。在uTP协议出现之前,BT下载会占用网络中大量的链接,直接导致其它网络应用服务质量下载和网络的拥堵,因此有很多ISP都开始限制BT的下载。uTP减轻了网络延迟并解决了传统的基于TCP的BT协议所遇到的拥塞控制问题,提供可靠的有序的传送。

一个有效的uTP数据包包含下面格式的报头

1

  1. type(包类型):

    1
    2
    3
    4
    5
    1) ST_DATA = 0: 最重要的数据包,uTP就是使用该类型的包传送数据
    2) ST_FIN = 1: 关闭连接,这是uTP连接的最后一个包,类似于TCP中的FIN
    3) ST_STATE = 2: 简单的应答包,表明已从对方收到了数据包,该包不包含任何数据,seq_nr值不变
    4) ST_RESET = 3: 终止连接,类似于TCP中的RST
    5) ST_SYN = 4: 初始化连接,类似于TCP中的SYN,这是uTP连接的第一个包
  2. ver: This is the protocol version. The current version is 1.

  3. extension: The type of the first extension in a linked list of extension headers.

    1
    2
    1) 0 means no extension.
    2) Selective acks: There is currently one extension:
  4. connection_id: This is a random, unique, number identifying all the packets that belong to the same connection. Each socket has one connection ID for sending packets and a different connection ID for receiving packets. The endpoint initiating the connection decides which ID to use, and the return path has the same ID + 1.

    uTP的一个很重要的特点是使用connection id来标识一次连接,而不是每个包算一次连接。所以在分析ST_DATA时,需要注意找所有connection id相同的数据包,然后按seq_nr排序,seq_nr应该是依次递增的(注意ST_STATE包不会增加seq_nr值),如果发现两个ST_DATA的seq_nr值相同则说明后面那个报文是重复报文需要忽略掉,如果发现两个ST_DATA的seq_nr值不是连续的,中间差了一个或多个,则可能是由于网络原因发生了丢包现象,数据包将不可用

  5. timestamp_microseconds: This is the ‘microseconds’ parts of the timestamp of when this packet was sent. This is set using gettimeofday() on posix and QueryPerformanceTimer() on windows. The higher resolution this timestamp has, the better. The closer to the actual transmit time it is set, the better.

  6. timestamp_difference_microseconds: This is the difference between the local time and the timestamp in the last received packet, at the time the last packet was received. This is the latest one-way delay measurement of the link from the remote peer to the local machine.
    When a socket is newly opened and doesn’t have any delay samples yet, this must be set to 0.

  7. wnd_size: Advertised receive window. This is 32 bits wide and specified in bytes. The window size is the number of bytes currently in-flight, i.e. sent but not acked. The advertised receive window lets the other end cap the window size if it cannot receive any faster, if its receive buffer is filling up. When sending packets, this should be set to the number of bytes left in the socket’s receive buffer.

  8. seq_nr

  9. ack_nr

在uTP连接建立之后,就开始传送需要的数据了。peer和peer之间传送数据也是遵循着一定的规范,就是Peer Wire协议。

6. Peer Wire协议

在BitTorrent中,节点的寻址是通过DHT实现的,而实际的资源共享和传输则需要通过uTP以及Peer Wire协议来配合完成

0x1: 握手

Peer Wire协议是Peer之间的通信协议,通常由一个握手消息开始。握手消息的格式是这样的

1
<pstrlen><pstr><reserved><info_hash><peer_id>

在BitTorrent协议的v1.0版本, pstrlen = 19, pstr = “BitTorrent protocol”,info_hash是上文中提到的磁力链接中的btih,peer_id每个客户端都不一样,但是有着一定的规则,根据前面几个字符可以推断出客户端的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
'AG' - Ares
'A~' - Ares
'AR' - Arctic
'AV' - Avicora
'AX' - BitPump
'AZ' - Azureus
'BB' - BitBuddy
'BC' - BitComet
'BF' - Bitflu
'BG' - BTG (uses Rasterbar libtorrent)
'BR' - BitRocket
'BS' - BTSlave
'BX' - ~Bittorrent X
'CD' - Enhanced CTorrent
'CT' - CTorrent
'DE' - DelugeTorrent
'DP' - Propagate Data Client
'EB' - EBit
'ES' - electric sheep
'FT' - FoxTorrent
'FX' - Freebox BitTorrent
'GS' - GSTorrent
'HL' - Halite
'HN' - Hydranode
'KG' - KGet
'KT' - KTorrent
'LH' - LH-ABC
'LP' - Lphant
'LT' - libtorrent
'lt' - libTorrent
'LW' - LimeWire
'MO' - MonoTorrent
'MP' - MooPolice
'MR' - Miro
'MT' - MoonlightTorrent
'NX' - Net Transport
'PD' - Pando
'qB' - qBittorrent
'QD' - QQDownload
'QT' - Qt 4 Torrent example
'RT' - Retriever
'S~' - Shareaza alpha/beta
'SB' - ~Swiftbit
'SS' - SwarmScope
'ST' - SymTorrent
'st' - sharktorrent
'SZ' - Shareaza
'TN' - TorrentDotNET
'TR' - Transmission
'TS' - Torrentstorm
'TT' - TuoTu
'UL' - uLeecher!
'UT' - µTorrent
'VG' - Vagaa
'WD' - WebTorrent Desktop
'WT' - BitLet
'WW' - WebTorrent
'WY' - FireTorrent
'XL' - Xunlei
'XT' - XanTorrent
'XX' - Xtorrent
'ZT' - ZipTorrent

Peer Wire协议是在uTP协议基础上里层应用态协议。收到握手消息后,对方也会回复一个握手消息,并且开始协商一些基本的信息。

7. BitTorrent协议扩展

BitTorrent协议扩展与 ut_metadata和ut_pex(Extension for Peers to Send Metadata Files) (磁力链接核心)

1
2
BEP:9 		Title:	Extension for Peers to Send Metadata Files
BEP:10 Title: Extension Protocol

借助于DHT/KRPC完成了的Node节点寻址,资源对应的Peer获取,以及uTP以及Peer Wire完成握手之后,接下要就要”动真格”了,我们需要获取到目标资源的”种子信息(infohash/filename/pieces分块sha1)”了,这个扩展的目的是为了在最初没有.torrent文件的情况仍然能够加入swarm并能够完成下载。这个扩展能让客户端从peer哪里下载metadata。这让支持magnet link成为了可能,magnet link是一个web页上的链接,仅仅包含了足够加入swarm的足够信息(info hash)

0x1: Metadata

这个扩展仅仅传输.torrent文件的info-字典字段,这个部分可以由infohash来验证。在这篇文档中,.torrent的这个部分被称为metadata。

Metadata被分块,每个块有16KB(16384字节),Metadata块从0开始索引,所有快的大小都是16KB,除了最后一个块可能比16KB小

0x2: Extension头部

Metadata扩展使用extension协议(BEP0010)来声称它的存在。它在extension握手消息的头部m字典加入ut_metadata项。它标识了这个消息可以使用这个消息码,同时也可以在握手消息中加入metadata_size这个整型字段(不是在m字典中)来指定metadata的字节数

1
{'m': {'ut_metadata', 3}, 'metadata_size': 31235}

0x3: Extension消息

Extension消息都是bencode编码,这里有3类不同的消息

  • request 0:

请求消息并不在字典中附加任何关键字,这个消息的回复应当来自支持这个扩展的peer,是一个reject或者data消息,回复必须和请求所指出的片相同
Peer必须保证它所发送的每个片都通过了infohash的检测。即直到peer获得了整个metadata并通过了infohash的验证,才能够发送片(即一个peer应该保证自己已经完整从其他peer中拷贝了一份相同的资源文件后,才能继续响应其他节点的拷贝请求)。Peers没有获得整个metadata时,对收到的所有metadata请求都必须直接回复reject消息

1
2
3
{'msg_type': 0, 'piece': 0}
d8:msg_typei0e5:piecei0ee
# 这代表请求消息在请求metadata的第一片
  • data 1

这个data消息需要在字典中添加一个新的字段,”total_size”.这个关键字段和extension头的”metadata_size”有相同的含义,这是一个整型

Metadata片被添加到bencode字典后面,他不是字典的一部分,但是是消息的一部分(必须包括长度前缀)。
如果这个片是metadata的最后一个片,他可能小于16KB。如果它不是metadata的最后一片,那大小必须是16KB

1
2
3
{'msg_type': 1, 'piece': 0, 'total_size': 3425}
d8:msg_typei1e5:piecei0e10:total_sizei34256eexxxxxxxx...
# x表示二进制数据(metadata)
  • reject 2

Reject消息没有附件的关键字。它的意思是peer没有请求的这个metadata片信息

在客户端收到收到一定数目的消息后,可以通过拒绝请求消息来进行洪泛攻击保护。尤其在metadata的数目乘上一个因子时

1
2
{'msg_type': 2, 'piece': 0}
d8:msg_typei1e5:piecei0ee

0x4: request消息: Metadat信息获取过程

  • 扩展支持交互(互相询问对方支持哪些扩展)

根据BEP-010我们知道,扩展消息一般在Peer Wire握手之后立即发出,是一个B编码的字典

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
e: 0,
ipv4: xxx,
ipv6: xxx,
complete_ago: 1,
m:
{
upload_only: 3,
lt_donthave: 7,
ut_holepunch: 4,
ut_metadata: 2,
ut_pex: 1,
ut_comment: 6
},
matadata_size: 45377,
p: 33733,
reqq: 255,
v: BitTorrent 7.9.3
yp: 19616,
yourip: xxx
}

1. m: 是一个字典,表示客户端支持的所有扩展以及每个扩展的编号
1) ut_pex: 表示该客户端支持PEX(Peer Exchange)
2) ut_metadata表示支持BEP-009(也就是交换种子文件的metadata)
  • 握手handshake

我们在完成双方握手之后,并且得到了对方支持的扩展信息。资源请求方也通知被请求方本机支持的扩展情况,然后后面接着一个扩展消息(从上面的m字典可以看到可能会有多种不同的扩展消息),具体是哪个类型的扩展消息由message ID后面那个数字决定,这个数字对应着m字典中的编号。譬如我们这里的消息是

1
2
3
4
00 00 00 1b 14 02 ... 00 00 00 1b 
1. 消息长度为 0x1b (27 bytes)
2. 14 表示是 扩展消息(0x14 = 20)
3. 02 对应上面m字典中的 ut_metadata,所以我们这个消息是ut_metadata消息

再次看上图的截图,我们这里的图显示的是[msg_type: 0, piece: 2]正是request消息,意思是向对象请求第二个piece的数据,piece的意思是分块的意思,根据BEP-009我们知道,种子文件的metadata(也就是info部分)会按16KB分成若干块,除最后一块每一块的大小都是16KB,每一块从0开始按顺序进行编号。所以这个请求的意思就是向对象请求第三块的metadata

  • 回复data信息

从图中形象的表示可以看到torrent文件整个info的长度为45377,这个值正是上面握手报文后的扩展消息中的metadata_size的值。在发送request消息之后,接下来对方应该回复data消息(如果对方有数据)或reject消息(如果对方没有数据)。

msg_type为1表示是回复就是我所需要的数据,但是注意这里的数据并没完,由于uTP协议的缘故,我们可以根据connection id找到这个连接后续的所有数据。 这里其实一共收到了三个消息,我们分别来看一下

1
2
3
00 00 00 03 09 83 c5 --> message ID为9,port消息,表示端口号为0x83c5 = 33733
00 00 00 03 14 03 01 --> message ID为20(0x14),extend消息,编号03为upload_only,表示设置upload_only = 1
00 00 31 70 14 02 xx --> message ID为20(0x14),extend消息,编号02为ut_metadata,后面的xx表示[msg_type: 1, piece: 2, total_size: 45377]和相应块的metadata数据

看第三个消息可以知道消息长度为0x3170,这个长度包括了[msg_type…]这一串字符串的长度,共0x2f个字节,我们将其减去就得到了piece2的长度:0x3170 - 0x2f = 0x3141 我们上面说过每个块的大小应该是16KB,也就是0x4000,这里的大小为0x3141,只可能是最后一块。我们稍微计算验证下,将整个info的长度45377(0xb141)按16KB分块

1
2
3
piece 0: 0x0001 ~ 0x4000 长度0x4000
piece 1: 0x4001 ~ 0x8000 长度0x4000
piece 2: 0x8001 ~ 0xb141 长度0x3141

可以看到piece2正是最后一块,大小为0x3141。至此我们得到了第二块的metadata,然后通过request消息获取piece0和piece1获取第一和第二块的metadata,将三块的消息合并成torrent文件info字段,然后再加上create date、create by或comment等信息,种子文件就算完成下载了。可见要在BT网络中完成实际的资源下载,就必须完整获取到种子文件,因为种子文件中不单有infohash值,还有piece sha1校验码,分块下载时需要进行校验,而磁力连接magnet只是一个最小化入口,最终还是需要通过磁力连接在DHT网络中获取种子文件的完整信息

0x5: 校验info_hash

我们将从DHT网络中下载的种子文件和原始的种子文件进行比较,可以看到annouce和annouce-list字段都丢掉了(引入了DHT网络后,BT可以实现Trackerless),create date发生了变化,info字段不变

磁力链是为了简化BT种子文件的分发,封装了一个简化版的magnet url,客户端解析这个magnet磁力链之后,需要在DHT网络中寻找infohash对应的peer节点,获取节点成功后,向目标peer节点获取真正的BitTorrent种子(.torrent文件)信息(包含了完整的pieces SHA1杂凑信息),另一个渠道就是传统的Bt种子论坛会分发.BT种子文件

8. 参考资料

给作者打赏,可以加首页微信,咨询作者相关问题!