概述
BitTorrent (简称 BT) 协议是一种点对点 (Peer-to-Peer, 或简写为 P2P) 传输协议, 它被设计用来高效地分发文件 (尤其是对于大文件、多人同时下载时效率非常高), 在传统的场景下, 用户希望下载一个文件, 一般都会通过比如 HTTP / FTP 的方式从目标站点的服务器上下载, 服务器的带宽通常都是有限的, 当同时下载的用户过多时, 将超出服务器的带宽限制, 这时大家都会下载地很慢甚至是无法继续下载, 而 BitTorrent 协议便是为了解决这个问题, 它的创新点在于将文件分片, 每个终端用户下载文件分片, 在下载的同时也会互相分发自己已下载的文件分片给其它正在下载的用户, 从而将部分原本应从服务器拉取数据所造成的带宽压力分散给了终端用户, 所有正在同时下载同一文件的终端用户构成一个图结构, 互相点对点式地分发文件片段, 因此对于大文件、多人同时下载时有非常好的文件分发效率, BitTorrent 协议不是 IETF 制定的互联网标准化协议, 因而没有 RFC 文档, 关于 BitTorrent 协议标准《The BitTorrent Protocol Specification》可以从 bittorrent.org 上获得。BitTorrent 协议是架构于 TCP/IP 协议之上的一个P2P文件传输通信协议,是一个应用层协议。
- 资源描述:一个文件资源的下载是从一个BT种子(.torrent文件)或磁力链接(magnet link)开始的,在BT协议中,也被称为元数据(metainfo),元数据中包含该文件资源如何被分片,以及每个分片的大小,校验哈希等信息。
- 资源定位:用户从数据发布者获取到文件的元数据后,接下来需要通过资源定位的机制,来找到拥有该资源的peer,如果无法找到拥有该资源的peer,也就意味着下载失败。通常一个peer只包含一个文件资源的部分分片信息,所以需要找到多个peer才能完整地拼接出整个文件。
- 数据下载:在获取到拥有资源的peer列表后,需要通过peer传输协议来沟通协商各自拥有的分片资源,请求下载各自所需的分片,来完成整个资源的下载。
资源描述
BT种子(.torrent)的文件格式
BT 协议的 metainfo file 用来描述资源的元信息, 如资源的 URL, 资源的 SHA1 哈希等, 它的文件后缀为 .torrent, 也就是常所说的 BT 种子文件 (BT 种子文件中的文本内容统一使用 UTF-8 编码), BT 种子文件使用 bencoding 编码来描述信息的。
- Bencoding编码(BEP_0003)
Bencoding 共有 4 种基本数据类型, 分别是 String, Integer, List 和 Dictionary
类型 | 定义 | 示例 |
---|---|---|
String:字符串类型 | 使用形如 length-prefix : string-content 的形式来描述字符串, 其中 length-prefix 是字符串的长度, string-content 是字符串内容 | 4:spam 表示字符串 \'spam\' |
Integer:整数类型 | 使用字符 i 作为前缀, 使用字符 e 作为后缀, 在前缀 i 和后缀 e 中间的是整数本身 (整数都使用十进制表达) | i3e 表示整数 3, i-3e 代表整数 -3 |
List:列表类型 | 使用字符 l作为前缀, 使用字符 e 作为后缀, 在前缀 l和后缀 e 之间的是列表的元素 (列表元素也都使用 bencoding 编码方法) | l4:spam4:eggse 表示 [\'spam\', \'eggs\'] |
Dictionary:字典类型 | 使用字符 d 作为前缀, 使用字符 e 作为后缀, 在前缀 d 和后缀 e 之间的是 k-v 对, 其中字典的键 (key) 必须是 bencoding 编码的 String 形式, 而字典键所对应的值可以是 bencoding 编码的任何一种类型 (String, Integer, List, Dictionary) | d3:cow3:moo4:spam4:eggse 表示 {\'cow\': \'moo\', \'spam\':\'eggs\'} d4:spaml1:a1:bee 表示 {\'spam\':[\'a\', \'b\']} |
- 文件格式
BT 种子文件实际上就是一个 bencoding 编码的 Dictionary, BEP_0003 定义了两个 Key, 分别是 announce 和 info。 BEP_0012(Metadata Extension)扩展协议增加了 announce-list 字段。另外,还有一些常用,但不在协议定义范围内的字段,如注释、创建时间等。
key | value | 协议版本 | |||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
announce | BT Tracker 的 URL | BEP_0003 | |||||||||||||
announce-list | 可选, 为备用 BT Tracker 的 URL | BEP_0012 | |||||||||||||
nodes | 可选,对于无tracker的种子文件(没有announce字段),可以给出DHT的临近的K个nodes列表,来查找peer信息 | BEP_0005 | |||||||||||||
creation date | 可选, 种子的创建时间 (UNIX 时间戳), 如 13:creation datei1542799851e | ||||||||||||||
comment | 可选, 种子制作者添加的备注信息 | ||||||||||||||
created by | 可选, 该关键字对应的值为生成种子文件的 BT 客户端软件的信息, 如客户端名、版本号等 | ||||||||||||||
info_hash | info字段内容的SHA1哈希值,用于唯一标识该资源 | ||||||||||||||
info | 是一个 dictionary, 有如下 key
|
BEP_0003 |
磁力链的格式(magnet URI format)
磁力链接是一种特殊链接,通过不同文件内容的Hash结果生成一个纯文本的“数字指纹”,并用它来识别文件。例如 magnet:?xt=urn:btih:a27463ca05b1e87ecf89f95a07c13f158b5f2437
磁力链非常类似于WEB服务中用于定位资源的URL,其中使用资源的哈希码即可唯一标识资源,相比BT种子文件,非常简单。任何人都可以分发一个磁力链接来确保该链接指向的资源就是他想要的,而和得到该资源的方式无关。因为磁力链中并不需要包含tracker服务器信息,因此相比于BT种子,更难于封锁。磁力链在BEP_0009(Extension for Peers to Send Metadata Files)
-
协议中定义有2种格式:
magnet:?xt=urn:btih:<info-hash>&dn=<name>&tr=<tracker-url>&x.pe=<peer-address>
magnet:?xt=urn:btmh:<tagged-info-hash>&dn=<name>&tr=<tracker-url>&x.pe=<peer-address>
-
字段含义
字段 | 定义 |
---|---|
magnet | 必须,协议名 |
xt | 必须,exact topic的缩写,包含文件哈希值的统一资源名称。bthi(BitTorrent Info Hash)表示哈希方法名,使用40个字符的16进制编码字符串,表示160位的哈希值。info-hash是对.torrent文件中的info-hash字段进行hash计算,在磁力链中的metadata也是指info-hash的部分。btmh是multihash ,使用较少。 |
dn | 可选,display name的缩写,表示向用户显示的文件名 |
tr | 可选,tracker服务器的地址 |
x.pe | 可选,用于获取metadata的peer地址,格式为hostname:port, ipv4-literal:port 或 [ipv6-literal]:port |
在磁力链协议中,metadata指.torrent文件中的info字段的字典信息。如果参数中不包含tracker信息,则需要从DHT(BEP_0005)获取peer。
资源定位机制
有了BT种子文件或磁力链接后,接下来我们就需要找到拥有资源的peer,来完成数据的下载。BT下载的定位机制相对简单,在种子文件中的announce字段,给出了tracker地址,客户端可以通过与tracker通信的方式获取拥有资源的peer,完成下载。对于无tracker的磁力链接来说,需要通过DHT,找到拥有metadata(包含文件分配及描述信息)的peer,获取到metadata后,再进行数据的下载。
BT的资源定位机制-Tracker协议
BT Tracker 是一个注册服务, 用来协调 BT 协议的文件分发, 通过 BT Tracker 可以知道当前有哪些终端在同时下载目标资源, 当终端执行下载任务时应首先请求 BT Tracker 服务获取当前正在下载同一资源的对等结点信息, 终端用户通过 Tracker GET 请求BT Tracker 服务。(可以是 HTTP 或 BEP_0015 UDP Tracker Protocol for BitTorrent 或其它, 具体使用的协议由 .torrent 文件的 announce 对应的 Value 值给出)
Tracker协议定义在 BEP_0003
- GET请求字段
字段名 | 定义 |
---|---|
info_hash | 20 字节, 将 .torrent 文件中的 info 键对应的值生成的 SHA1 哈希, 该哈希值可作为所要请求的资源的标识符 |
peer_id | 终端生成的 20 个字符的唯一标识符, 每个进行 BT 下载的终端随机生成的 20 个字符的字符串作为其标识符 (终端应在每次开始一个新的下载任务时重新随机生成一个新的 peer_id),只需保证在客户端唯一即可 |
ip | 可选,该终端的 IP 地址(或域名), 一般情况下该参数没有必要, 因为传输层 (Transport Layer, 如 TCP) 本身可以获取 IP 地址, 但比如 BT 下载器通过 Proxy 与 Tracker 交互时, 该在该字段中设置源端的真实 IP |
port | 终端正在监听的端口 (因为 BT 协议是 P2P 的, 所以每一个下载终端也都会暴露一个端口, 供其它结点下载), BT 下载器首先尝试监听 6881 端口, 若端口被占用被继续尝试监听 6882 端口, 若仍被占用则继续监听 6883, 6884 ... 直到 6889 端口, 若以上所有端口都被占用了, 则放弃尝试 |
uploaded | 当前已经上传的文件的字节数 (十进制数字表示) |
downloaded | 当前已经下载的文件的字节数 (十进制数字表示) |
left | 当前仍需要下载的文件的字节数 (十进制数字表示),因为存在下载分片校验失败的情况,该值不能从文件长度和已下载字节数推算出来 |
numwant | 可选,希望 BT Tracker 返回的 peer 数目, 若不填, 默认返回 50 个 IP 和 Port |
event | 可选, 该参数的值可以是 started, completed, stopped, empty 其中的一个, 该参数的值为 empty 与该参数不存在是等价的。当开始下载时, 该参数的值设置为 started, 当下载完成时, 该参数的值设置为 completed, 当下载终止时, 该参数的值设置为 stopped |
compact | 可选,compact=1时返回紧凑格式的peers列表(默认),在 BEP_0023 中定义 |
在向 BT Tracker 发送包含以上参数的 GET 请求后, 正常情况下, BT Tracker 会返回一个 bencoding 编码的字典, 如果字典中含有 failure reason 的 Key, 则说明此次请求失败, 同时该 Key 对应的值是一个可读性的字符串, 用于向终端用户展示此次请求失败的原因, 在请求失败的情况下, BT Tracker 返回的字典里有且仅有这个一个 Key, 请求成功时, BT Tracker 返回的字典中应含有以下 Key
Key | Value | |||||||||
---|---|---|---|---|---|---|---|---|---|---|
warnging message | 当发生非致命性错误时, Tracker 返回的可读的警告信息 | |||||||||
interval | 必须,终端在下一次请求 BT Tracker 前应等待的时间 (以秒为单位) | |||||||||
min interval | 终端在下一次请求 BT Tracker 前的最短等待时间 (以秒为单位) | |||||||||
complete | 当前已完成整个资源下载的 peer 的数量 | |||||||||
incomplete | 当前未完成整个资源下载的 peer 的数量 | |||||||||
peers | 必须,一个peers的字典列表, 列表中每个字典包含有如下 Key
为了缩减tracker返回的peers列表大小,BEP_0023 将peers表示为一个字符串,每个peer表示为6字节的字符串,其中4字节ip和2字节port,去掉了peer id字段。默认情况下,tracker返回该紧凑格式的peer列表,只有当请求中的compact=0时,返回原始的peer列表格式 |
效率更优的UDP Tracker协议
UDP tracker协议(BEP_0015 UDP Tracker Protocol for BitTorrent)是一种高性能、低消耗的tracker协议,这种协议的URL一般是这样:udp://tracker:port。 数据开销的优势:基于HTTP的tracker协议,通常一个请求和响应的数据包含10个包,1206个字节。如采用UDP协议,则可以将数据传输量减少到4个包,618个字节,数据传输量约减少了50%。对于一个客户端而言,这些数据量的减少影响可能不大,但对于一个同时服务数百万客户端的tracker服务器来说,节省的带宽开销非常可观。另外一个优势是,基于UDP的协议,采用了二进制传输方式,省去了tracker解析数据的工作,也使得tracker的代码变得更为简单。 相比于TCP协议,UDP属于无连接协议,无需维护连接状态。一方面减少了服务器维护连接的开销,提升了系统性能,另一方面也带来了不可靠的问题:
-
UDP包的伪造问题:由于无需进行TCP的握手连接过程,服务端收到数据包可以对来源IP进行伪造。为此,需要在UDP协议的基础上,增加建立连接的机制,服务端通过给客户端分配connection_id的机制,来避免伪造数据包来源的问题(伪造方无法接收到服务端的connection_id信息)
-
超时重传问题:由于UDP协议是无连接的不可靠协议,需要由应用方来实现数据的可靠传输。如果客户端经过$15*2^n$秒后,还没有接收到来自服务端的响应数据,则重新发送数据请求,n取值从0到8。
-
UDP Tracker协议有如下4种类型的请求
action | 请求类型 | 定义 |
---|---|---|
0 | connect | 客户端与服务器建立连接,从服务器端获取connection_id |
1 | announce | 向tracker请求peer列表 |
2 | scrape | 获取某个种子当前的统计信息 |
3 | error | 错误消息 |
- Connect请求:在发送announce或scrape请求前,首先需要发送connect请求建立连接,从服务端获取connection_id。请求及响应的数据包格式如下:
- Announce请求:与HTTP tracker协议的请求参数类似,从服务器获取peer信息。数据格式不再是bencode,而是直接二进制编码,省去解析的步骤。
对于IPv6协议,需要将对应的IP地址字段的长度替换为16字节。具体使用哪种地址格式,可以从UDP请求包使用的地址类型来判断。 - Scrape请求:从tracker服务器获取种子的统计信息,一次最多可以获取74个种子的信息。
- 错误消息:请求失败,服务端返回错误消息,格式如下:
UDP tracker协议中不指定每个UDP报的大小,这样可以方便后续在每个请求包的后面增加额外的字段,来进行协议扩展。在 BEP_0041 中定义了协议的扩展字段option,用于描述带有路径和请求参数的tracker url,例如:udp://tracker.example.com:80/dir?a=b&c=d
和udp://tracker.example.com:80
对于tracker来说,是2个完全不同的url。UDP协议中扩展出option数据,来描述路径和参数信息。option数据附加在announce请求的数据包后,包含有3个字段。一个请求中可以包含有多个option。
option type | 定义 |
---|---|
0 | EndOfOptions,固定长度1个字节,没有后续的2个字段。表示option的解析结束的位置。 |
1 | NOP,固定长度1个字节,没有后续的字段。用作占位符 |
2 | URLData,包含请求的PATH和QUERY参数,后续的1个字节表示字符串的长度,例如:URL(1)中的参数字符串可以表示如下 0x2, 0xC, '/', 'd', 'i', 'r', '?', 'a', '=', 'b', '&', 'c', '=', 'd' 如果参数长度大于255,可以使用多个URLData的option数据进行拼接。 |
磁力链的资源定位机制-DHT
磁力链协议属于无tracker协议,定义在BEP_0005 DHT Protocol。无tracker并不是没有tracker,而是没有中心式tracker。磁力链将tracker分散存储到DHT(Distributed Hash Table,分布式哈希表),解决了集中式的tracker一旦出现宕机或封禁,将使整个网络失效的问题。每个节点负责一个小范围的路由,并负责存储一小部分数据,从而实现整个DHT网络的寻址和存储,相当于所有人一起构成了一个庞大的分布式存储数据库。 一个BT的客户端拥有2个角色:
- 作为 BT 下载的节点,实现bittorrent协议来进行上传和下载资源,我们称为peer。
- 作为DHT网络中的一员,实现DHT协议,保存一部分其他 Peer 的地址信息,作为tracker,我们称为node。
当需要进行下载的时候,先根据自己本地存的路由表找其他节点,其他节点再去找他们保存的其他节点,直到找到拥有文件的人。一传十十传百、千、万,最后通过N个人的中转,找到应该连上的人。
- Kademila网络
DHT是基于Kademila网络(简称Kad),在UDP上实现的协议。在Kademlia网络中,所有信息均以哈希表条目形式加以存储,这些条目被分散地存储在各个节点上,从而以全网方式构成一张巨大的分布式哈希表。Kademlia中规定所有的节点都具有一个160bit的node ID,为了方便资源的查找,该节点ID与种子文件中的info_hash相同。节点必须维护一个路由表,路由表中含有一部分其它节点的联系信息。其它节点距离自己越近时,路由表信息越详细。因此每个节点都知道 DHT 中离自己很近的节点的联系信息,而离自己非常远的 ID 的联系信息却知道的很少。
数据的冗余存储:由于在实际的Kad网络当中,并不能保证在任一时刻目标节点N均一定存在或者在线,因此Kad网络规定:任一条目,依据其key的具体取值,该条目将被复制并存放在节点ID距离key值最近(即当前距离目标节点N最近)的k个节点当中;之所以要将重复保存k份,这完全是考虑到整个Kad系统稳定性而引入的冗余。k的取值准则为:“在当前规模的Kad网络中任意选择至少k个节点,令它们在任意时刻同时不在线的几率几乎为0”;k的典型取值为20,即,为保证在任何时刻我们均能找到至少一份某条目的拷贝,我们必须事先在Kad网络中将该条目复制至少20份。 由上述可知,对于某一条目,在Kad网络中ID越靠近key的节点区域,该条目保存的份数就越多,存储得也越集中;事实上,为了实现较短的查询响应延迟,在条目查询的过程中,任一条目可被cache到任意节点之上;同时为了防止过度cache、保证信息足够新鲜,必须考虑条目在节点上存储的时效性:越接近目标结点N,该条目保存的时间将越长,反之,其超时时间就越短;保存在目标节点之上的条目最多能够被保留24小时,如果在此期间该条目被其发布源重新发布的话,其保存时间还可以进一步延长。 距离的度量:在 Kademlia 网络中,距离是通过异或(XOR)计算的,结果为无符号整数。distance(A, B) = |A xor B|,值越小表示越近。
Peer信息的查找流程如下:
- 数据通信协议-KRPC 协议
KRPC 协议是由 bencode 编码组成的一个简单的 RPC 结构,他使用 UDP 报文发送。一个独立的请求包被发出去然后一个独立的包被回复。这个协议没有重发。它包含 3 种消息类型:请求(query),回复(response)和错误(error)。一条 KRPC 消息由一个独立的字典组成,其中有 2 个关键字是所有的消息都包含的,其余的附加关键字取决于消息类型。
key | value |
---|---|
t | transaction ID,编码为2字节的二进制字符串。transaction ID 由请求节点产生,并且回复中要包含回显该字段。 |
y | 消息的类型,1个字节。y 对应的值有三种情况:q 表示请求,r 表示回复,e 表示错误 |
Queries请求消息(y=q)它包含 2 个附加的关键字 q 和 a
key | value |
---|---|
q | 字符串类型,包含了请求的方法名字 |
a | 字典类型,包含了请求所附加的参数 |
其中q有4种方法:ping,find_node,get_peers 和 announce_peer。
请求类型q | 定义 | 示例 |
---|---|---|
ping | Ping 请求包含一个参数 id,它是一个 20 字节的字符串包含了发送者网络字节序的节点 ID。对应的 ping 回复也包含一个参数 id,包含了回复者的节点 ID。 | ping Query = {t:aa, y:q, q:ping, a:{id:abcdefghij0123456789}} bencoded = d1:ad2:id20:abcdefghij0123456789e1:q4:ping1:t2:aa1:y1:qe Response = {t:aa, y:r, r: {id:mnopqrstuvwxyz123456}} bencoded = d1:rd2:id20:mnopqrstuvwxyz123456e1:t2:aa1:y1:re |
find_node | 被用来查找给定 ID 的节点的联系信息。包含 2 个参数,第一个参数是 id,包含了请求节点的ID。第二个参数是 target,包含了请求者正在查找的节点的 ID。当一个节点接收到了 find_node 的请求,他应该给出对应的回复,回复中包含 2 个关键字 id 和 nodes,nodes 是字符串类型,包含了被请求节点的路由表中最接近目标节点的 K(8) 个最接近的节点的联系信息。 | find_node Query = {t:aa, y:q, q:find_node, a: {id:abcdefghij0123456789, target:mnopqrstuvwxyz123456}} bencoded = d1:ad2:id20:abcdefghij01234567896:target20:mnopqrstuvwxyz123456e1:q9:find_node1:t2:aa1:y1:qe Response = {t:aa, y:r, r: {id:0123456789abcdefghij, nodes: def456...}} bencoded = d1:rd2:id20:0123456789abcdefghij5:nodes9:def456...e1:t2:aa1:y1:re |
get_peers | 请求包含 2 个参数。 第一个参数是 id,包含了请求节点的 ID。 第二个参数是 info_hash,它代表 torrent 文件的 infohash。如果被请求的节点有对应 info_hash 的 peers,他将返回一个关键字 values,这是一个列表类型的字符串。每一个字符串包含了 CompactIP-address/portinfo 格式的 peers 信息。如果被请求的节点没有这个 infohash 的 peers,那么他将返回关键字 nodes,这个关键字包含了被请求节点的路由表中离 info_hash 最近的 K 个节点,使用 Compactnodeinfo 格式回复。在这两种情况下,关键字 token 都将被返回。token 关键字在今后的 annouce_peer 请求中必须要携带。token 是一个短的二进制字符串。 | get_peers Query = {t:aa, y:q, q:get_peers, a: {id:abcdefghij0123456789, info_hash:mnopqrstuvwxyz123456}} bencoded = d1:ad2:id20:abcdefghij01234567899:info_hash20:mnopqrstuvwxyz123456e1:q9:get_peers1:t2:aa1:y1:qe Response with peers = {t:aa, y:r, r: {id:abcdefghij0123456789, token:aoeusnth, values: [axje.u, idhtnm\]}} bencoded = d1:rd2:id20:abcdefghij01234567895:token8:aoeusnth6:valuesl6:axje.u6:idhtnmee1:t2:aa1:y1:re Response with closest nodes = {t:aa, y:r, r: {id:abcdefghij0123456789, token:aoeusnth, nodes: def456...}} bencoded = d1:rd2:id20:abcdefghij01234567895:nodes9:def456...5:token8:aoeusnthe1:t2:aa1:y1:re |
announce_peer | 这个请求用来表明发出 announce_peer 请求的节点,正在某个端口下载 torrent 文件。announce_peer 包含 4 个参数。 第一个参数是 id,包含了请求节点的 ID; 第二个参数是 info_hash,包含了 torrent 文件的 infohash; 第三个参数是 port 包含了整型的端口号,表明 peer 在哪个端口下载; 第四个参数数是 token,这是在之前的 get_peers 请求中收到的回复中包含的。收到 announce_peer 请求的节点必须检查这个 token 与之前我们回复给这个节点 get_peers 的 token 是否相同。如果相同,那么被请求的节点将记录发送 announce_peer 节点的 IP 和请求中包含的 port 端口号在 peer 联系信息中对应的 infohash 下。 | announce_peers Query = {t:aa, y:q, q:announce_peer, a: {id:abcdefghij0123456789, implied_port: 1, info_hash:mnopqrstuvwxyz123456, port: 6881, token: aoeusnth}} bencoded = d1:ad2:id20:abcdefghij012345678912:implied_porti1e9:info_hash20:mnopqrstuvwxyz1234564: porti6881e5:token8:aoeusnthe1:q13:announce_peer1:t2:aa1:y1:qe Response = {t:aa, y:r, r: {id:mnopqrstuvwxyz123456}} bencoded = d1:rd2:id20:mnopqrstuvwxyz123456e1:t2:aa1:y1:re |
Responses回复消息(y=r) Errors错误消息(y=e),包含一个附加的关键字 e。关键字 e 是列表类型。第一个元素是数字类型,表明了错误码。第二个元素是字符串类型,表明了错误信息。 错误码有4种:
错误码 | 定义 |
---|---|
201 | 一般错误 |
202 | 服务错误 |
203 | 协议错误,比如不规范的包,无效的参数,或者错误的 token |
204 | 未知方法 |
示例: generic error = {t:aa, y:e, e:[201, A Generic Error Ocurred\]} bencoded = d1:eli201e23:A Generic Error Ocurrede1:t2:aa1:y1:ee
- Node的冷启动问题
当一个新节点首次试图加入DHT 网络时,它必须做三件事:
- 如果一个新节点要加入到DHT网络中,它必须要先认识一个人带你进去。这样的人我们把他叫做bootstrap node,常见的bootstrap node有:router.bittorrent.com,router.utorrent.com,router.bitcomet.com,dht.transmissionbt.com
等等,我们可以称之为节点A ,并将其加入自己的路由表 - 向该节点发起一次针对自己ID的节点查询请求,从而通过节点A获取一系列与自己距离邻近的其他节点的信息
- 刷新所有的路由表,保证自己所获得的节点信息全部都是新鲜的。
局域网服务发现机制-LSD(Local Service Discovery)
在局域网内BT客户端可以通过类似于SSDP(简单服务发现协议),采用多播的方式发现局域网内拥有资源的peer。这种方法简单高效,易于实现,在 BEP_0014 中定义。LSD使用的多播地址为
- IPv4:239.192.152.143:6771 (org-local)
- IPv6:[ff15::efc0:988f]:6771 (site-local)
IPv4的多播地址,属于D类地址,范围224.0.0.0到239.255.255.255,或表示为224.0.0.0/4。IPv6的多播地址为ff00::/8 LSD协议在多播地址上,基于UDP协议发送如下的HTTP报文
BT-SEARCH * HTTP/1.1\r\n
Host: <host>\r\n
Port: <port>\r\n
Infohash: <ihash>\r\n
cookie: <cookie (optional)>\r\n
\r\n
\r\n
- 报文中header的含义:
header | 定义 |
---|---|
Host | 发送的多播地址,即239.192.152.143 或 ff15::efc0:988f |
Port | BT客户端监听的端口 |
Infohash | 40字节的info哈希值,允许一个客户端发多个Infohash,但需要注意不能超过1400字节的MTU限制 |
cookie | 可选。用于在多播消息中,过滤掉自己发出的消息。 |
在加入网络后,BT客户端应当每5分钟发送一次LSD多播消息,为了避免多播风暴,每个客户端每分钟不能发送超过一个LSD报文。当一个客户端有超过5个种子时,可以选择轮流多播各个种子信息,或者一次多播多个种子信息。接收到LSD消息的客户端,从UDP报文中获取来源IP,并进一步确认该IP是否为可连接的peer。
数据下载
Peer Protocol (BEP_0003 )是当前正在下载同一资源的对等结点 (peer) 之间进行数据传输使用的协议, BT 协议的 Peer Protocol 基于 TCP 或 uTP(BEP_0029), 在使用 BT 协议下载时, 所有正在下载同一资源的结点都是对等的, 结点之间相互建立的连接也是对等的, 对等结点建立的连接的数据传输方向是双向的, 数据可以由任何一端发往另一端, 对等结点每接收到一个完整的 piece 之后, 接收方便计算该 piece 的 SHA1 哈希并与 .torrent 文件中的 info 对应的字典的 piece 对应的 Value 的相应分段做对比, 若哈希值相等, 则说明该分片传输成功, 此时这两个对等结点都拥有了资源文件关于该 piece 的数据。
一次完整的数据获取流程
Peer握手协议
握手消息的格式为 <pstrlen><pstr><reserved><info_hash><peer_id>
, 对等结点一方在传输层连接建立以后便发送握手信息, 另一方收到握手信息后也回复一个握手信息, 任何一方当收到非法的握手信息 (如 pstrlen 不等于 19, pstr 缺失或其值不是 BitTorrent protocol, info_hash 不相等, peer_id 与预期值不同等) 应立即断开连接。需要连接几个peer,就需要完成几次握手。
- 扩展协议(BEP_0010):如果握手消息中的reserved字段,右数第20 bit(即
reserved_byte[5] & 0x10 == 1
)设置为1,表示该peer支持扩展协议消息。扩展协议中,同样有握手消息,和数据传输消息,在下节中详细说明 - 扩展协议(BEP_0005):Peer如果支持 DHT 协议,可以将 BitTorrent 协议握手消息的reserved字段的第 8 字节的最后一位置为 1。后续的消息中,将发送DHT的监听端口给peer
Peer数据传输协议
若校验没有问题, 则握手成功, 之后对等双方开始进行数据传输, 数据的通用格式为 <length prefix><type id><payload>
, 其中 length prefix 顾名思义是消息的长度 (即 len(type id) + len(payload)), type id (十进制整数) 指示消息的类型, 特别地, length prefix 为 0 (此时消息没有 type id 也没有 payload) 代表 Keep Alive 消息, BT 协议标准规定 Keep Alive 消息每 2 min 发送一次, 接收端忽略该消息即可。
对于其它类型的消息, 其对应的 type id 如下
消息类型 | 定义 | |||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | choke 消息, choke 消息没有 payload, 其 length prefix 等于 1 | |||||||||||||||||
1 | unchoke 消息, unchoke 消息没有 payload, 其 length prefix 等于 1 | |||||||||||||||||
2 | interested 消息, interested 消息没有 payload, 其 length prefix 等于 1 | |||||||||||||||||
3 | not interested 消息, not interested 消息没有 payload, 其 length prefix 等于 1 | |||||||||||||||||
4 | have 消息, have 消息的 paylod 只包含一个整数, 该整数对应的是该终端刚刚接收完成并校验通过 SHA1 哈希的 piece 索引 (终端在接收到并校验完一个 piece 后, 就向它所知道的所有 peer 都发送 have 消息以宣示它拥有了这个 piece, 其它终端接收到 have 消息之后便可以知道对方已经有该 piece 的文件数据了, 因而可以向其发送 request 消息来获取该 piece) | |||||||||||||||||
5 | bitfield 消息, bitfield 消息只作为对等结点进行通信时所发送的第一个消息 (即在握手完成之后, 其它类型消息发送之前), 若文件没有分片, 则不发送该消息, 它的 Payload 是一个字节序列, 逻辑上是一个 bitmap 结构, 指示当前该终端已下载的文件分片, 其中第一个字节的 8 位分别表示文件的前 8 个分片, 第二个字节的 8 位分别表示文件的第 9 至 16 个分片, 以此类推, 已下载的分片对应的位的值为 1, 否则为 0, 由于文件分片数不一定是 8 的整数倍, 所以最后一个分片可能有冗余的比特位, 对于这些冗余的比特位都设置为 0 | |||||||||||||||||
6 | request 消息, request 消息用于一方向另一方请求文件数据, 它的 Payload 含有 3 个字段, 分别是 index, begin 和 length. 其中 index 指示文件分片的索引 (索引从 0 开始), begin 指示 index 对应的 piece 内的字节索引 (索引从 0 开始), length 指定请求的长度, length 一般都取 2 的整数次幂, 现在所有的 BitTorrent 实现中, length 的值都取 214214, 即 16 KB, BitTorrent 协议规定结点通过随机的顺序请求下载文件 piece | |||||||||||||||||
7 | piece 消息, piece 消息是对 request 消息的响应, 即返回对应的文件片段, 它的 Payload 含有 3 个字段, 分别是 index, begin 和 piece, 其中 index 和 begin 字段与 request 消息中的 index 与 begin 含义相同, 而 piece 是所请求的文件片段 | |||||||||||||||||
8 | cancel 消息, cancel 消息与 request 消息的 Payload 字段完全相同, 但作用相反, 用于取消对应的下载请求 | |||||||||||||||||
9 | port消息,payload为2字节的端口号。当使用DHT作为tracker时,port指定DHT结点监听的端口,peer收到该消息后,将该结点加入自己的路由表中。(BEP_0005) 该消息是在 BEP_0005 中定义的BitTorrent扩展协议,可以支持DHT协议发现peer,实现无tracker下载,或同时支持tracker和DHT。Peer如果支持 DHT 协议,可以将 BitTorrent 协议握手消息的保留位的第 8 字节的最后一位置为 1。相应地,一个无 tracker 的 torrent 文件字典不包含 announce 关键字,而使用 nodes 关键字来替代。这个关键字对应的内容应该设置为 torrent 创建者的路由表中 K 个最接近的节点。可供选择的,这个关键字也可以设置为一个已知的可用节点,比如这个 torrent 文件的创建者。例如: nodes = [[ |
|||||||||||||||||
20 | 扩展协议消息(BEP_0010),扩展协议消息的消息类型取值固定为20,相比普通的消息格式,在payload前增加了一个字节,来表示扩展消息的id。消息的格式如下: extended message ID 为 0 的消息是扩展协议的握手消息,消息中的payload是一个 bencode 字典。字典中的所有键都是可选的,peer 需要忽略所有自己不支持的键。可选的键(还可以有更多)如下:
|
这个消息需要在普通 handshake 成功后立即发送,在连接的生命周期内这个 Extend Handshake 多次发送都是有效的,但实际实现中有可能被忽略。 |
- 扩展协议(BEP_0009):Peer间传输metadata协议
在磁力链协议中,磁力链本身不包含任何文件相关的信息,如文件如何进行分片,每个分片的校验哈希等,需要通过DHT找到拥有资源的peer,再通过peer获取文件的metadata数据。Peer间传输文件的metadata数据协议,属于扩展协议的一种。在扩展的握手消息中,m字典内容指定 ut_metadata 这个键,同时保持其对应的消息 id 在 m 字典中唯一。metadata数据按照16KB大小进行分片(最后一片的大小可以小于16KB),分片索引以0开始。一个支持ut_metadata消息的peer将发送类似如下的消息:{\'m\': {\'ut_metadata\': 3}, \'metadata_size\': 31235},除了m字典外,还需额外发送metadata_size字段,用于表示metadata数据的大小(字节数)。
ut_metadata消息的payload是一个bencode的字典类型
key | value |
---|---|
msg_type | 消息类型,取值为 1. request:数据请求消息,piece参数表示请求第几个分片。对应返回消息类型为data或reject,一个没有完整metadata数据的peer因为无法校验其infohash,必须返回reject消息。例如:{\'msg_type\': 0, \'piece\': 0} 2. data:返回metadata数据的消息,metadata的分片数据附在消息字典的后面,大小计入消息的总长度中。该消息增加一个total_size字段,与metadata_size的含义相同。每次返回的数据分片大小为固定的16KB,最后一个分片可以小于16KB。例如:{\'msg_type\': 1, \'piece\': 0, \'total_size\': 3425} 3. reject:表示被请求的 peer 没有序号为 piece 的 metadata 片段。也有可能是一定时间内请求超过数量限制,为了防止洪泛攻击,直接表示拒绝。例如:{\'msg_type\': 2, \'piece\': 0} |
piece | 数据分片的索引,从0开始 |
total_size | 与握手消息中的metadata_size的含义相同。在data类型的消息中使用 |
- 扩展协议(BEP_0011 ):Peer Exchange (PEX)
Peer Exchange(PEX) 协议用于在 peer 间交换 peer 列表。通过在 Extend Handshake 消息的 字典 m 中加入 ut_pex 这个键来表明支持。PEX 消息的payload是一个 bencode 字典,格式如下:
key | value |
---|---|
added | 当前连接的 IPv4 peer 压缩格式列表,告知对方进行添加 |
added.f | 当前连接的 IPv4 peer 标志位,每个 peer 一个字节 |
added6 | 当前连接的 IPv6 peer 压缩格式列表,告知对方进行添加 |
added6.f | 当前连接的 IPv6 peer 压缩格式列表标志位 |
dropped | 过去断开连接的 IPv4 peer 压缩格式列表,告知对方进行删除 |
dropped6 | 过去断开连接的 IPv6 peer 压缩格式列表,告知对方进行删除 |
1字节的标志位定义如下:
标志位 | 定义 |
---|---|
0x01 | 加密连接 |
0x02 | 属于 seed 或者 partial seed,仅上传数据 |
0x04 | 支持uTP协议 |
0x08 | 支持ut_holepunch扩展协议 |
0x10 | 可达连接 |
PEX消息每分钟发送不能超过一条,除了最初的 PEX 消息之外,每条消息中添加的 peer 数量或者 删除的 peer 数量均不能超过 50 条。通过 PEX 获得的 peer 应该视为不可信的。攻击者可能通多伪造 PEX 消息来攻击这个 swarm。攻击者也可能通过 PEX 消息诱导 BT 客户端对特定 IP 进行尝试连接而引发 DDoS 攻击。为了缓解这些问题,peer 应该避免从单个 PEX 源获取其所有连接作为候选连接。应忽略具有不同的端口的重复 IP,还可以根据 peer 的优先级来进行排序。
Peer连接的状态管理:在进行 BT 下载时, 建立了 P2P 连接的对等结点两端各维护一个状态机, 状态机的状态除了要知道自己的状态外,还需要知道对方peer对自己的处置状态,可以用四个比特位来表征:
- am_chocking:该值若为 1 代表当前终端将另一端阻塞 (通过向对端发送 choke 消息), 将另一端阻塞期间将不再接受对方的请求, 对方无法从当前终端下载数据, 反之, 则没有阻塞对方, 则允许对方下载数据
- am_interested:该值若为 1 代表当前终端对另一端 感兴趣 (通过向对方发送 interested 消息), 所谓 感兴趣 即是对方拥有自己所没有的文件 piece, 因此当前终端希望从对方下载对应的数据
- peer_chocking:该值为 1 代表对方将当前终端阻塞 (收到了对端发来的 choke 消息), 此时当前终端无法继续向对方请求数据
- peer_interested: 该值为 1 代表对方对自己感兴趣 (收到了对端发来的 interested 消息), 即当前终端拥有另一端所没有的文件片段
当前终端从另一端下载数据当且仅当当前终端对另一端感兴趣且另一端没有将当前终端阻塞, 反过来, 另一端从当前终端下载数据当且仅当另一端对当前终端感兴趣且当前终端没有将另一端阻塞, 初始化状态下, BT 客户端对所有其它的 peer 结点都是 choked 状态。choked状态的存在用于控制每个连接peer的下载速度,维护peer网络中的公平性,对于那些贡献少索取多的peer进行相应的惩罚。
数据传输的加密/混淆
由于BT的流量对运营商的带宽消耗很大,在某些国家(或地区)互联网服务供应商限制BitTorrent流量或用户,他们认为BitTorrent流量占用过多网络资源(增加运营成本)、干扰网络正常运行,或认为或限制“非法的”文件共享。为此,可以采用混淆和加密的方法,使BT流量更难以被检测和控制。消息流加密(Message Stream Encryption,简称MSE),被很多BT客户端支持,用来规避运营商的流量检测。
MSE使用密钥交换结合torrent的infohash创建一个RC4加密密钥。密钥交换有助于最小化被动监听器的风险,而infohash有助于避免中间人攻击。选择RC4是为了速度更快。该规范允许用户选择仅加密报头或者完全加密整个连接。加密整个连接提供更强的混淆能力,但也消耗更多的CPU时间。为确保与不支持此规范的其他客户端的兼容性,用户还可选择是否仍允许未加密的传入或传出连接。支持的客户端通过节点交换(PEX)和分布式散列表(DHT)通告它们已启用MSE。
MSE的数据加密传输过程主要有如下5个步骤
DH秘钥交换协议:MSE协议中使用的RC4加密算法为对称加密算法,即加密和解密使用的是同一个秘钥,双方使用Diffie-Hellman密钥交换协议/算法(Diffie-Hellman Key Exchange/Agreement Algorithm),简称DH算法来交换秘钥。DH算法交换秘钥的过程如下:
选择一个768bit的大素数P,选择底数G,例如:
G = 2
P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A63A36210000000000090563
交换数据双方,分别生成一个只有自己知道的随机整数Xa和Xb,使用这两个随机数生成各自的公钥
Pubkey of A: Ya = (G^Xa) mod P
Pubkey of B: Yb = (G^Xb) mod P
利用发布的公钥信息,交换数据双方,可以各自生成相同的加密秘钥。
Sa = (Yb^Xa) mod P = (G^(Xb*Xa)) mod P
Sb = (Ya^Xb) mod P = (G^(Xa*Xb)) mod P
S = Sa = Sb
P, S, Ya and Yb are 768bits long
DH密钥交换算法的安全性依赖于这样一个事实:虽然计算以一个素数为模的指数相对容易,但计算离散对数却很困难.对于大的素数,计算出离散对数几乎是不可能的。第三方在已知公钥的情况下,很难推算出Xa,Xb,进而也很难得到加密秘钥S
DH算法也存在着一些明显的缺陷。例如容易遭受中间人的攻击,第三方C在和A通信时扮演B,和B通信时扮演A,A和B都与C协商了一个密钥,然后C就可以监听和传递或篡改通信数据,而A、B却无法感知。可以考虑在交换秘钥的环节中,增加hash签名的机制来对对方的身份进行验证,例如使用秘钥S和info_hash计算SHA1的hash值,来校验对方是否伪造身份,由于中间人C与A、B协商的秘钥是不同的,因此A、B接收到的签名信息无法通过校验。
完整的MSE握手过程:
A->B: Diffie Hellman Ya, PadA
B->A: Diffie Hellman Yb, PadB
A->B: HASH(\'req1\', S), HASH(\'req2\', SKEY) xor HASH(\'req3\', S), ENCRYPT(VC, crypto_provide, len(PadC), PadC, len(IA)), ENCRYPT(IA)
B->A: ENCRYPT(VC, crypto_select, len(padD), padD), ENCRYPT2(Payload Stream)
A->B: ENCRYPT2(Payload Stream)
上述流程中的常量定义
常量名 | 定义 |
---|---|
PadA, PadB | 长度为0-512字节的随机数据,用于填充。由于PadA,PadB对于接收方来说,长度未知,因此依赖于后续的数据来定位下个消息。B使用HASH(\'req1\', S)来定位,A使用ENCRYPT(VC)来定位下个消息的开始。 |
PadC, PadD | 用于未来扩展握手协议,当前未长度0-512字节的填充数据, |
SKEY | 数据流标识符,在BT协议中使用info_hash |
VC | verification constant,用于校验数据发送方是否拥有私钥S及SKEY的hash值。在当前版本中为8字节字符串,取值为0x00 |
crypto_provide | 32-bit的标志位,每一位表示数据发送方可以支持的加密算法。当前版本中 0x01 表示未加密文本, 0x02 表示 RC4 加密算法,其余字段保留扩展。 |
crypto_select | 32-bit的标志位,接收方从crypto_provide中选择一位置1,表示自己选择的通信加密算法。 |
IA | initial payload data from A。在BT协议中,通常用来传输peer握手消息数据。 |
函数定义
函数名 | 定义 |
---|---|
len(X) | x的长度,用2字节的整数表示,最大值65535字节 |
ENCRYPT() | RC4加密函数 |
ENCRYPT2() | 双方协商后选定的数据加密算法 |
HASH() | SHA1哈希函数,输出20字节的哈希值 |
NAT 穿越问题
由于IPv4地址不足,大多数的家庭网络都使用网络地址转换(NAT)技术建立了一个网关,使用内网IP来上网。采用NAT上网的技术很好地解决了IPv4地址不足的问题,但对于P2P应用来说,却导致外网的用户无法连接到内网的客户端,导致数据传输链路不畅。
NAT穿越技术允许网络应用程序使用UPnP(Universal Plug and Play)协议与NAT路由器进行交互,通过配置端口映射的方式,来实现NAT外部端口的数据包转发到应用程序使用的内部端口上。所有这一切都是自动完成的,用户无需手动映射端口或者进行其它工作。
数据传输控制算法
BT的数据传输控制算法,由保证网络数据可用性的分片算法,及客户端下载效率的阻塞算法2部分组成。
分片选择算法(Piece selection algorithm)
BT的数据传输是以分片Piece为单位,在Peers间有多个分片数据可供选择下载时,Peers又是动态变化的,可能存在随时下线。如何选择下载的分片,来确保网络中数据的可用性,是由分片选择机制来决定的。常见的分片选择机制有如下几种:
- Rarest first:最少优先策略。为了确保网络数据的可用性,最直观的策略就是让最稀缺的数据得到尽快复制,避免丢失,导致整个网络下载失败。最少优先策略即是选择最稀缺的数据分片加以复制。
- Strict Policy:严格优先级策略。Peers间在传输分片数据时,又会将分片再进行划分为更小的子分片进行下载(如将256KB的分片,切分为16KB的单元下载)。当一个分片的子分片开始启动下载后,后续的下载严格执行该分片数据优先的原则,这样可以保证一个分片尽快地被完整下载。
- Random first piece:随机选择第一个分片。“最少优先”的一个例外是在下载刚开始的时候。此时,下载者没有任何片断可供上传,所以,需要尽快的获取一个完整的分片。而最少的分片,通常只有某一个peer拥有,所以,它可能比多个peers都拥有的那些片断下载的要慢。因此,第一个分片是随机选择的,尽快完成第一分片的下载,并开始上传,才切换到“最少优先”的策略。
- Endgame mode:终局模式。在下载过程中,可能会出现从一个速率很慢的peer那里请求一个分片,导致下载很慢。在下载即将完成时,这种情况可能会进一步放大,最后的数据分片出现下载的瓶颈。为了防止这种情况,在最后阶段,peer向它的所有的peers们都发送某分片的子分片请求,一旦某些子分片到了,那么就会向其它peer发送 cancel 消息,取消对这些子分片的请求,以避免带宽的浪费。因为只在下载临近结束时采用这种方法,实际上并没有浪费多少带宽,而文件的结束部分也会下载的非常快,尽快地在网络中提供一份完整的数据。
阻塞控制算法(Choking)
由于P2P是对等协议,客户端之间没有相互控制的权力,所以在选择从谁下载,或者为谁提供数据时,存在相互博弈的现象,一般会采用针锋相对(tit-for-tat)的策略来进行数据传输。对于那些不贡献数据的peer予以惩罚,对于贡献多的peer,也提供更多的上传资源。
- Choking:BitTorrent 设计的初衷是希望结点之间公平地进行文件分发, 保证所有结点的对等性, 为了避免某些结点只下载而不上传, BitTorrent 协议采用特定的 choke 算法来维护平衡, 当阻塞(choke)一个peer时,可以从对方那里下载数据,但不再为对方上传数据。BT 终端每 10s 统计一次将当前下载速度最快并且对其感兴趣的 4 个 peer 结点, 向它们发送 unchoke 消息, 解除对其的阻塞, choke 算法将直接决定 BT 下载的公平性与效率。
- Optimistic Unchoking:开放检测。如果只是简单的为提供最好的下载速率的peers们提供上载,那么就没有办法来发现那些空闲的连接是否比当前正使用的连接更好。为了解决这个问题,在任何时候,每个peer都拥有一个称为“optimistic unchoking”的连接,这个连接总是保持连接状态,而不管它的下载速率是怎样。每隔30秒,重新计算一次哪个连接应该是“optimistic unchoking”。30秒足以让上传能力达到最大,下载能力也相应的达到最大。
- Anti-snubbing:反对歧视。某些情况下,一个peer可能被它所有的peers都阻塞了,这种情况下,它将会保持较低的下载速率直到通过 “optimistic unchoking”找到更好peers。为了减轻这种问题,如果一段时间过后,从某个peer那里一个片断也没有得到,那么这个peer认为自己被对方 “怠慢”了,于是不再为对方提供上传,除非对方是“optimistic unchoking”。这种情况下,会允许多于一个的“optimistic unchoking”,以保证能够发现可以连接的peer。
- Upload Only:仅仅上传。一旦某个peer完成了下载,它不能再通过下载速率(因为下载速率已经为0了)来决定为哪些 peers 提供上载了。目前采用的解决办法是,优先选择那些从它这里得到更好的上载速率的peers。这样的理由是可以尽可能的利用上载带宽。
参考资料
- BEP协议官网
- wiki介绍 BitTorrentSpecification
- ucla课程资料 cs217/05BitTorrent.pdf
- BitTorrent 协议及其工作原理解析
- 知乎文章《BitTorrent 简介》
- 百度百科 DHT
- Incentives Build Robustness in BitTorrent
- BT 增强建议之 Peer
- Message Stream Encryption(MSE)
- 百度百科 BitTorrent协议加密
- 百度百科 Diffie-Hellman 秘钥交换协议
- 百度百科 RC4 加密算法
- Java开源客户端 https://github.com/atomashpolskiy/bt
- 开源客户端 transmission
- 开源客户端 qBittorrent
- 开源客户端(包含开源的webui) aria2
这是我读过的最为详细、最为深入的BT协议分析!非常难得在2022年,还有博主这样的大牛,对它有如此研究。