torrent笔记
前文
要理解下面的内容之前,首先要知道什么是 BitTorrent
BitTorrent 是一种点对点(P2P, Peers to Peers)的通信协议,用于在互联网上分发大量的数据和电子文件,包括但不限于数字视频文件或者歌曲文件。
而点对点(P2P, Peers to Peers)网络是指仅两台或多台 PC 连接并共享资源而无需通过单独的服务器的用户网络。
BitTorrent 协议可用于减少分发大文件对服务器和网络的影响。BitTorrent 协议不是从单个源服务器下载文件,而是允许用户加入主机“群”(一群人下载和上传相同的文件),以同时从彼此下载和上传。
使用 BitTorrent 协议,几台基本计算机(例如家用计算机)可以取代大型服务器,同时有效地将文件分发给许多收件人。这种较低的带宽使用量还有助于防止给定区域的互联网流量大幅飙升,从而为所有用户保持更高的互联网速度,无论他们是否使用 BitTorrent 协议。
Bencoding 编码
Bencoding 编码在 BitTorrent 协议中非常常见,更重要的是,BT 种子文件本身就是一个该编码的字典。
Bencoding 编码是一种非常简洁的数据格式,共支持 4 种不同的类型:
字符串
格式为
<字符串长度>:<字符串>
,例如4:qsdz
代表字符串qsdz
整型
格式为
i<值>e
,例如i10086e
代表整型10086
列表
格式为
l[data1][data2][...]e
,例如l4:qsdz4:yydsi666ee
可以拆分看作l[4:qsdz][4:yyds][i666e]e
,即表示列表[qsdz, yyds, 666]
字典
格式为
d[key1][value2][...]e
,例如d4:qsdzi666e4:yyds2:mee
可以拆分看作d[4:qsdz][i666e][4:yyds][2:me]e
,即表示字典{qsdz:666, yyds:me}
但是需要注意的是,字典的 key 必须是字符串,并且需按照字母顺序排序。
Seeds & Peers & Leechers
Seed 是一个在其客户端中打开 torrent 文件的人(假设你正在尝试下载相同的文件),你和他们之间的唯一区别是他们已经下载了完整的文件并且现在正在播种,即正在共享给 Peers 文件,但不从其他人那里下载文件的任何部分。
Leecher 是那些同时进行下载和上传的人。如果用户开始共享他已经拥有的文件,并下载其他用户已经上传或正在上传 torrent 文件的文件,他就成为了 Leecher。显而易见,Leechers 倾向于下载他们所没有的资源,共享他们已有的资源。
理论上来说,播种的人越多,对种子文件的下载速度就会越快。当 Leechers 的文件下载完成,并且他们仍然没有从上传中删除该文件时,他们就会成为 Seeds。
在上图中,中间的电脑即是 Seed,而其他的电脑是 Peers。
Peer 是在种子网络中同时下载和上传文件的人。文件是分块下载的,当一个用户下载了一些文件块后,他就会自动开始上传它。
如果更多的人参与到种子网络中,文件的下载速度就会更快。当一个节点下载完毕并希望继续上传时,他就变成了 Seed。
种子文件与磁力链接
在很早的网络时代,下载都是简单的集中式客户端/服务器模式,一个或多个服务器支撑成千上万的客户端连接下载,不仅带宽遇到了瓶颈,而且太容易出现单点故障。这时 P2P 被提出来解决这个问题,通过分布式提高网络资源的利用率。但是 P2P 只是一个理念,真正将这个理念贯彻的还是 BitTorrent 协议。
在 BitTorrent (简称 BT)中,所有的资源被切成很小的一份文件块(pieces),在这个基础上,BitTorrent 网络中的所有具有相同请求的用户可以相互传资源碎片,并且谁传得越多谁就能获得越多。
而种子文件,就是拥有完整资源的人,根据 BitTorrent 协议,生成的一个包含 Tracker 信息和文件信息的文本文件。
Tracker 信息主要是BT下载中需要用到的 Tracker 服务器的地址和针对 Tracker 服务器的设置,文件信息是根据对目标文件的计算生成的,计算结果根据 BitTorrent 协议内的 Bencode 规则进行编码。它的主要原理是需要把提供下载的文件虚拟分成大小相等的块,块大小必须为2k的整数次方(由于是虚拟分块,硬盘上并不产生各个块文件),并把每个块的索引信息和 Hash 验证码写入种子文件中;所以,种子文件就是被下载文件的索引。
下载者要下载文件内容,需要先得到相应的种子文件,然后使用 BT 客户端软件进行下载。 下载时,BT 客户端首先解析种子文件得到 Tracker 地址,然后连接 Tracker 服务器。Tracker 服务器回应下载者的请求,提供下载者其他下载者(包括发布者)的 IP。
下载者再连接其他下载者,根据种子文件,两者分别告知对方自己已经有的块,然后交换对方所没有的数据。此时不需要其他服务器参与,分散了单个线路上的数据流量,因此减轻了服务器负担。 下载者每得到一个块,需要算出下载块的 Hash 验证码与种子文件中的对比,如果一样则说明块正确,不一样则需要重新下载这个块。
与此同时,当拥有完整文件的用户越多,种子的寿命也就越长。
综上所述,可以看出 Tracker 服务器在BT网络中充当着非常重要的作用,和传统的客户端/服务器模式一样,Tracker 服务器同样会存在单点故障问题。所以在BT技术的基础上,后来又衍生出 DHT 网络和磁力链接技术,DHT 全称为分布式哈希表(Distributed Hash Table),是一种分布式存储方法。
DHT 网络是 Tracker-less 的,不依赖于其他的 Tracker 服务器。在这种情况下,每个客户端负责一个小范围的路由,并负责存储一小部分数据,从而实现整个 DHT 网络的寻址和存储。使用支持该技术的BT下载软件,用户无需连上Tracker就可以下载,因为软件会在 DHT 网络中寻找下载同一文件的其他用户并与之通讯,开始下载任务。
在网络中定位资源最简单的方法是URL(统一资源定位符),它是通过资源的位置来进行定位。而在DHT网络中,则是使用URN(统一资源名称)来进行定位,磁力链接就是基于文件内容的散列函数值来链接到特定文件,生成一个唯一的文件识别符,从而在DHT网络中定位并下载文件。
磁力链接格式如下所示:
1 | magnet:?xt=urn:btih:e2467cbf021192c241367b892230dc1e05c0580e |
其中 urn
为统一资源名称,bith
是
BitTorrent Info Hash 的缩写,是 BitTorrent 使用的 Hash
函数。
实际上除了 bith
还可以是其他类型的 Hash
函数,但主流仍然是 bith
。
通过这一串字符串,BT 工具就可以在 DHT 网络中定位下载文件。
根据以上我们可以得知,种子的稳定性高,信息多,但不便于传播扩散;而磁力链接不稳定,可能在某时刻不能获取,但是很便于扩散。
DHT 网络
磁力链接的发明使得 P2P 客户端直接从 DHT
网络中寻找资源,而不是传统的依赖于 Tracker 服务器,这样就避免了 Tracker
服务器的单点故障问题,所以从 DHT 中获取的种子有时候也叫做
Trackerless torrent
。DHT
网络是一种分布式的去中心化网络,每个加入 DHT
网络的节点都要负责存储这个网络中的资源和其他成员的联系信息。
DHT 网络很像是六人定律,它通过用户与用户之间的联系和转移,最终得到我们的目标。
如果一个新节点要加入到 DHT
网络中,它必须要先认识一个人带你进去。这样的人我们把他叫做
bootstrap node
,常见的 bootstrap node
有:router.bittorrent.com
、router.utorrent.com
、router.bitcomet.com
、dht.transmissionbt.com
等等。
所以在一个 torrent 流量中,我们经常能看到一开始会有 DNS 请求。例如下图
KRPC 协议
DHT 建立在 UDP 之上,想要获取需要的 Peers 信息,首先要了解下
Kademlia
的 KRPC
协议。 关于路由表和 Kademlia
的介绍,可以参照官方文档。我们这里的重点在于如何根据磁力链接获取拥有该磁力链接对应的种子文件信息的
Peers,所以只需要了解 KRPC
协议。 KRPC
协议是由 Bencoding 编码组成的一个简单的 RPC
结构,有4种请求:ping
、find_node
、get_peers
和 announce_peer
。
ping
→ 检测节点是否可达,请求包含一个参数id
,代表该节点的 id。对应的回复也应该包含回复者的id。find_node
→ 该请求包含两个参数id
和target
,id
为该节点的 id,target
为要查询的 id。回复中应该包含被请求节点的路由表中距离target
最接近的 8 个 id。get_peers
→ 该请求包含两个参数id
和infohash
,id
为该节点的 id,info_hash
为种子文件的SHA1哈希值,也就是磁力链接的btih
值。如果被请求的节点有对应info_hash
的 peers,他将返回一个关键字values
,这是一个列表类型的字符串。每一个字符串包含了CompactIP-address/portinfo
格式的peers信息。如果被请求的节点没有这个infohash
的 peers,那么他将返回关键字nodes
,这个关键字包含了被请求节点的路由表中离info_hash
最近的 K 个 nodes,使用Compactnodeinfo
格式回复。announce_peer
→ 如果节点正在下载 torrent 文件,则需要通知其他人你正在哪个端口进行下载,这样就可以分享给其他人,让其他人连接你进行下载。
在 WireShark 中,可以查看 BT-DHT
协议查看这些请求:
ping
find_node
get_peers
announce_peer
种子文件格式
BT种子文件整个是一个 Bencoding 编码字典格式,比较重要的
key
有:
announce
→ tracker 服务器的地址announce-list
→ 可选的 tracker 服务器地址creation date
→ 文件创建时间created by
→ 文件创建者info
→ BT 种子文件的文件信息
而 info
的 value
会根据种子包含的是单文件还是多文件有所区别,以下是公共部分:
piece length
→ 每一数据块的长度pieces
→ 所有数据块的 SHA1 校验值
如果种子包含的是单文件,则还有以下 key
name
→ 文件名称length
→ 文件的长度
如果包含的是多文件,则是以下 key
name
→ 文件夹名称files
→ 文件列表,每个文件列表下面是包括每一个单文件的信息,文件信息是个字典
而 info
中的内容是种子文件中的重中之重,在 DHT
网络中保留的内容就是 info
字段的内容。
而磁力链接中的 infohash
便是根据 info
字段的内容计算的,info
字段的 pieces
字段为每个数据块的校验值,作用便是验证下载的文件是否正确。
要注意的是 info
字段的下载也是分块的下载完成后使用
infohash
进行校验。
所以,我们可以知道,磁力链接下载文件分成两个步骤:
- 根据
infohash
下载种子文件的info
字段,注意到种子文件并不是必须的。 - 再根据
info
下载源文件,将每一个数据块进行校验,不一致则重新下载。
需要注意的是,一般的种子文件会包含 announce
,也就是
tracker 服务器的地址,如果没有 tracker 服务器,文件中可能会包含
nodes
,nodes
是存有种子信息的
peer 节点,这样的种子文件就是
trackerless torrent
。如果有 nodes 客户端直接从
nodes
获取种子信息,而从 DHT 网络中下载下来的种子文件既没有
annouce
也没有 nodes
,客户端只能通过
info
字段计算出 hashinfo
,再从
bootstrap node
节点开始在 DHT 网络中寻找种子信息。
uTP 协议
uTP
是一个基于 UDP 的开放的 BT 点对点文件共享协议。
在uTP
协议出现之前,BT下载会占用网络中大量的链接,直接导致其它网络应用服务质量下载和网络的拥堵,因此有很多ISP都开始限制BT的下载。uTP
减轻了网络延迟并解决了传统的基于TCP的BT协议所遇到的拥塞控制问题,提供可靠的有序的传送。
一个有效的 uTP
数据包包含下面格式的报头:
其中,我们最为关心的是
type
、connection id
、seq_nr
和ack_nr
这几个值。type
字段表示包类型,uTP的包类型有下面5种:
ST_DATA
= 0 → 最重要的数据包,uTP 就是使用该类型的包传送数据ST_FIN
= 1 → 关闭连接,这是 uTP 连接的最后一个包,类似于 TCP 中的 FINST_STATE
= 2 → 简单的应答包,表明已从对方收到了数据包,该包不包含任何数据,seq_nr
值不变ST_RESET
= 3 → 终止连接,类似于 TCP 中的RST
ST_SYN
= 4 → 初始化连接,类似于 TCP 中的SYN
,这是 uTP 连接的第一个包
关于 uTP
协议的内容参考官方文档BEP-029。
uTP
的一个很重要的特点是使用 connection id
来标识一次连接,而不是每个包算一次连接。所以在分析 ST_DATA
时,需要注意找所有 connection id
相同的数据包,然后按
seq_nr
排序,seq_nr
应该是依次递增的(注意
ST_STATE
包不会增加seq_nr
值),如果发现两个
ST_DATA
的 seq_nr
值相同则说明后面那个报文是重复报文需要忽略掉,如果发现两个
ST_DATA
的 seq_nr
值不是连续的,中间差了一个或多个,则可能是由于网络原因发生了丢包现象,数据包将不可用。
下图是一个简单的 ST_SYN
包:
Peer Wire协议
Peer Wire
协议是Peer之间的通信协议,通常由一个握手消息开始。握手消息的格式是这样的:<pstrlen><pstr><reserved><info_hash><peer_id>
下面是一个握手报文的示例:
关于 Peer Wire
协议的详细内容,请参考BitTorrent的规范。