【转载请标明出处】:https://blog.csdn.net/qq_25870633/article/details/81939101
本文章参考自:
http://www.yeolar.com/note/2010/03/21/kademlia/#id13 Kademlia协议原理简介
https://keeganlee.me/post/blockchain/20180313 详解区块链P2P网络
大家好,今天我们来说一说以太坊的Kad网络;在此之前我们先来聊一聊少部分P2P方面的知识,P2P 主要存在四种不同的网络模型,也代表着 P2P 技术的四个发展阶段:集中式、纯分布式、混合式和结构化模型。
优点:就是结构简单、实现容易。
缺点:由于中心节点需要存储所有节点的路由信息,当节点规模扩展时,就很容易出现性能瓶颈;而且也存在单点故障问题。
新节点加入该网络的实现方法也有很多种,最简单的就是随机选择一个已经存在的节点并建立邻居关系。像比特币的话,则是使用 DNS 的方式来查询其他节点,DNS 一般是硬编码到代码里的,这些 DNS 服务器就会提供比特币节点的 IP 地址列表,从而新节点就可以找到其他节点建立连接通道。新节点与邻居节点建立连接后,还需要进行全网广播,让整个网络知道该节点的存在。
全网广播的方式:该节点首先向邻居节点广播,邻居节点收到广播消息后,再继续向自己的邻居节点广播,以此类推,从而广播到整个网络。这种广播方法也称为泛洪机制。纯分布式结构不存在集中式结构的单点性能瓶颈问题和单点故障问题,具有较好的可扩展性,但泛洪机制引入了新的问题,主要是可控性差的问题:
【 一是】:容易形成泛洪循环,比如节点 A 发出的消息经过节点 B 到 节点 C,节点 C 再广播到节点 A,这就形成 了一个循环;
【二是】:响应消息风暴问题,如果节点 A 想请求的资源被很多节点所拥有,那么在很短时间内,会出现大量节点同时向节点 A 发送响应消息,这就可能会让节点 A 瞬间瘫痪。
P2P 网络中,可以抽象出两种空间:资源空间和节点空间。资源空间就是所有节点保存的资源集合,节点空间就是所有节点的集合。对所有资源和节点分别进行编号,如把资源名称或内容用 Hash 函数变成一个数值(这也是 DHT 常用的一种方法),这样,每个资源就有对应的一个 ID,每个节点也有一个 ID,资源 ID 和节点 ID 之间建立起一种映射关系,比如,将资源 n数值 的所有索引信息存放到 n数值节点上,那要搜索资源 n 时,只要找到节点 n 即可,从而就可以避免泛洪广播,能更快速而又准确地路由和定位数据。【当然,在实际应用中,资源 ID 和节点 ID 之间是无法做到一一对应的(因为无法确保数值为100的资源ID可以在全网中找到数值也为100的节点ID),通常的解决方案是,但因为 ID 都是数字,就存在大小关系或偏序关系等来决定数值为 100 的资源ID应该存放到那些离数值 100 最近的k个节点上,基于这些关系就能建立两者的映射关系】。这就是 DHT 的核心思想。DHT 算法在资源编号和节点编号上就是使用了分布式哈希表,使得资源空间和节点空间的编号有唯一性、均匀分布式等较好的性质,能够适合结构化分布式网络的要求。
好了,废话逼逼了太多了(咳咳,应该是抄了人家的太多了),现在我们来说一说传统kad网络和以太坊的kad网络,【因为他咩的,以太坊是用来做节点发现的,而类似于Bittorrent之类的除了可能要做节点发现外还需要做资源发现的】
异或操作也是单向性的。对于任意给定的节点 x 和距离 Δ≥0 ,总会存在一个精确的节点 y ,使得 d(x,y)=Δ。
单向性也确保了对于同一个 key 值的所有查询都会逐步收敛到同一个路径上,而不管查询的起始节点位置如何。这样,只要沿着查询路径上的节点都缓存这个
在 Kad 网络中,所有节点都被当作一颗二叉树的叶子【这个信息是以各个节点本地的k-bucket所记录的,可能每个节点的k-bucket所记录的内容不尽一样,但是大家所维护的k-bucket 组成的整网的节点状态既是这样】,并且每一个节点的位置都由其 ID 值的最短前缀唯一的确定。【这句话我们先记下,不明白也没关系,后面的内容会让我们明白】,对于任意一个节点,都可以把这颗二叉树分解为一系列连续的,不包含自己的子树。最高层的子树,由整颗树不包含自己的树的另一半组成;下一层子树由剩下部分不包含自己的一半组成;依此类推,直到分割完整颗树。比如节点0011如何进行子树的划分,如图:
其中,虚线包含的部分就是各子树,由上到下各层的前缀分别为 1,01,000,0010 。Kad 协议确保每个节点知道其各子树的至少一个节点。在这个前提下,每个节点都可以通过ID值来找到任何一个节点。这个路由的过程是通过所谓的 XOR(异或)距离得到的。
下面演示了节点0011如何通过连续查询来找到节点1110的。节点0011通过在逐步底层的子树间不断学习并查询最佳节点,获得了越来越接近的节点,最终收敛到目标节点上:
由于网上很多说法都是互抄的,其实上光看这个图还是不能明白,如何的收敛到目标节点:是这样纸的,首先,需要知道请求发起节点 0011 和 目标节点 1110 的 距离 n ,然后从 请求节点 0011 的本地 k-bucket 的 n 桶中返回 k个 节点信息,然后 0011 再给这几个节点广播,如果 这几个节点中刚好有距离是 n 的节点,那么就会用 0011 ,否则,就从这些节点的bendi k-bucket 中继续上述操作,直到找到 1110 节点,所以这个过程是个递归的过程。
Kad 的路由表是通过一些称之为 K 桶的表格构造起来的,传统的kad的k-bucket 的数目取值为 0≤i≤160 ;每个节点都保存有一些和自己距离范围在区间 [2i,2i+1) 内的一些节点信息,这些信息由一些 (IP address,UDP port,Node ID) 数据列表构成(Kad 网络是靠 UDP 协议交换信息的)。每一个这样的列表都称之为一个 K 桶,每一个list(k-桶)中最多存放k个对端节点信息;【注意】此处的k与上文所提到的条目被存放的最近节点数含义是一致的;每一个 list中的对端节点信息均按访问时间排序,最早访问的在list头部,而最近新访问的则放在list的尾部。
如图:
表格形式如:
不过通常来说当 i 值很小时,K 桶通常是空的(也就是说没有足够多的节点,比如当 i = 0 时,就最多可能只有1项);而当 i 值很大时,其对应 K 桶的项数又很可能会超过 k 个(当然,覆盖距离范围越广,存在较多节点的可能性也就越大),这里 k 是为平衡系统性能和网络负载而设置的一个常数,是个经验取值,但必须是偶数,比如 k = 20。在 BitTorrent 的实现中,取值为 k = 8。
经过证明,对于一个有 N 个节点的 Kad 网络,最多只需要经过 logN 步查询,就可以准确定位到目标节点。
当节点 x 收到一个 PRC 消息时,发送者 y 的 IP 地址就被用来更新对应的 K 桶,具体步骤如下:
K 桶的更新机制非常高效的实现了一种把最近看到的节点更新的策略,除非在线节点一直未从 K 桶中移出过。也就是说在线时间长的节点具有较高的可能性继续保留在 K 桶列表中;这是有依据得的:在线时间长一点的节点更值得我们信任,因为它在下一个小时以内保持在线的可能性将比我们最新访问的节点更大 这对应 Kad 网络的稳定性和减少网络维护成本(不需要频繁构建节点的路由表)带来很大好处。
上述这些 K 桶机制使 Kad 缓和了流量瓶颈(所有节点不会同时进行大量的更新操作),同时也能对节点的失效进行迅速响应。
Kademlia 协议包括四种远程 RPC 操作:PING、STORE、FIND_NODE、FIND_VALUE。
PING 操作的作用是探测一个节点,用以判断其是否仍然在线。
STORE 操作的作用是通知一个节点存储一个
FIND_NODE 操作使用一个 160 bit 的 ID 作为参数。本操作的接受者返回它所知道的更接近目标 ID 的 K 个节点的 (IP address, UDP port, Node ID) 信息。
这些节点的信息可以是从一个单独的 K 桶获得,也可以从多个 K 桶获得(如果最接近目标 ID 的 K 桶未满)。不管是哪种情况,接受者都将返回 K 个节点的信息给操作发起者。但如果接受者所有 K 桶的节点信息加起来也没有 K 个,则它会返回全部节点的信息给发起者。
FIND_VALUE 操作和 FIND_NODE 操作类似,不同的是它只需要返回一个节点的 (IP address, UDP port, Node ID) 信息。如果本操作的接受者收到同一个 key 的 STORE 操作,则会直接返回存储的 value 值。
【注意】为了防止伪造地址,在所有 RPC 操作中,接受者都需要响应一个随机的 160 bit 的 ID 值 【及接受者自己的ID】。另外,为了确信发送者的网络地址,PING 操作还可以附带在接受者的 RPC 回复信息中。
Kad 技术的最大特点之一就是能够提供快速的节点查找机制,并且还可以通过参数进行查找速度的调节;
假如节点 x 要查找 ID 值为 t 的节点,Kad 按照如下递归操作步骤进行路由查找:
【注意】:这里用“最接近”这个说法,是因为 ID 值为 t 的节点不一定存在网络中,也就是说 t 没有分配给任何一台电脑。所以,Kad之所以没有把节点查询过程严格地定义成为仅仅只查询单个目标节点的过程,这主要是因为Kad网络并没有对节点的上线时间作出 任何前提假设,因此在多数情况下我们并不能肯定需要查找的目标节点一定在线或存在
这里 α 也是为系统优化而设立的一个参数,就像 K 一样。在 BitTorrent 实现中,取值为 α=3 。
当 α=1 时,查询过程就类似于 Chord 的逐跳查询过程,如图:
整个路由查询过程是递归操作的,其过程可用数学公式表示为:
n0=x (即查询操作的发起者)
N1=find −noden0(t)
N2=find −noden1(t)
... ...
Nl=find −nodenl−1(t)
这个递归过程一直持续到 Nl=t ,或者 Nl 的路由表中没有任何关于 t 的信息,即查询失败。
由于每次查询都能从更接近 t 的 K 桶中获取信息,这样的机制保证了每一次递归操作都能够至少获得距离减半(或距离减少 1 bit)的效果,从而保证整个查询过程的收敛速度为 O(logN) ,这里 N 为网络全部节点的数量。【因为:对于一个有 N 个节点的 Kad 网络,最多只需要经过 logN 步查询】
当节点 x 要查询
一旦 FIND_VALUE 操作成功执行,则
存放
另外,为了保证数据发布、搜寻的一致性,规定在任何时候,当节点 w 发现新节点 u 比 w 上的某些
如果节点 u 要想加入 Kad 网络,它必须要和一个已经在 Kad 网络的节点,比如 w,取得联系。
u 首先把 w 插入自己适当的 K 桶中,然后对自己的节点 ID 执行一次 FIND_NODE 操作,然后根据接收到的信息更新自己的 K 桶内容。通过对自己邻近节点由近及远的逐步查询,u 完成了仍然是空的 K 桶信息的构建,同时也把自己的信息发布到其他节点的 K 桶中。
所以, 节点 u 必须做三件事,【其一】不管通过何种途径,获知一个已经加入Kad网络的节点信息(我们可以称之为节点 w),并将其加入自己的k-buckets;【其二】向该节点发起一次针对自己ID的节点查询请求,从而通过节点I获取一系列与自己距离邻近的其他节点的信 息;【最后】刷新所有的k-bucket,保证自己所获得的节点信息全部都是新鲜的
还记得在 节点状态 那一小节中我们讲了,在 Kad 网络中,所有节点都被当作一颗二叉树的叶子,并且每一个节点的位置都由其 ID 值的最短前缀唯一的确定。那么我们下面就讲一讲这个是怎么个设计:
在 Kad 网络中,每个节点的路由表都表示为一颗二叉树,叶子节点为 K 桶 (因为k桶里面装的也是节点),K 桶存放的是有相同 ID 前缀的节点信息,而这个前缀就是该 K 桶在二叉树中的位置【前缀值确定了节点应该放到 什么距离的k-桶中】。这样,每个 K 桶都覆盖了 ID 空间的一部分,全部 K 桶的信息加起来就覆盖了整个 160 bit 的 ID 空间,而且没有重叠。
如:0000111 和 0000001是属于同一个k-桶的两个节点,其中对于他们的前缀可能是 0000 ; 而对于 1111111 和 1011111 来说 他们所属于前缀为 1 的这个 k-桶,总之当前节点 N 来说,它本地的全部 k-桶 组成了 节点N 所感知的全网节点的拓扑,当然这肯定只是真实的全网节点拓扑的一部分而已,但是对于节点N来说却是他所认为的全网拓扑,这样纸的各个节点本地的k-桶所组成的各个节点所认为的全网拓扑组成了一张逻辑上的真实全网拓扑。
下面是对某节点本地的k-桶进行二叉树的分割原理,以节点 u 为例,其路由表的生成过程为:
上述过程不断重复,最终会达到距离近的节点的信息多,距离远的节点的信息少的结果【因为随着新节点的加入需要对原有某包含了当前节点u的k-桶进行拆分,可知,图中越来越多的节点加入k-桶中,随着k-桶的拆分,我们知道离目标节点越近的二叉树(k-
桶)越多】,保证了路由查询过程能快速收敛。具体原理为图中所示:
当 K 桶 010 满了之后,由于其覆盖范围包含了节点 0100 的 ID,故该 K 桶分裂为两个新的 K 桶:0101 和 0100,原 K 桶 010 的信息会根据其其前缀值重新分布到这两个新的 K 桶中。注意,这里并没有使用 160 bit 的 ID 值表示法,只是为了方便原理的演示,实际 Kad 网络中的 ID 值都是 160 bit 的。
节点离开 Kad 网络不需要发布任何信息,Kademlia 协议的目标之一就是能够弹性工作在任意节点随时失效的情况下。为此,Kad 要求每个节点必须周期性的发布全部自己存放的
我们知道常规的kad网络节点的本地维护者 最多 160 个k-桶的信息,每个桶中 最多有K个临近节点的信息 (K 为经验取值, K 取值的原则是 不管什么时刻都能确保这K个节点中有 一个节点在网络中是存活的,通常取值 k == 20 为最好);以太坊的节点本地有 256 个k-bucket,每个里面最多 16 个临近节点的信息;
而且传统的kad网络除了需要节点发现外还需要资源发现;而以太坊中主要是主要节点发现,所以kad协议的操作类型和传统的不一样:
节点的发现:
邻居节点发现流程说明:
系统第一次启动随机生成本机节点NodeId,记为LocalId,生成后将固定不变,本地节点记为local-eth。
系统读取公共节点信息,ping-pong握手完成后,将其写入K桶。
系统每隔7200ms刷新一次K桶。
a. 随机生成目标节点Id,记为TargetId,从1开始记录发现次数和刷新时间。
b. 计算TargetId与LocalId的距离,记为Dlt,然后从该距离的桶中取出所有kadId。
c. K桶中节点的NodeId记为KadId,计算KadId与TargetId的距离,记为Dkt
d. 找出K桶中Dlt 【这里的Dlt 是 KadId 和 LocalId的距离,因为任何和LocalId的距离都叫做Dlt】大于Dkt的节点,记为k桶节点,向k桶节点发送FindNODE命令,FindNODE命令包含TargetId
e. K桶节点收到FindNODE命令后,同样执行b-d的过程【这时候,上一个KadId成为了新的LocalId了】,将从K桶中找到的节点使用Neighbours命令发回给本机节点。
f. 本机节点收到Neighbours后,将收到的节点写入到K桶中。
g. 若搜索次数不超过8次,刷新时间不超过600ms,则返回到b步骤循环执行。
1 TargetId为随机生成的虚拟节点ID。
2 以太坊Kad网络与传统Kad网络的区别:
以太坊节点在发现邻居节点的8次循环中,所查找的节点均在距离上向随机生成的TargetId收敛。
传统Kad网络发现节点时,在距离上向目标节点本身收敛。
可能上面这种说法不是很明白,来,我们下面用另外一种说法来说一说,既然以太坊刷桶,是基于节点发现这个角度出发的,那么我们应该发现的是所有和目标节点相邻的节点集,基于上面的说法和下面这个图,我们应该会明白:
即,每一次都用新的LocalId和返回的桶里面的KadId分别和targetId的距离做对比,符合:Dlt > Dkt 的kadId,然后继续发送 FindNODE命令。【假想: 如果用 Dlt < Dkt 的呢?自己画个图就知道了,这样纸获取的KadId 永远距离 targetId越来越远了】
至此,我们先告一段落,放心,后面发现有不足的我都会及时补充~