前言
说句大实话,网上介绍怎么用java实现p2p种子的搜索这种资料不是特别多,大部分都是python的,用python的话就会简单很多,它里面有很多简单方便的包,libtorrent等等,当然你用这些包可以实现功能,但是它封装了太好,以致于你很难知道里面的细节。为了深入了解,然后我就用java实现了一把,当然中间遇到了很多的问题,也参考了github的项目。
说到p2p,我想大家可能都用种子下载过文件,比较常见的就是.torrent结尾的文件,通过种子可以下载到种子对应的文件。
基本概念
那么什么是种子呢?种子其实就是一个文件里面保存的是一个字典,其中最重要的一个字段就是info字段,里面保存文件名和文件长度。还有一个比较重要的字段是announce,这个是tracker地址通过这个地址可以查找到这个文件在哪些peer上面。还有一些种子没有announce字段,这样的种子被称为trackerless torrent,会有nodes字段来代替。
除了种子可以下载文件,还有一种磁力链接也可以下载文件。那么什么是磁力链接呢?just like magnet:?xt=urn:btih:19838A8C4DE7DC2E34382249C9A52CFD9E3BB41A
复制这个链接,打开迅雷会让你下载权利的游戏,磁力链接的前面部分是不变的,也就是说最后那一长串字符串才是对应了你下载的资源。最后一长串的字符串叫做infohash,每个种子都会对应一个infohash,所以磁力链接就是根据infohash来下载种子对应的文件。所以我们只要收集到足够多的infohash,然后根据infohash再找到那些文件,建立起infohash和文件的对应关系,那么我们的搜索也就完成了,这里其实做的是磁力的搜索。
好了现在我们需要解决两个问题,第一个是怎么爬取infohash,第二个是怎么通过infohash来找到种子包含的文件名。
那么我们怎么能够获取到infohash呢?这里以bittorrnt dht protocol为例
要想获取infohash就必须成为dht网络中的一员,dht网络中由很多node组成,每个node由160个bit组成。每个node呢,都有存储其中一部分node的信息,这个信息呢包括node的id,ip,port等等,在存储node id的时候也不是随便存储,会按照距离的远近来存储,这里的距离不是物理的距离 ,而是逻辑距离。通过两个id的异或来计算。举个小例子 node a 的id是1110, node b的id等于0110,那么node a和node b之间的距离就是2的3次方。node的信息都存在路由表里面,每个路由表分为160个K桶,上面的那个例子中,因为node a和node b距离是2^3,所以node b会存放在node a的第三个bucket。这里稍微解释下,这是因为node的id是160位,所有的id范围在0~2^160,一个路由160个bucket刚好覆盖id的所有的范围。那么这样是不是意味着数字越大的桶里面的节点数也就越多呢?为了防止一个bucket里面节点太多,所以规定了每个bucket最多有8个节点,那么当有别的节点来了,又超过了8个节点的时候应该怎么办呢?这个问题放到后面再来讲。
那怎么来建立路由表呢?首先dht网络中有4个方法,通过这四个方法可以来建立路由表
- ping 检查一个节点是否在线
- find_node 查找一个节点
- get_peers 查找指定的某个infohash
- announce_peer 发起一个通知,用来告诉节点下载完了
建立路由表,最重要的就是find_node,每发起一次find_node请求,对方就会返回距离被查询节点最近的前8个节点,通过不断的find_node,我们的路由表就建立了。
好了,到了这里其实还会有很多疑问?比方说路由表有什么用,建立了路由表后又怎么获取infohash呢?还是没讲明白,带着这几个问题继续下面的分析。
假设有一个infohash xxx,那么怎么知道xxx在哪个节点上面呢?这个地方很关键,因为infohash也是160bit,所以可以用同样的方法进行异或计算。因为在dht中规定离infohash距离最近的N个节点有责任知道这个infohash在哪,但是不一定保存这个infohash。这里用到的是get_peers,通过get_peers可以查找一个infohash,具体流程如下
(1) 从路由表中查到最近的8个node,依次发起get_peers请求
(2)如果没有查到的情况下,那么会返回离对方最近的8个node,继续对返回的node进行get_peers请求
(3)如果查到了,那么就返回values参数,里面包含了 拥有该infohash的ip和port
从上面可以知道,如果没有查到infohash对应的node,那么会不断从路由表最近的节点里面去查找,当然最后可能会找不到。
还有个announce_peer没说,上面提到dht中规定离infohash距离最近的N个节点有责任知道这个infohash在哪,当你从某个节点获取到infohash的时候,就要告诉那些节点你也获取到了infohash,这样那些节点就会保存你也有infohash这个信息。
好了只剩下ping这个没说了,ping主要用来检测一个节点是否存在存活。上面有说到路由表的每个bucket只能存放最多8个节点,当有新的节点来的时候,又超过了8个节点。这个时候就会分成两个情况。
- 该节点本来所在的bucket不是该bucket,那么bucket就会一分为二,因为一开始bucket只有一个,而不是一开始就有160个(后面讲实现还会再来讲)
- 该节点所在的bucket已经存在了8个节点,那么就会对节点发起ping,会替换没有响应的请求,如果所有节点都是好的,那么就丢弃该节点。
上面说的那四个ping,find_node,get_peers,announce_peer中的任意一个 当客户端收到请求的时候就会把他们加入自己的路由表。
好了讲了这么多,现在来总结一下ping和find_node都是和路由表最相关的ping用来检查bucket中的节点状态,find_node用来构建路由表。get_peers和announce_peer都是和infohash最相关get_peers可以通过主动查找的方式获取infohash,announce_peer通过被动接受的方式获取到infohash。所以get_peers和announce_peer都是我们从dht网络中获取infohash的最重要的方式。后面具体实现部分还会详细介绍。