一、文件共享网络
第一代
中央服务器。以文件服务器作为中心,典型的提供FTP的服务器。这种方式对服务器带宽压力和机器性能压力巨大。而且,这种单点集权的模式风险非常大,一旦出现故障,整个服务将停止。
第二代
泛洪式查询。网络中每一个节点都向他邻居节点询问是否拥有资源,如果没有,邻居继续向他的邻居询问。这种广播式的查询缺点明显,每次向外传播都是指数级,很容易就形成蝴蝶效应般的影响,对网络资源消耗非常大,而且容易形成网络风暴。以至于要对广播的广度和深度进行限制。
典型的例子是Gnutella,2000年的一种文件共享网络。广播广度被限制在5个邻居,深度不大于7跳。但5^7=78125这个数量也是很惊人。尽管它没有直接查询中心节点有效率,但它不再依赖一个中心化的索引节点。
第三代
BTtorrent-tracker。BT的网络架构中存在一个中心服务器Tracker,用来保存BT网络巾各节点的IP地址和端口等信息。下载节点刚加入网络时从Trakcer上获得其他节点的地址信息,同时在tracker中注册自己的信息成为一个peer。实现一个节点同时与多个节点交互。
第四代
Bttorrent-DHT。虽然第三代文件共享网络实现了P2P,但也有个缺点,如果tracker被屏蔽或者被黑,那么新节点无法注册,也无法获得peer列表,整个P2P网络就瘫痪。DHT(分布式哈希表)网络的出现,作为BT协议强而有力的拓展,解决了以上问题。
DHT全称 Distributed Hash Table,中文翻译分布式哈希表。它是一种去中心化的分布式系统,特点主要有自动去中心化,强大的容错能力,支持扩展。另外它规定了自己的架构,包括keyspace和overlay network(覆盖网络)两部分。但是他没有规定具体的算法细节,所以出现了很多不同的实现方式,比如Chord,Pastry,Kademlia等。BitTorrent中的DHT是基于Kademlia的一种变形。本文也是根据Kademlia协议描述DHT网络。
二、相关名词
peer:一个实现了BT协议并且开启了tcp监听端口的bt客户端或者服务器
node:一个实现了DHT协议并且开启了udp监听端口的bt客户端或者服务器
tracker:最终指引请求者找到目标peer的主机。在DHT网络中,存储了peer信息的node充当一个虚拟tracker的角色。
info_hash:一个由40位16进制数字组成的字符串,总共有160bit。由SHA-1计算torrent文件中的info得出的作为资源的唯一标识。
B编码(Bencode编码):一种torrent文件信息存储格式
哈希表:也称散列表。根据关键码值(Key value)而直接进行访问的数据结构。这个映射函数叫哈希函数,存放记录的数组叫哈希表。
三、Kademlia协议(简称 Kad)
1.两个关键的id:
nodeid,节点id。节点包含了网络ip地址和端口号,由唯一的节点id指向这一个节点。节点id为160位的二进制表示。
key。网络中的资源名索引和资源名以key-value键值对的形式表示,资源名经过哈希函数计算,转为160位的key。
2.异或距离:
由上述的两个id,把资源放置在nodeid与key的距离相近的节点上,就能实现资源的定位。也就是我们可以根据key去匹配距离相近的nodeid,然后找到在该节点上与key对应的资源value。
其中的距离指的是异或距离。也就是对两个id进行异或运算,可以是nodeid与nodeid,定位节点。或者nodeid与key,定位资源。
举个例子:00110 与 00011 的异或距离为 00101,也就是5。(0⊕0=0,1⊕0=1,1⊕1=0)
3.异或特性:
x ⊕ x = 0,节点与它本身的异或距离为0。
x ⊕ y > 0 , if x != y,不同节点异或距离一定大于0。
x ⊕ y = y ⊕ x,异或距离是对称的。
x ⊕ y + y ⊕ z >= x ⊕ z,类似于三角形不等式,第三边的距离小于等于另外两边的距离之和。
x ⊕ y ⊕ y ⊕ z = x ⊕ z
x + y >= x ⊕ y
4.路由表,与k-bucket:
由于节点id是一串二进制数,每一位的取值只有0或1,因此我们可以把节点id表示为二叉树的形式。以100为例,第一位是1,(从上到下)往右子树,第二位是0,往左子树,第三位是0,再往左子树。最终的叶子节点即为100。类似于文件夹的路径。
k-bucket
k-buckst树形结构
我们把同一个灰色部分的节点放置在同一个列表,这个列表称为k-bucket,k桶。k桶中的k指的是每一个列表所能容纳的节点的最大个数。
划分k桶是有规律的。k桶的个数跟节点id的位数有关,假设节点id的位数为n,则最多有n个k桶。第1个k桶中的节点与本地节点前n-1位id是相同,第2个k桶中的节点与本地节点前n-2位id是相同,以此类推,最后第n个k桶中的节点与本地节点第1位id就不一样了。
所有的k桶合起来也就是一个节点的路由表。
这里以nodeid为110的节点为例,共有3个k桶。第i个k桶中异或距离的范围为[2^i,2^(i+1))。
可以类比为国家的行政区域划分来理解。
节点距离
节点id为110的路由表
5.协议消息:
PING — 验证远程节点是否在线。
STORE — 在某个节点上存储 key-value 键值对。在节点上存储资源。
FIND_NODE — 查找节点,给定一个nodeid,被请求的节点返回与nodeid相近的k个节点。
FIND_VALUE — 查找资源,给定一个key,被请求的节点的nodeid与key相近,并且具有该key,返回与key对应的value。
四、BitTorrent基于Kad协议的实现
KRPC协议
KRPC是BitTorrent在Kademlia理论基础之上定义的一个通信消息格式协议,是由B编码组成的一个简单的RPC结构,有4种请求:ping、find_node、get_peers 和 announce_peer。一条 KRPC 消息即可能是request,也可能是response
KRPC字典基本组成元素
1. t关键字: 每条消息都包含 t 关键字,它是一个代表了 transaction ID 的字符串。transaction ID 由请求节点产生,并且回复中要包含回显该字段(挑战-响应模型),所以回复可能对应一个节点的多个请求。transaction ID 应当被编码为一个短的二进制字符串,比如 2 个字节,这样就可以对应 2^16 个请求
2. y关键字: 它由一个字节组成,表明这个消息的类型。y 对应的值有三种情况
1) q 表示请求(请求Queries): q类型的消息它包含 2 个附加的关键字 q 和 a
1.1) 关键字 q: 是字符串类型,包含了请求的方法名字(get_peers/announce_peer/ping/find_node)
1.2) 关键字 a: 一个字典类型包含了请求所附加的参数(info_hash/id..)
2) r 表示回复(回复 Responses): 包含了返回的值。发送回复消息是在正确解析了请求消息的基础上完成的,包含了一个附加的关键字 r。关键字 r 是字典类型
2.1) id: 当前节点id,peer节点id号或者下一跳DHT节点
2.2) nodes: ""
2.3) token: token
3) e 表示错误(错误 Errors): 包含一个附加的关键字 e,关键字 e 是列表类型
3.1) 第一个元素是数字类型,表明了错误码,当一个请求不能解析或出错时,错误包将被发送。下表描述了可能出现的错误码
201: 一般错误
202: 服务错误
203: 协议错误,比如不规范的包,无效的参数,或者错误的 toke
204: 未知方法
3.2) 第二个元素是字符串类型,表明了错误信息
ping报文
检测节点是否可达,请求包含一个参数id,代表该节点的nodeID。对应的回复也应该包含回复者的nodeID
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报文
find_node 被用来查找给定 ID 的DHT节点的联系信息,该请求包含两个参数id(代表该节点的nodeID)和target。回复中应该包含被请求节点的路由表中距离target最接近的K个nodeID以及对应的nodeINFO。应用在定位node的位置,初始化或更新自己的K桶列表。
find_node 请求包含 2 个参数,第一个参数是 id,包含了请求节点的ID。第二个参数是 target,包含了请求者正在查找的节点的 ID
当一个节点接收到了 find_node 的请求,他应该给出对应的回复,回复中包含 2 个关键字 id(被请求节点的id) 和 nodes,nodes 是字符串类型,包含了被请求节点的路由表中最接近目标节点的 K(8) 个最接近的节点的联系信息(被请求方每次都统一返回最靠近目标节点的节点列表K捅)
find_node Query = {"t":"aa","y":"q","q":"find_node","a": {"id":"abcdefghij0123456789","target":"mnopqrstuvwxyz123456"}}
# "id"containing the node ID of the querying node, and"target" containing the ID of the node sought by the queryer.
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报文(相当于find_value)
1.get_peers 请求包含2 个参数(id请求节点ID,info_hash代表torrent文件的infohash,infohash为种子文件的SHA1哈希值,也就是磁力链接的btih值)
2. response get_peer:
1) 如果被请求的节点有对应 info_hash 的 peers,他将返回一个关键字 values,这是一个列表类型的字符串。每一个字符串包含了"nodeid-ip-port"格式的 peers 信息。关键字 token 都将被返回。token 关键字在今后的 annouce_peer 请求中必须要携带。
2) 如果被请求的节点没有这个 info_hash 的 peers,那么他将返回关键字 nodes,这个关键字包含了被请求节点的路由表中离 info_hash 最近的 K 个节点(我这里没有该节点,去别的节点试试运气)。
announce_peer 报文(相当于store)
announce_peer: 这个请求用来表明发出 announce_peer 请求的节点,正在某个端口下载 torrent 文件
announce_peer 包含 4 个参数
1. 第一个参数是 id: 包含了请求节点的 ID
2. 第二个参数是 info_hash: 包含了 torrent 文件的 infohash
3. 第三个参数是 port: 包含了整型的端口号,表明 peer 在哪个端口下载
4. 第四个参数数是 token: 这是在之前的 get_peers 请求中收到的回复中包含的。收到 announce_peer 请求的节点必须检查这个 token 与之前我们回复给这个节点 get_peers 的 token 是否相同(也就说,所有下载者/发布者都要参与检测新加入的发布者是否伪造了该资源)
如果相同,那么被请求的节点将记录发送 announce_peer 节点的 IP 和请求中包含的 port 端口号在 peer 联系信息中对应的 infohash 下,这意味着一个一个事实: 当前这个资源有一个新的peer提供者了,下一次有其他节点希望或者这个资源的时候,会把这个新的(前一次请求下载资源的节点)也当作一个peer返回给请求者,这样,资源的提供者就越来越多,资源共享速度就越来越快。
一个peer正在下载某个资源,意味着该peer有能够访问到该资源的渠道,且该peer本地是有这份资源的全部或部分拷贝的,它需要向DHT网络广播announce消息,告诉其他节点这个资源的下载地址
五、DHT网络的其他应用
1、以太坊
2、洋葱网络
六、一些数字比较
为了让大家能更直观的了解DHT网络的节点空间容量,我翻了一些数据作为比较。
1、DHT网络有160bit,可以容纳2^160个节点,也就约等于 10^48个节点。
2、地球的总人数70亿,四舍五入100亿(10^10)
3、原子核直径10^-14米。
4、银河系最长直径1.7029e+21米。本星系群9.4607e+22米 。室女座超星系团 9.4607e+23(约10^24)米。
那么假如每个人都成为DHT网络中的节点,就只占据整个网络的 10^38分之一;这个尺度就相当于一颗原子核相对于室女座超星系团那么大。哈希碰撞基本不用考虑。
但神奇的是,在那么大的网络中,寻找其中任意一点,最多只需要160次find_node就可以完成。这与六度人脉理论异曲同工。
七、留下思考问题
为什么BT协议实现DHT网络要用二叉树呢?如果用16叉树会怎样?
参考资料