- 創建 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