它与其他关于kademlia路由表的文档有些不同,其中bucket是根据节点id的位前缀排列的,但还有一个令人困惑的地方。
bittorrent 规范描述了一种路由表实现,它仅近似于kademlia paper 中描述的那个。它更容易实现,但也有一些缺点。
因此,例如,如果我们拿这个桶,从左边取一个,从右边取一个,并搜索 XOR 最接近的节点 ID,我们会找到我们正在寻找的东西,没有必要遍历所有节点在路由表中。
在这两种情况下——完整的树状路由表实现和简化的 BEP5 变体——每个桶都可以被认为有一个 CIDR-like prefix(参见 this SO answer),由桶覆盖的前缀位和一个掩码位数。
在 BEP5 变体中,每个存储桶的前缀都简单地从数组索引和您的节点自己的 ID 派生而来。在树状表中,由于桶拆分/合并操作,桶必须跟踪它们的前缀。
使用这些前缀,您可以找到覆盖目标键的存储桶。
问题是桶不一定是满的,如果你想发送,假设一个响应中有 20 个节点一个桶是不够的。
因此,您必须以相对于目标键的升序距离 (XOR) 顺序遍历路由表(根据您自己的节点 ID 或自然距离排序)以访问多个存储桶。
由于 XOR 距离度量在每个位进位处折叠(XOR == 无进位加法),它不能很好地映射到任何路由表布局。换句话说,访问最近的存储桶是不行的。
Here's my implementation 用于从树状路由表中找到与特定目标键最近的 N 个节点。
我认为很多人只是简单地遍历整个路由表,因为对于常规节点,它最多只包含几十个桶,而 DHT 节点看不到太多流量,所以它只需要执行几次这个操作每秒,如果你在一个密集的、缓存友好的数据结构中实现它,那么最大的份额实际上可能是内存流量,而不是 CPU 指令执行一些 XOR 和比较。
即全表扫描很容易实现。
假设我们有一个路由表,每个存储桶都有以下位前缀。这些字母用作方便的名称)。
A 000...
B 00100...
C 0010100...
D 0010101000...
E 0010101001...
F 001010101...
G 00101011...
H 001011...
I 0011...
J 01...
K 1...
现在假设我们正在寻找这个目标键:
T = 0010011010111111001111100101011000001010010100001100100010011100000000100100000111001101100110110110101100010100111010111001101111110101001001001000100000001001
此外,存储桶并没有完全装满,或者我们需要的条目比单个存储桶中的更多,因此我们必须访问多个存储桶才能获得所需的数量。
现在,第一个要访问的存储桶相当明显,它是B,因为它的前缀覆盖了目标键。
由于B 的前缀长度为 5 位,因此该桶中的任何条目都将与 T 的 00000???????... 有异或距离。 5 个前缀位共享。
B是距离T最近的桶,这意味着不可能有任何路由表条目比相对距离00000...更近。相反,这意味着B 之外的任何条目的最小距离是00001...。这意味着下一个最近的存储桶必须覆盖T xor 00001... -> 00101110101111110[...]。
覆盖这个的桶是H。
H 不与B 相邻
最终 - 假设我们必须访问 6 个存储桶 - 顺序将如下所示:
00100... -> B
001011... -> H
001010101... -> F
0010101000... -> D
0010101001... -> E
00101011... -> G
这看起来相当混乱。但是,如果我们绘制每个访问过的桶的前缀到目标键的距离,它就会变得更加明显:
00000...
000010...
000011000...
0000110010...
0000110011...
00001101...
所以算法如下:
- 找到覆盖目标键的初始存储桶
- 存储桶的前缀与目标键异或(零掩码尾随位)
- 将距离增加该前缀的最低有效位
- 与目标键异或增加距离
- 找到下一个覆盖 XORed 键的存储桶
- 转到 2
TL;DR:“只看左边一桶,右边一桶”是不够的。正确的算法相当复杂,整个表的线性扫描更容易实现。