- 创建 RoutingTable 实例
//go-libp2p-kad-dht/dht.go
func New(ctx context.Context, h host.Host, options ...opts.Option) (*IpfsDHT, error) {
......
// 从这里开始看,这里创建了今天的重点内容 RoutingTable
dht := makeDHT(ctx, h, cfg.Datastore, cfg.Protocols)
......
// 下文中提到的 RoutingTable.Update 方法会在这个 handler 中调用
h.SetStreamHandler(p, dht.handleNewStream)
......
}
- 关于 RoutingTable
先看看它是如何初始化吧
// go-libpep-kbucket/table.go
// NewRoutingTable creates a new routing table with a given bucketsize, local ID, and latency tolerance.
func NewRoutingTable(bucketsize int, localID ID, latency time.Duration, m pstore.Metrics) *RoutingTable {
rt := &RoutingTable{
Buckets: []*Bucket{newBucket()},
bucketsize: bucketsize,
local: localID,
maxLatency: latency,
metrics: m,
PeerRemoved: func(peer.ID) {},
PeerAdded: func(peer.ID) {},
}
return rt
}
// go-libp2p-kad-dht/dht.go
//在 New DHT 的时候调用了 NewRoutingTable,我这句注视绝对是废话哈哈
func makeDHT(ctx context.Context, h host.Host, dstore ds.Batching, protocols []protocol.ID) *IpfsDHT {
// KValue == 20 ,
rt := kb.NewRoutingTable(KValue, kb.ConvertPeerID(h.ID()), time.Minute, h.Peerstore())
......
}
RoutingTable 这个类还是很强硬的,它都不是一个接口只能用这一种实现,在 makeDHT 创建了一个 RoutingTable 的实例,接下来看看我最关心的k桶是如何更新的呢?
RoutingTable.Update 更新 k桶
- 桶的数据结构相当于一个二维数组 Bucket[i][j], i 是桶号 j 对应桶中的内容
具体如下:
//kBuckets define all the fingers to other nodes.
Buckets []*Bucket
......
//Bucket holds a list of peers.
type Bucket struct {
lk sync.RWMutex
list *list.List
}
- 更新 k 桶中的内容,请阅读下文中的注视:
// Update adds or moves the given peer to the front of its respective bucket
// If a peer gets removed from a bucket, it is returned
// 添加或移除给定的 peer 到它对应的桶的前端
// 如果从桶中移除这个 peer ,则返回这个 peer
func (rt *RoutingTable) Update(p peer.ID) {
peerID := ConvertPeerID(p)
//计算桶号,通过 peerID 和 当前节点 ID 做异或,算出对应的桶号
//后面会单独讲解这个实现
cpl := commonPrefixLen(peerID, rt.local)
rt.tabLock.Lock()
defer rt.tabLock.Unlock()
bucketID := cpl
//如果计算出来的桶 ID 已经比现有的桶多了,则把它放到最后一个桶里
if bucketID >= len(rt.Buckets) {
bucketID = len(rt.Buckets) - 1
}
//通过桶号得到一个具体的桶
bucket := rt.Buckets[bucketID]
// 如果这个 peer 已经在桶中了,将它移动到当前桶的最前面。
if bucket.Has(p) {
// If the peer is already in the table, move it to the front.
// This signifies that it it "more active" and the less active nodes
// Will as a result tend towards the back of the list
bucket.MoveToFront(p)
return
}
// 延迟太大的会丢弃
if rt.metrics.LatencyEWMA(p) > rt.maxLatency {
// Connection doesnt meet requirements, skip!
return
}
// New peer, add to bucket
bucket.PushFront(p)
rt.PeerAdded(p)
/*
---------------------------
这个地方的逻辑是:
---------------------------
当前桶中的数据已经大于 最大限额时
如果 当前桶号已经是最后一个桶了,那么创建下一个桶,这个地方比较复杂
下一个桶会将所有桶中 peer 的与 localID 距离大于 总桶数的 peer 移动到下一个桶中。
因为未必会找到距离大于总桶数的 peer,
所以 bucket.Split 之后当前桶的总数还有可能会大于最大限额,所以要判断并删除最后一个元素
如果当前桶不是最后一个桶,则直接删除当前桶中最后一个元素
*/
// Are we past the max bucket size?
if bucket.Len() > rt.bucketsize {
// If this bucket is the rightmost bucket, and its full
// we need to split it and create a new bucket
if bucketID == len(rt.Buckets)-1 {
rt.nextBucket()
} else {
// If the bucket cant split kick out least active node
rt.PeerRemoved(bucket.PopBack())
}
}
}
- 仔细看看怎么分桶
通过下面两个方法可以看出 a、b 做了异或,
然后通过 ZeroPrefixLen 取前导 0 来求出桶号
假设 a = 1 ,b = 2 换成 比特位
a = 00000001 ,b = 00000010
a xor b = 00000011 一共 6 个前导 0
所以如果 b 是自己,那么 a 应该放在第 6 个桶里,
前面讲过 6 这个桶可能还未创建,那么则放到最新的桶中
在实际使用中 peerID 会通过 sha256 得到一个 32 位的 hash
每一位是 8 个bit,所以最多可以分 8*32 = 256 个桶
这样算下来,第0个桶可以装 2 的 256 次方个id
第256个桶就只能放 1 个 id ,k 桶变成了一个三角形
dht 中又约定了一个桶最多只能放 20个 id ,
最后几个桶是装不满 20 个的,懒得算了,
填满所有桶,可以装大约小于 256 * 20 = 5120 个 id
//go-libp2p-kbucket/util.go
func commonPrefixLen(a, b ID) int {
return ks.ZeroPrefixLen(u.XOR(a, b))
}
//go-libp2p-kbucket/keyspace/xor.go
func ZeroPrefixLen(id []byte) int {
for i, b := range id {
if b != 0 {
return i*8 + bits.LeadingZeros8(uint8(b))
}
}
return len(id) * 8
}
- 谁来调用这个 RoutingTable.Update 呢?
有两个地方会去调用 RoutingTable.Update ,它被封装在 IpfsDHT.Update 中。笔记一开头就提到了 dht.handleNewStream ,顺着这个方法可以找到调用 Update 的逻辑,还有 IpfsDHT.sendRequest 方法也会调用 Update
首先看 sendRequest ,dht 包中所有发给 peer 的请求都会调用这个方法
// sendRequest sends out a request, but also makes sure to
// measure the RTT for latency measurements.
func (dht *IpfsDHT) sendRequest(ctx context.Context, p peer.ID, pmes *pb.Message) (*pb.Message, error) {
ms, err := dht.messageSenderForPeer(p)
if err != nil {
return nil, err
}
start := time.Now()
rpmes, err := ms.SendRequest(ctx, pmes)
if err != nil {
return nil, err
}
//这里会有条件的调用 RoutingTable.Update 方法
// update the peer (on valid msgs only)
dht.updateFromMessage(ctx, p, rpmes)
dht.peerstore.RecordLatency(p, time.Since(start))
log.Event(ctx, "dhtReceivedMessage", dht.self, p, rpmes)
return rpmes, nil
}
再看看 handleNewMessage ,是通过 dht.handleNewStream 来调用的
func (dht *IpfsDHT) handleNewMessage(s inet.Stream) {
......
//这里会有条件的调用 RoutingTable.Update 方法
// update the peer (on valid msgs only)
dht.updateFromMessage(ctx, mPeer, pmes)
......
}
以上代码片段可以看出 sendRequest 成功时主动去调用 Update 而 handleNewMessage 成功时也会被动的调用一次 Update 去更新 RoutingTable