以太坊原始碼分析(49)p2p-table.go原始碼分析
table.go主要實現了p2p的Kademlia協議。
### Kademlia協議簡介(建議閱讀references裡面的pdf文件)
Kademlia協議(以下簡稱Kad) 是美國紐約大學的PetarP. Maymounkov和David Mazieres.
在2002年釋出的一項研究結果《Kademlia: A peerto -peer information system based on
the XOR metric》。
簡單的說, Kad 是一種分散式雜湊表( DHT) 技術, 不過和其他 DHT 實現技術比較,如
Chord、 CAN、 Pastry 等, Kad 通過獨特的以異或演算法( XOR)為距離度量基礎,建立了一種
全新的 DHT 拓撲結構,相比於其他演算法,大大提高了路由查詢速度。
### table的結構和欄位
const (
alpha = 3 // Kademlia concurrency factor
bucketSize = 16 // Kademlia bucket size
hashBits = len(common.Hash{}) * 8
nBuckets = hashBits + 1 // Number of buckets
maxBondingPingPongs = 16
maxFindnodeFailures = 5
autoRefreshInterval = 1 * time.Hour
seedCount = 30
seedMaxAge = 5 * 24 * time.Hour
)
type Table struct {
mutex sync.Mutex // protects buckets, their content, and nursery
buckets [nBuckets]*bucket // index of known nodes by distance
nursery []*Node // bootstrap nodes
db *nodeDB // database of known nodes
refreshReq chan chan struct{}
closeReq chan struct{}
closed chan struct{}
bondmu sync.Mutex
bonding map[NodeID]*bondproc
bondslots chan struct{} // limits total number of active bonding processes
nodeAddedHook func(*Node) // for testing
net transport
self *Node // metadata of the local node
}
### 初始化
func newTable(t transport, ourID NodeID, ourAddr *net.UDPAddr, nodeDBPath string) (*Table, error) {
// If no node database was given, use an in-memory one
//這個在之前的database.go裡面有介紹。 開啟leveldb。如果path為空。那麼開啟一個基於記憶體的db
db, err := newNodeDB(nodeDBPath, Version, ourID)
if err != nil {
return nil, err
}
tab := &Table{
net: t,
db: db,
self: NewNode(ourID, ourAddr.IP, uint16(ourAddr.Port), uint16(ourAddr.Port)),
bonding: make(map[NodeID]*bondproc),
bondslots: make(chan struct{}, maxBondingPingPongs),
refreshReq: make(chan chan struct{}),
closeReq: make(chan struct{}),
closed: make(chan struct{}),
}
for i := 0; i < cap(tab.bondslots); i++ {
tab.bondslots <- struct{}{}
}
for i := range tab.buckets {
tab.buckets[i] = new(bucket)
}
go tab.refreshLoop()
return tab, nil
}
上面的初始化啟動了一個goroutine refreshLoop(),這個函式主要完成以下的工作。
1. 每一個小時進行一次重新整理工作(autoRefreshInterval)
2. 如果接收到refreshReq請求。那麼進行重新整理工作。
3. 如果接收到關閉訊息。那麼進行關閉。
所以函式主要的工作就是啟動重新整理工作。doRefresh
// refreshLoop schedules doRefresh runs and coordinates shutdown.
func (tab *Table) refreshLoop() {
var (
timer = time.NewTicker(autoRefreshInterval)
waiting []chan struct{} // accumulates waiting callers while doRefresh runs
done chan struct{} // where doRefresh reports completion
)
loop:
for {
select {
case <-timer.C:
if done == nil {
done = make(chan struct{})
go tab.doRefresh(done)
}
case req := <-tab.refreshReq:
waiting = append(waiting, req)
if done == nil {
done = make(chan struct{})
go tab.doRefresh(done)
}
case <-done:
for _, ch := range waiting {
close(ch)
}
waiting = nil
done = nil
case <-tab.closeReq:
break loop
}
}
if tab.net != nil {
tab.net.close()
}
if done != nil {
<-done
}
for _, ch := range waiting {
close(ch)
}
tab.db.close()
close(tab.closed)
}
doRefresh函式
// doRefresh performs a lookup for a random target to keep buckets
// full. seed nodes are inserted if the table is empty (initial
// bootstrap or discarded faulty peers).
// doRefresh 隨機查詢一個目標,以便保持buckets是滿的。如果table是空的,那麼種子節點會插入。 (比如最開始的啟動或者是刪除錯誤的節點之後)
func (tab *Table) doRefresh(done chan struct{}) {
defer close(done)
// The Kademlia paper specifies that the bucket refresh should
// perform a lookup in the least recently used bucket. We cannot
// adhere to this because the findnode target is a 512bit value
// (not hash-sized) and it is not easily possible to generate a
// sha3 preimage that falls into a chosen bucket.
// We perform a lookup with a random target instead.
//這裡暫時沒看懂
var target NodeID
rand.Read(target[:])
result := tab.lookup(target, false) //lookup是查詢距離target最近的k個節點
if len(result) > 0 { //如果結果不為0 說明表不是空的,那麼直接返回。
return
}
// The table is empty. Load nodes from the database and insert
// them. This should yield a few previously seen nodes that are
// (hopefully) still alive.
//querySeeds函式在database.go章節有介紹,從資料庫裡面隨機的查詢可用的種子節點。
//在最開始啟動的時候資料庫是空白的。也就是最開始的時候這個seeds返回的是空的。
seeds := tab.db.querySeeds(seedCount, seedMaxAge)
//呼叫bondall函式。會嘗試聯絡這些節點,並插入到表中。
//tab.nursery是在命令列中指定的種子節點。
//最開始啟動的時候。 tab.nursery的值是內建在程式碼裡面的。 這裡是有值的。
//C:\GOPATH\src\github.com\ethereum\go-ethereum\mobile\params.go
//這裡面寫死了值。 這個值是通過SetFallbackNodes方法寫入的。 這個方法後續會分析。
//這裡會進行雙向的pingpong交流。 然後把結果儲存在資料庫。
seeds = tab.bondall(append(seeds, tab.nursery...))
if len(seeds) == 0 { //沒有種子節點被發現, 可能需要等待下一次重新整理。
log.Debug("No discv4 seed nodes found")
}
for _, n := range seeds {
age := log.Lazy{Fn: func() time.Duration { return time.Since(tab.db.lastPong(n.ID)) }}
log.Trace("Found seed node in database", "id", n.ID, "addr", n.addr(), "age", age)
}
tab.mutex.Lock()
//這個方法把所有經過bond的seed加入到bucket(前提是bucket未滿)
tab.stuff(seeds)
tab.mutex.Unlock()
// Finally, do a self lookup to fill up the buckets.
tab.lookup(tab.self.ID, false) // 有了種子節點。那麼查詢自己來填充buckets。
}
bondall方法,這個方法就是多執行緒的呼叫bond方法。
// bondall bonds with all given nodes concurrently and returns
// those nodes for which bonding has probably succeeded.
func (tab *Table) bondall(nodes []*Node) (result []*Node) {
rc := make(chan *Node, len(nodes))
for i := range nodes {
go func(n *Node) {
nn, _ := tab.bond(false, n.ID, n.addr(), uint16(n.TCP))
rc <- nn
}(nodes[i])
}
for range nodes {
if n := <-rc; n != nil {
result = append(result, n)
}
}
return result
}
bond方法。記得在udp.go中。當我們收到一個ping方法的時候,也有可能會呼叫這個方法
// bond ensures the local node has a bond with the given remote node.
// It also attempts to insert the node into the table if bonding succeeds.
// The caller must not hold tab.mutex.
// bond確保本地節點與給定的遠端節點具有繫結。(遠端的ID和遠端的IP)。
// 如果繫結成功,它也會嘗試將節點插入表中。呼叫者必須持有tab.mutex鎖
// A bond is must be established before sending findnode requests.
// Both sides must have completed a ping/pong exchange for a bond to
// exist. The total number of active bonding processes is limited in
// order to restrain network use.
// 傳送findnode請求之前必須建立一個繫結。 雙方為了完成一個bond必須完成雙向的ping/pong過程。
// 為了節約網路資源。 同時存在的bonding處理流程的總數量是受限的。
// bond is meant to operate idempotently in that bonding with a remote
// node which still remembers a previously established bond will work.
// The remote node will simply not send a ping back, causing waitping
// to time out.
// bond 是冪等的操作,跟一個任然記得之前的bond的遠端節點進行bond也可以完成。 遠端節點會簡單的不會傳送ping。 等待waitping超時。
// If pinged is true, the remote node has just pinged us and one half
// of the process can be skipped.
// 如果pinged是true。 那麼遠端節點已經給我們傳送了ping訊息。這樣一半的流程可以跳過。
func (tab *Table) bond(pinged bool, id NodeID, addr *net.UDPAddr, tcpPort uint16) (*Node, error) {
if id == tab.self.ID {
return nil, errors.New("is self")
}
// Retrieve a previously known node and any recent findnode failures
node, fails := tab.db.node(id), 0
if node != nil {
fails = tab.db.findFails(id)
}
// If the node is unknown (non-bonded) or failed (remotely unknown), bond from scratch
var result error
age := time.Since(tab.db.lastPong(id))
if node == nil || fails > 0 || age > nodeDBNodeExpiration {
//如果資料庫沒有這個節點。 或者錯誤數量大於0或者節點超時。
log.Trace("Starting bonding ping/pong", "id", id, "known", node != nil, "failcount", fails, "age", age)
tab.bondmu.Lock()
w := tab.bonding[id]
if w != nil {
// Wait for an existing bonding process to complete.
tab.bondmu.Unlock()
<-w.done
} else {
// Register a new bonding process.
w = &bondproc{done: make(chan struct{})}
tab.bonding[id] = w
tab.bondmu.Unlock()
// Do the ping/pong. The result goes into w.
tab.pingpong(w, pinged, id, addr, tcpPort)
// Unregister the process after it's done.
tab.bondmu.Lock()
delete(tab.bonding, id)
tab.bondmu.Unlock()
}
// Retrieve the bonding results
result = w.err
if result == nil {
node = w.n
}
}
if node != nil {
// Add the node to the table even if the bonding ping/pong
// fails. It will be relaced quickly if it continues to be
// unresponsive.
//這個方法比較重要。 如果對應的bucket有空間,會直接插入buckets。如果buckets滿了。 會用ping操作來測試buckets中的節點試圖騰出空間。
tab.add(node)
tab.db.updateFindFails(id, 0)
}
return node, result
}
pingpong方法
func (tab *Table) pingpong(w *bondproc, pinged bool, id NodeID, addr *net.UDPAddr, tcpPort uint16) {
// Request a bonding slot to limit network usage
<-tab.bondslots
defer func() { tab.bondslots <- struct{}{} }()
// Ping the remote side and wait for a pong.
// Ping遠端節點。並等待一個pong訊息
if w.err = tab.ping(id, addr); w.err != nil {
close(w.done)
return
}
//這個在udp收到一個ping訊息的時候被設定為真。這個時候我們已經收到對方的ping訊息了。
//那麼我們就不同等待ping訊息了。 否則需要等待對方傳送過來的ping訊息(我們主動發起ping訊息)。
if !pinged {
// Give the remote node a chance to ping us before we start
// sending findnode requests. If they still remember us,
// waitping will simply time out.
tab.net.waitping(id)
}
// Bonding succeeded, update the node database.
// 完成bond過程。 把節點插入資料庫。 資料庫操作在這裡完成。 bucket的操作在tab.add裡面完成。 buckets是記憶體的操作。 資料庫是持久化的seeds節點。用來加速啟動過程的。
w.n = NewNode(id, addr.IP, uint16(addr.Port), tcpPort)
tab.db.updateNode(w.n)
close(w.done)
}
tab.add方法
// add attempts to add the given node its corresponding bucket. If the
// bucket has space available, adding the node succeeds immediately.
// Otherwise, the node is added if the least recently active node in
// the bucket does not respond to a ping packet.
// add試圖把給定的節點插入對應的bucket。 如果bucket有空間,那麼直接插入。 否則,如果bucket中最近活動的節點沒有響應ping操作,那麼我們就使用這個節點替換它。
// The caller must not hold tab.mutex.
func (tab *Table) add(new *Node) {
b := tab.buckets[logdist(tab.self.sha, new.sha)]
tab.mutex.Lock()
defer tab.mutex.Unlock()
if b.bump(new) { //如果節點存在。那麼更新它的值。然後退出。
return
}
var oldest *Node
if len(b.entries) == bucketSize {
oldest = b.entries[bucketSize-1]
if oldest.contested {
// The node is already being replaced, don't attempt
// to replace it.
// 如果別的goroutine正在對這個節點進行測試。 那麼取消替換, 直接退出。
// 因為ping的時間比較長。所以這段時間是沒有加鎖的。 用了contested這個狀態來標識這種情況。
return
}
oldest.contested = true
// Let go of the mutex so other goroutines can access
// the table while we ping the least recently active node.
tab.mutex.Unlock()
err := tab.ping(oldest.ID, oldest.addr())
tab.mutex.Lock()
oldest.contested = false
if err == nil {
// The node responded, don't replace it.
return
}
}
added := b.replace(new, oldest)
if added && tab.nodeAddedHook != nil {
tab.nodeAddedHook(new)
}
}
stuff方法比較簡單。 找到對應節點應該插入的bucket。 如果這個bucket沒有滿,那麼就插入這個bucket。否則什麼也不做。 需要說一下的是logdist()這個方法。這個方法對兩個值進行按照位置異或,然後返回最高位的下標。 比如 logdist(101,010) = 3 logdist(100, 100) = 0 logdist(100,110) = 2
// stuff adds nodes the table to the end of their corresponding bucket
// if the bucket is not full. The caller must hold tab.mutex.
func (tab *Table) stuff(nodes []*Node) {
outer:
for _, n := range nodes {
if n.ID == tab.self.ID {
continue // don't add self
}
bucket := tab.buckets[logdist(tab.self.sha, n.sha)]
for i := range bucket.entries {
if bucket.entries[i].ID == n.ID {
continue outer // already in bucket
}
}
if len(bucket.entries) < bucketSize {
bucket.entries = append(bucket.entries, n)
if tab.nodeAddedHook != nil {
tab.nodeAddedHook(n)
}
}
}
}
在看看之前的Lookup函式。 這個函式用來查詢一個指定節點的資訊。 這個函式首先從本地拿到距離這個節點最近的所有16個節點。 然後給所有的節點傳送findnode的請求。 然後對返回的界定進行bondall處理。 然後返回所有的節點。
func (tab *Table) lookup(targetID NodeID, refreshIfEmpty bool) []*Node {
var (
target = crypto.Keccak256Hash(targetID[:])
asked = make(map[NodeID]bool)
seen = make(map[NodeID]bool)
reply = make(chan []*Node, alpha)
pendingQueries = 0
result *nodesByDistance
)
// don't query further if we hit ourself.
// unlikely to happen often in practice.
asked[tab.self.ID] = true
不會詢問我們自己
for {
tab.mutex.Lock()
// generate initial result set
result = tab.closest(target, bucketSize)
//求取和target最近的16個節點
tab.mutex.Unlock()
if len(result.entries) > 0 || !refreshIfEmpty {
break
}
// The result set is empty, all nodes were dropped, refresh.
// We actually wait for the refresh to complete here. The very
// first query will hit this case and run the bootstrapping
// logic.
<-tab.refresh()
refreshIfEmpty = false
}
for {
// ask the alpha closest nodes that we haven't asked yet
// 這裡會併發的查詢,每次3個goroutine併發(通過pendingQueries引數進行控制)
// 每次迭代會查詢result中和target距離最近的三個節點。
for i := 0; i < len(result.entries) && pendingQueries < alpha; i++ {
n := result.entries[i]
if !asked[n.ID] { //如果沒有查詢過 //因為這個result.entries會被重複迴圈很多次。 所以用這個變數控制那些已經處理過了。
asked[n.ID] = true
pendingQueries++
go func() {
// Find potential neighbors to bond with
r, err := tab.net.findnode(n.ID, n.addr(), targetID)
if err != nil {
// Bump the failure counter to detect and evacuate non-bonded entries
fails := tab.db.findFails(n.ID) + 1
tab.db.updateFindFails(n.ID, fails)
log.Trace("Bumping findnode failure counter", "id", n.ID, "failcount", fails)
if fails >= maxFindnodeFailures {
log.Trace("Too many findnode failures, dropping", "id", n.ID, "failcount", fails)
tab.delete(n)
}
}
reply <- tab.bondall(r)
}()
}
}
if pendingQueries == 0 {
// we have asked all closest nodes, stop the search
break
}
// wait for the next reply
for _, n := range <-reply {
if n != nil && !seen[n.ID] { //因為不同的遠方節點可能返回相同的節點。所有用seen[]來做排重。
seen[n.ID] = true
//這個地方需要注意的是, 查詢出來的結果又會加入result這個佇列。也就是說這是一個迴圈查詢的過程, 只要result裡面不斷加入新的節點。這個迴圈就不會終止。
result.push(n, bucketSize)
}
}
pendingQueries--
}
return result.entries
}
// closest returns the n nodes in the table that are closest to the
// given id. The caller must hold tab.mutex.
func (tab *Table) closest(target common.Hash, nresults int) *nodesByDistance {
// This is a very wasteful way to find the closest nodes but
// obviously correct. I believe that tree-based buckets would make
// this easier to implement efficiently.
close := &nodesByDistance{target: target}
for _, b := range tab.buckets {
for _, n := range b.entries {
close.push(n, nresults)
}
}
return close
}
result.push方法,這個方法會根據 所有的節點對於target的距離進行排序。 按照從近到遠的方式決定新節點的插入順序。(佇列中最大會包含16個元素)。 這樣會導致佇列裡面的元素和target的距離越來越近。距離相對遠的會被踢出佇列。
// nodesByDistance is a list of nodes, ordered by
// distance to target.
type nodesByDistance struct {
entries []*Node
target common.Hash
}
// push adds the given node to the list, keeping the total size below maxElems.
func (h *nodesByDistance) push(n *Node, maxElems int) {
ix := sort.Search(len(h.entries), func(i int) bool {
return distcmp(h.target, h.entries[i].sha, n.sha) > 0
})
if len(h.entries) < maxElems {
h.entries = append(h.entries, n)
}
if ix == len(h.entries) {
// farther away than all nodes we already have.
// if there was room for it, the node is now the last element.
} else {
// slide existing entries down to make room
// this will overwrite the entry we just appended.
copy(h.entries[ix+1:], h.entries[ix:])
h.entries[ix] = n
}
}
### table.go 匯出的一些方法
Resolve方法和Lookup方法
// Resolve searches for a specific node with the given ID.
// It returns nil if the node could not be found.
//Resolve方法用來獲取一個指定ID的節點。 如果節點在本地。那麼返回本地節點。 否則執行
//Lookup在網路上查詢一次。 如果查詢到節點。那麼返回。否則返回nil
func (tab *Table) Resolve(targetID NodeID) *Node {
// If the node is present in the local table, no
// network interaction is required.
hash := crypto.Keccak256Hash(targetID[:])
tab.mutex.Lock()
cl := tab.closest(hash, 1)
tab.mutex.Unlock()
if len(cl.entries) > 0 && cl.entries[0].ID == targetID {
return cl.entries[0]
}
// Otherwise, do a network lookup.
result := tab.Lookup(targetID)
for _, n := range result {
if n.ID == targetID {
return n
}
}
return nil
}
// Lookup performs a network search for nodes close
// to the given target. It approaches the target by querying
// nodes that are closer to it on each iteration.
// The given target does not need to be an actual node
// identifier.
func (tab *Table) Lookup(targetID NodeID) []*Node {
return tab.lookup(targetID, true)
}
SetFallbackNodes方法,這個方法設定初始化的聯絡節點。 在table是空而且資料庫裡面也沒有已知的節點,這些節點可以幫助連線上網路,
// SetFallbackNodes sets the initial points of contact. These nodes
// are used to connect to the network if the table is empty and there
// are no known nodes in the database.
func (tab *Table) SetFallbackNodes(nodes []*Node) error {
for _, n := range nodes {
if err := n.validateComplete(); err != nil {
return fmt.Errorf("bad bootstrap/fallback node %q (%v)", n, err)
}
}
tab.mutex.Lock()
tab.nursery = make([]*Node, 0, len(nodes))
for _, n := range nodes {
cpy := *n
// Recompute cpy.sha because the node might not have been
// created by NewNode or ParseNode.
cpy.sha = crypto.Keccak256Hash(n.ID[:])
tab.nursery = append(tab.nursery, &cpy)
}
tab.mutex.Unlock()
tab.refresh()
return nil
}
### 總結
這樣, p2p網路的Kademlia協議就完結了。 基本上是按照論文進行實現。 udp進行網路通訊。資料庫儲存連結過的節點。 table實現了Kademlia的核心。 根據異或距離來進行節點的查詢。 節點的發現和更新等流程。
網址:http://www.qukuailianxueyuan.io/
欲領取造幣技術與全套虛擬機器資料
區塊鏈技術交流QQ群:756146052 備註:CSDN
尹成學院微信:備註:CSDN
相關文章
- 以太坊原始碼分析(36)ethdb原始碼分析原始碼
- 以太坊原始碼分析(38)event原始碼分析原始碼
- 以太坊原始碼分析(41)hashimoto原始碼分析原始碼
- 以太坊原始碼分析(43)node原始碼分析原始碼
- 以太坊原始碼分析(51)rpc原始碼分析原始碼RPC
- 以太坊原始碼分析(52)trie原始碼分析原始碼
- 以太坊原始碼分析(13)RPC分析原始碼RPC
- 以太坊原始碼分析(35)eth-fetcher原始碼分析原始碼
- 以太坊原始碼分析(20)core-bloombits原始碼分析原始碼OOM
- 以太坊原始碼分析(24)core-state原始碼分析原始碼
- 以太坊原始碼分析(29)core-vm原始碼分析原始碼
- 以太坊原始碼分析(34)eth-downloader原始碼分析原始碼
- 以太坊原始碼分析(37)eth以太坊協議分析原始碼協議
- 以太坊原始碼分析(18)以太坊交易執行分析原始碼
- 以太坊原始碼分析(5)accounts程式碼分析原始碼
- 以太坊交易池原始碼分析原始碼
- 以太坊原始碼分析(23)core-state-process原始碼分析原始碼
- 以太坊原始碼分析(31)eth-downloader-peer原始碼分析原始碼
- 以太坊原始碼分析(32)eth-downloader-peer原始碼分析原始碼
- 以太坊原始碼分析(33)eth-downloader-statesync原始碼分析原始碼
- 以太坊原始碼分析(8)區塊分析原始碼
- 以太坊原始碼分析(9)cmd包分析原始碼
- 以太坊原始碼分析(16)挖礦分析原始碼
- 以太坊原始碼分析(26)core-txpool交易池原始碼分析原始碼
- 以太坊原始碼分析(27)core-vm-jumptable-instruction原始碼分析原始碼Struct
- 以太坊原始碼分析(28)core-vm-stack-memory原始碼分析原始碼
- 以太坊原始碼分析(30)eth-bloombits和filter原始碼分析原始碼OOMFilter
- 以太坊原始碼分析(10)CMD深入分析原始碼
- 以太坊原始碼分析(12)交易資料分析原始碼
- 以太坊原始碼分析(19)core-blockchain分析原始碼Blockchain
- 以太坊原始碼分析(44)p2p-database.go原始碼分析原始碼DatabaseGo
- 以太坊原始碼分析(45)p2p-dial.go原始碼分析原始碼Go
- 以太坊原始碼分析(46)p2p-peer.go原始碼分析原始碼Go
- 以太坊原始碼分析(48)p2p-server.go原始碼分析原始碼ServerGo
- 以太坊原始碼分析(50)p2p-udp.go原始碼分析原始碼UDPGo
- 以太坊原始碼分析(52)以太坊fast sync演算法原始碼AST演算法
- 以太坊原始碼分析(39)geth啟動流程分析原始碼
- 以太坊原始碼分析(6)accounts賬戶管理分析原始碼