1、P2P方法的思想和产生原因
P2P全称Peer to Peer。要想理解p2p首先要了解另一种更原始的结构:C/S结构。
C/S结构中,C指client,S指server。这是最常见的一种结构,比如上淘宝买东西,阿里有专门的服务器集中为全国的用户提供服务。所有的工作都由一个中心服务器完成。
C/S结构有他的优势,但是在某些情况下,可能就不是太有效了。比如下载电影,假设某人从自己的电脑上分享了一部电影,那么如果网络中的所有其他用户都连接到分享者的电脑进行下载,分享者是一定无法承受这么大的流量的。那如何解决呢?难道要租用一个分布式服务器集群专门用来分享视频?虽然不是不可以,但这势必就加大了文件分享的难度。由此,P2P方法出现了。
Peer to Peer 方法中的Peer就是只一个用户、一个client。这种结构下,用户可以传输数据给用户,而不是所有用户都从同一个服务器获取数据。通过用户之间的数据分享来降低服务器的压力,这就是P2P方法的思想。
2、BitTorrent协议基础
BT协议经历了几代的更迭,这里我们先讲最原始的BT协议。
要知道BT协议是一个典型的P2P应用。如果要实现用户之间的下载,最直接的一个问题是,如何知道下载同一个资源的用户有哪些呢?
这就是Tracker服务器的工作了。Tracker顾名思义,工作便是跟踪下载用户信息。一个种子,对应一个活跃用户列表,当有新用户要下载该种子时,便把活跃用户列表返回给新用户,并把该用户添加到列表,这样新用户就可以从其他用户那里下载数据了。
有了Tracker服务器就能知道其他用户的信息了。那么还缺少一环,那就是如何获得Tracker服务器信息呢?答案是从种子文件中。种子文件中包含了该种子文件的Tracker服务器地址。
种子文件中除了有Tracker服务器的地址,另外还有文件每个块的校验值。文件通过BT协议分享时,就虚拟地被分为了多个文件块,用户之间交换文件就是以块为单位进行的。接收到文件后,会比对种子文件中对应的校验值,不符合的就需要重新下载。
3、原始BT协议的缺陷和改进方法
可以看出,原始的BT协议仍然是中心化的结构,Tracker服务器作为整个系统的核心,一旦出故障,对应的种子文件都将失效。去中心化,是BT协议非常需要的一个改进。去中心化之后的BT协议,应该能够从网络中去寻找自己需要的其他用户。
对原始BT协议的改进有两个,第一个失败的改进是用广播的方式寻找活跃用户,接收广播的用户再转发广播直到发现了某个种子文件的活跃用户。这种方法极易引发“广播风暴”,所以很快被放弃了。
另一种改进方法便是使用至今的DHT方法。接下来,开始讲DHT方法。
DHT = Distributed Hash Table 中文分布式哈希表。
一句话说明DHT的作用:代替Tracker服务器,通过分布式存储的方式将原本存储在Tracker中的信息存储到各个用户中。
有了DHT以后,下载种子文件的过程变成了:首先,从DHT中查询种子对应的活跃用户。然后,根据获得的用户信息开始p2p下载。和原始BT协议不同的地方只是在于改变了获取活跃用户的方式。
因而DHT可以说是一个分布式数据库。网络中的所有用户构成的DHT网络是一个大的数据库。每个用户都存储一小部分数据。
理解DHT需要理解一下几个重要的问题:
1、如何分配数据的存储位置?就是说如何知道某个数据应该存储在哪个节点(node)上?
2、如何设计路由算法?路由表的更新?
3、数据的查询、添加。
以下详细说明DHT方法。
(要说明一下,DHT是一个概念性的方法,具体实现有许多种,这里仅说明Kademlia方法,该方法是BT协议中使用的方法。)
DHT中的数据是按键值对(key value pairs)的方式存储的。在BT协议的应用中,key是种子文件的ID,value是该种子文件的活跃用户(peers)信息。这里种子的ID是160bit的infohash,就是每个种子文件hash生成的独一无二的字符串。
DHT中的每个存储单元,其实也是用户,叫做节点(node),注意和peer区分。每个node也有一个ID,是一个随机生成的160bit 字符串。如此以来,数据ID和nodeID都是160bit字符串了。
接下来再引入一个概念做为铺垫——距离。node和node之间,node和数据之间都有距离,距离的计算方法,是做两个160bit ID的异或运算。注意,node ID是随机生成的,因而这里的距离是与实际网络连接情况无关的。
DHT中数据存储的基本想是把数据存放在离它最近的node中,也就是说,把数据存放在ID和数据ID最接近的node中。准确说是最接近的几个node中。
知道DHT中数据和node的距离计算和存储原则,我们可以知道只要查找和数据ID最接近的几个node,就能够找到目标数据了。但是有个问题,一个node中,可以存储部分其他node的ID以及对应的IP地址端口号。但是无法存储所有的节点的信息。要连接到目标node,需要询问其他节点,慢慢的接近目标节点。
1、bucket
存储node ID、ip地址、端口号列表叫做路由表。简单说,这个路由表中,离自己所在node距离越近的node越多,越远的越少。具体讲,用到了叫做bucket的结构。
bucket结构是路由表中其他节点信息的集合,一个bucket中最多只能存放8个node。node初始情况下只有一个bucket,范围是160bit ID的整个空间。如果一个bucket满了,而且这个bucket的范围包含了自己的node ID,那么就允许将这个bucket平均一分为二。如果bucket的范围不包括自己的nodeID,那么就不能够分裂。可以看出,持续的分裂下去,新的bucket范围会越来越小,而且离nodeID越来越近。由于每个bucket都只能存8个node,这么以来就确保了距离近的node信息多,距离远的node信息少。
2、查询过程
DHT中数据的查询过程是:发起查询的节点(node)首先计算自己路由表中的节点到目标数据的距离,然后对最近的k个节点发起数据查询。收到数据查询请求的节点如果有数据直接返回,没有则在自己的路由表中查询距离数据最近的节点,并返回。发起查询的节点收到了返回的节点,则继续查询这些节点,知道返回了需要的数据。
这个持续的查询过程会一步步接近存在数据的节点。
3、路由表的更新
DHT中用户在持续的变化,所以需要一些机制保证路由表信息不会过时。
路由表中的node有三种状态:good、questionable、bad。简单说,good就是活跃的节点。如果长期没有一个节点的信息,这个节点就变成了questionable,如果对某个节点多次发送请求都没有回复,那么这个节点就是bad的,会从路由表中去除。
questionable节点暂时不会被去除,只有当需要加入新节点但该bucket已经满了之后,才会对该bucket中的questionable节点进行测试,并删除没有回应的节点。
另外,每个bucket中,至少需要维持一个good节点(这样才能保证整个网络中的任意点都是可达的),如果一个bucket里的所有点都长期没有动静,就需要对整个bucket进行刷新,刷新方法是发起一个在该bucket范围内的查询。
4、新节点初始化路由表
新节点进入DHT网络时,必须至少已知一个节点。然后对该节点发起查询,查询的是自己的ID。这样以来,会返回越来越接近自己的Node信息。查询的过程中存储这些node信息。查询结束后,就完成了路由表的初始化。
1、增删改查
前面已经说到了数据的查询过程。修改数据方法类似,也是首先查询数据ID,最后可以得到几个node,修改这些node上的数据。
2、应对节点变化
节点会不断变化,存储数据的k个节点都可能全部下线从而导致数据丢失。
刷新机制可以应对这个问题,刷新机制很简单,就是每隔一段时间,所有节点将自己拥有的数据重新进行一次存储。就是重新查询数据ID,将自己有的数据存储到返回的那几个node中去。这个方法不能百分之百保证数据不丢失,只是丢失概率比较低而已。
新加入节点如何处理呢?新节点在加入过程中会初始化路由表,这个过程中其他节点会发现该节点。其他节点会判断新节点到自己存储的数据之间的距离,如果新节点够近,就将数据存储到新节点上。
[1] BitTorrent 社区文档 DHT Protocol
[2] (DHT方法论文)Kademlia: A peer-to-peer information system based on the xor metric
[3] (一篇不错的博文)聊聊分布式散列表(DHT)的原理——以 Kademlia(Kad) 和 Chord 为例
PS:本文旨在将DHT进行简要的描述,省略了部分细节,也有部分表述不精确。想要详细了解的请阅读参考资料。