Bootstrap

go语言代码实现区块链-P2P节点发现(一)

1、P2P网络节点发现流程

设定每次节点启动需要发现的节点数最小为N,最大值为M,节点ID为160位的二进制值。

根据Kademlia算法,假设启动的客户端(节点)为A,节点A的ID为“NodeIDA”,A启动后,发现其他节点的流程如下:

检测A的K桶下是否已经存在持久化的其他节点信息,如果存在,尝试能否ping-pong握手成功,如果可以,保留该节点。如果不可以,从K桶中删除该节点。如果此时节点A的K桶中没有节点,从种子节点列表中获取一个种子几点,将其节点ID写入K桶中。

如果K桶中节点数量小于N,启动节点发现。

节点A生成随机节点ID为“TargetNodeID”,该ID是一个160位的二进制值。

节点A的K桶中存在节点B,ID为“NodeIDB”。

记节点A与随机节点的距离DistanceTA= TargetNodeID ^ NodeIDA,^为异或运算符。

记节点B与随机节点的距离DistanceTB= TargetNodeID ^ NodeIDB,^为异或运算符。

如果DistanceTB<DistanceTA,说明节点B与随机几点的距离小于节点A与随机节点的距离。

节点B执行FindNode,比较节点B与B的K桶中的节点与随机节点的距离,查找比节点B距离随机节点更近的节点,将找到的节点发送给节点A,节点A按照与随机节点的距离依次写入K桶。

依次执行4-8的过程。

搜索时间超过600ms,停止搜索。

完成搜索后,使用距离随机节点最近的16个节点作为连接节点。

使用这种方式发现的节点会呈现一种随机性,使各个节点之间连接呈松散性分布。

2、代码实现

1、创建K桶对象,以太坊K桶按照与target节点距离进行排序,共20个Bucket桶,每个Bucket桶包含N个节点。K桶的结构如下图:

这里,K桶中Buckets数量为20。之所以这样设计是因为go语言包中sha1哈希算法生成的是长度为160位的二进制值,使用byte表示就是[20]byte,每个字节是8位二进制。

这样我们在计算过程中,不必要每个进制进行比较,只需要按照每个byte进行比较即可。

代码如下:

//Kad桶,存储所有bucket桶
type Kad struct{
	mutex_n   *sync.Mutex 
	mutex_a *sync.Mutex 
	
	nodes []*Node             //连接的节点
	currendNode *Node         //当前节点
	
	Buckets [HashBits/8]*bucket //K桶
	
	asked   []NodeID//存储Ping通的Nodeid
	
	ticker *time.Ticker //定时器,用来定时刷新连接的节点
	
	closed chan struct{}
	tc chan Node
	sc chan Node
}

//bucket桶,存储不同距离的节点

type bucket struct {

         entries      []*Node // 每个bucket中的节点

         location     int     //bucket桶在K桶中的位置

}

//节点对象
type Node struct{
	Addr string       //节点Url地址,包括ip和端口号
	Nodeid NodeID     //节点编号
	TargetID NodeID   //随机节点编号
	AddedAt time.Time //节点发现时间
	index uint8         //节点在K桶中的位置
	distance uint8    //记录每个节点距离目标节点的距离
}

 

这样的话,基本的K桶结构已经完成设计,下面我们逐步实现节点查找功能。

//创建K桶对象

func NewKad()*Kad{

	kad:=Kad{
		mutex_n:new(sync.Mutex),
		mutex_a:new(sync.Mutex),
		nodes:findNodesFromDB(),//持久化数据库中读取已经连接好的节点
		currendNode:NewNode(Addr), //根据当前IP信息生成本地节点
		ticker:time.NewTicker(refreshInterval),//节点刷新,这里设置30分钟执行一次
		closed:make(chan struct{}),
		tc:make(chan Node),
		sc:make(chan Node),
	}
	log.Println("create new kad...")	    

    go kad.checkNodes()

    go kad.loop()     

    return &kad        

}

如果该节点已经连接其他节点,为了防止重复读取,可以将已经连接的节点存入持久化数据中,方便下次使用。

节点刷新是为了防止连接过程有其他节点断开,所以需要定时刷新,如果连接的节点不足,需要重新发现其他新的节点。

K桶创建时会执行两个goruntime,checkNodes()用来检测当前可以正常连接的节点数量,如果数量不足,执行节点发现功能。

Loop函数执行刷新及其他goruntime功能。

;