[以太坊源码分析][p2p网络04]:基于UDP的节点发现

[以太坊源码分析][p2p网络04]:基于UDP的节点发现_第1张图片

为什么要进行节点发现呢?

因为要加入一个p2p网络,并且与网络中的节点交互,需要知道这个p2p网络中的一些节点信息。节点发现,使本地节点得知其他节点的信息,进而加入到p2p网络中。节点发现就是一个寻找邻居节点的过程。

这里有一点跟去中心化违背的地方,就是节点第一次启动的时候,节点会与硬编码在以太坊源码中的bootnode进行连接,这个bootnode有一种中心化服务器的感觉,因为所有的节点加入几乎都先连接了它。然而,只有一个可以通信的节点,明显是不足够的。连接上bootnode后,获取bootnode部分的邻居节点,然后进行节点发现,获取更多的活跃的邻居节点。

以太坊的节点发现

Kademlia - wiki
kademlia
以太坊的节点发现基于类似的kademlia算法,源码中有两个版本,v4和v5。v4适用于全节点,通过discover.ListenUDP使用,v5适用于轻节点通过discv5.ListenUDP使用,这里主要介绍v4版本,较为简单的版本。

接下来是以太坊节点发现v4的源码分析部分,分为udp发现k桶刷新

0.索引

01.p2p服务开启节点发现
02.udp节点发现
03.k桶刷新

1.p2p服务开启节点发现

p2p.server.go中的Start方法中:

if !srv.NoDiscovery {
    cfg := discover.Config{
        PrivateKey:   srv.PrivateKey,
        AnnounceAddr: realaddr,
        NodeDBPath:   srv.NodeDatabase,
        NetRestrict:  srv.NetRestrict,
        Bootnodes:    srv.BootstrapNodes,
        Unhandled:    unhandled,
    }
    ntab, err := discover.ListenUDP(conn, cfg)
    if err != nil {
        return err
    }
    srv.ntab = ntab
}
  • 其中discover.ListenUDP方法即开启了节点发现的功能。discover.ListenUDP方法创建了一个新的udp对象(在discover/udp.go中),用于节点发现,和Table对象(在discover/table.go中),用于维护k桶。
    func ListenUDP(c conn, cfg Config) (*Table, error) {
      tab, _, err := newUDP(c, cfg)
      ...
      return tab, nil
    }
    

2.udp节点发现

新建一个udp对象

首先要从newUDP方法看起,newUDP方法如下:

func newUDP(c conn, cfg Config) (*Table, *udp, error)
[以太坊源码分析][p2p网络04]:基于UDP的节点发现_第2张图片
udp

源码中三个主要的过程:

  • 1.newTable方法新建Table对象,并且开启了k桶刷新(即更新路由表)的功能。这部分在下面的内容再做介绍。
  • 2.go udp.loop()协程,循环的监听4个通道。
    • case <-t.closing,检测是否停止。
    • case p := <-t.addpending,检测是否有添加新的待处理消息。
    • case r := <-t.gotreply,检测是否接收到其他节点的回复消息。
    • case now := <-timeout.C,检测是否延时。
  • 3.go udp.readLoop(cfg.Unhandled)协程。
    • 循环接收其他节点发来的udp消息。
      nbytes, from, err := t.conn.ReadFromUDP(buf)
    • 处理接收到的udp消息。
      t.handlePacket(from, buf[:nbytes])

udp消息有4种:

  • ping,用于判断远程节点是否在线。
  • pong,用于回复ping消息的响应。
  • findnode,查找与给定的目标节点相近的节点。
  • neighbors,用于回复findnode的响应,与给定的目标节点相近的节点列表。
节点发现的过程

(假设节点A要进行节点发现,向节点B进行查询)

  • 1.节点A向节点B发送ping消息,判断节点B是否在线。节点A加入与该ping消息对应的pending。(这里使用了t.addpending通道。)
    对应的方法:
    func (t *udp) ping(toid enode.ID, toaddr *net.UDPAddr) error
    
  • 2.节点A收到节点B发来的pong消息,确认节点B在线。将pongpending进行匹配。(这里使用了t.gotreply通道。)
    对应的方法:
    func (req *pong) handle(t *udp, from *net.UDPAddr, fromKey encPubkey, mac []byte) error
    
  • 3.节点A向节点B发送findnode消息,想要获取与目标节点相近的节点。发送findnode消息时,会先检测上次收到节点B的pong消息是否超过24小时,超过则发送ping消息,接收到pong消息后再发送findnode消息。同时也记录一个与findnode消息对应的pending。(这里使用了t.addpending通道。)
    对应的方法:
    func (t *udp) findnode(toid enode.ID, toaddr *net.UDPAddr, target encPubkey) ([]*node, error)
    
  • 4.节点A收到节点B发来的neighbors消息,获取到几个与目标节点相近的节点。将neighborspending进行匹配。(这里使用了t.gotreply通道。)
    对应的方法:
    func (req *neighbors) handle(t *udp, from *net.UDPAddr, fromKey encPubkey, mac []byte) error
    
  • 5.节点A向新获取的节点继续进行上述4个步骤,直到查找完成。

其中,节点B启动了loopreadloop两个单独的协程来处理节点A发送来的消息。

3.k桶刷新

以太坊的k桶设置:
alpha为3
nBuckets,k桶数量为17
bucketSize,k桶中最多存16个节点
maxReplacements,每个k桶的候选节点列表最多存10个节点

新建一个Table对象

newTable方法如下:

func newTable(t transport, self *enode.Node, db *enode.DB, bootnodes []*enode.Node)(*Table, error)
[以太坊源码分析][p2p网络04]:基于UDP的节点发现_第3张图片
Table

主要介绍三个方法:

  • 1.tab.setFallbackNodes(bootnodes)方法,设置初始引导节点,验证其完整性,然后加入引导节点列表。
    初始引导节点的作用,如开头所说,首次启动并且没有加入到此p2p网络的节点,要加入到网络中,必须知道网络中一些节点的信息。初始引导节点的作用便是如此:引导初始启动的节点加入到p2p网络中。
  • 2.tab.loadSeedNodes()方法,从保留已知节点的数据库中随机的抽取30个节点,再加上引导节点列表中的节点,放置入k桶中。
  • 3.go tab.loop()协程,刷新k桶。下面介绍。
k桶刷新的过程

也就是go tab.loop()协程具体做了什么,如下:

[以太坊源码分析][p2p网络04]:基于UDP的节点发现_第4张图片
loop

主要介绍三个协程:

  • 1.go tab.doRefresh(refreshDone),刷新的协程。
    [以太坊源码分析][p2p网络04]:基于UDP的节点发现_第5张图片
    doRefresh

    主要的查找逻辑在lookup里,lookup会对3个异或距离较近的节点进行查询,查询方法用到的是udp.findnode。将每一次的查询结果,根据距离范围,加入到k桶中,如果k桶未满。
  • 2.go tab.doRevalidate(revalidateDone),重新验证的协程。选取随机的k桶中的最后一个节点,使用udpping消息,如果ping通了,将该节点移动在k通中的最前面。如果ping不通,删除该节点,从replacements候选节点列表中选取节点加入k桶。
  • 3.go tab.copyLiveNodes(),节点入数据库的协程。将k桶中的节点存入数据库中,选取节点的条件是节点在k桶中存在5分钟以上。

4.总结

  • 1.以太坊的节点发现分为两个部分,基于udp的节点发现k桶的刷新维护机制
  • 2.基于udp的节点发现使用ping消息去确定远程节点是否在线,以及通过findnode消息去查找到更多的远程节点。其中使用了两个独立的协程,loopreadloop,用于对接收到远程节点的udp消息进行处理。
  • 3.k桶的刷新维护机制,也就是时时更新节点的路由表,每30分钟进行一次查找新的节点,每10秒进行一次k桶中节点的重新验证,每30秒进行一次k桶节点的入库操作。其中使用了三个独立的协程。

你可能感兴趣的:([以太坊源码分析][p2p网络04]:基于UDP的节点发现)