p2p(peer to peer)负责以太坊底层节点间的通讯,主要包括底层节点发现(discover)和上层协议运行两大部分。node
节点发现功能主要涉及 Server Table udp 这几个数据结构,它们有独自的事件响应循环,节点发现功能即是它们互相协做完成的。其中,每一个以太坊客户端启动后都会在本地运行一个Server,并将网络拓扑中相邻的节点视为Node,而Table是Node的容器,udp则是负责维持底层的链接。这些结构的关系以下图golang
p2p/server.go type Server struct { PrivateKey *ecdsa.PrivateKey Protocols []protocol StaticNodes[] *discover.Node newTransport func(net.Conn) transport ntab disvocerTable ourHandshake *protoHandshake addpeer chan *conn ...... }
PrivateKey - 本节点的私钥,用于与其余节点创建时的握手协商
Protocols - 支持的全部上层协议
StaticNodes - 预设的静态Peer,节点启动时会首先去向它们发起链接,创建邻居关系
newTransport - 下层传输层实现,定义握手过程当中的数据加密解密方式,默认的传输层实现是用newRLPX()建立的rlpx,这不是本文的重点
ntab - 典型实现是Table
,全部peer以Node的形式存放在Table
ourHandshake - 与其余节点创建链接时的握手信息,包含本地节点的版本号以及支持的上层协议
addpeer - 链接握手完成后,链接过程经过这个通道通知Server
数组
Server
的监听循环,启动底层监听socket,当收到链接请求时,Accept后调用setupConn()开始链接创建过程网络
Server的主要事件处理和功能实现循环数据结构
Node惟一表示网络上的一个节点dom
p2p/discover/node.go type Node struct { IP net.IP UDP, TCP uint16 ID NodeID sha common.Hash }
IP - IP地址
UDP/TCP - 链接使用的UDP/TCP端口号
ID - 以太坊网络中惟一标识一个节点,本质上是一个椭圆曲线公钥(PublicKey),与Server
的PrivateKey对应。一个节点的IP地址不必定是固定的,但ID是惟一的。
sha - 用于节点间的距离计算socket
Table
主要用来管理与本节点与其余节点的链接的创建更新删除分布式
p2p/discover/table.go type Table struct { bucket [nBuckets]* bucket refreshReq chan chan struct{} ...... }
bucket - 全部peer按与本节点的距离远近放在不一样的桶(bucket)中,详见以后的节点维护
refreshReq - 更新Table
请求通道函数
Table
的主要事件循环,主要负责控制refresh和revalidate过程。
refresh.C - 定时(30s)启动Peer刷新过程的定时器
refreshReq - 接收其余线程投递到Table
的刷新Peer链接的通知,当收到该通知时启动更新,详见以后的更新邻居关系
revalidate.C - 定时从新检查以链接节点的有效性的定时器,详见以后的探活检测oop
udp
负责节点间通讯的底层消息控制,是Table
运行的Kademlia
协议的底层组件
type udp struct { conn conn addpending chan *pending gotreply chan reply *Table }
conn - 底层监听端口的链接
addpending -udp
用来接收pending的channel。使用场景为:当咱们向其余节点发送数据包后(packet)后可能会期待收到它的回复,pending用来记录一次这种尚未到来的回复。举个例子,当咱们发送ping包时,老是期待对方回复pong包。这时就能够将构造一个pending结构,其中包含期待接收的pong包的信息以及对应的callback函数,将这个pengding投递到udp的这个channel。udp
在收到匹配的pong后,执行预设的callback。
gotreply - udp
用来接收其余节点回复的通道,配合上面的addpending,收到回复后,遍历已有的pending链表,看是否有匹配的pending。
Table - 和Server
中的ntab是同一个Table
udp
的处理循环,负责控制消息的向上递交和收发控制
udp
的底层接受数据包循环,负责接收其余节点的packet
以太坊使用Kademlia
分布式路由存储协议来进行网络拓扑维护,了解该协议建议先阅读易懂分布式。更权威的资料能够查看wiki。总的来讲该协议:
使用UDP进行节点间消息通讯,有 4 种消息
本文说的距离,均是指两个节点NodeID的距离,计算方式可见 p2p/discover/node.go的 logdist()方法
源码中由Table
结构保存全部bucket,bucket结构以下
p2p/discover/table.go type bucket struct { entries []*Node replacemenets []*Node ips netutil.DistinctNetSet }
节点能够在entries和replacements互相转化,一个entries节点若是Validate失败,那么它会被本来将一个本来在replacements数组的节点替换。
有效性检测就是利用ping消息进行探活操做。Table.loop()启动了一个定时器(0~10s),按期随机选择一个bucket,向其entries中末尾的节点发送ping消息,若是对方回应了pong,则探活成功。
举个栗子,假设某个bucket, entries最多保存2个节点, replacements最多保存4个节点。初始状况下 entries=[A, B], replacements = [C, D, E],若是此时节点F加入网络, bond经过,因为 entries已满,只能加入到 replacements = [C, D, E, F]。 此时Revalidate定时器到期,则会对 B进行检测,若是经过,则 entries=[B, A],若是不经过,则将随机选择 replacements中的一项(假设为D)替换B的位置,最终 entries=[A, D], replacements = [C, E, F]
Table.loop()会按期(定时器超时)或不按期(收到refreshReq)地进行更新邻居关系(发现新邻居),二者都调用doRefresh()方法,该方法对在网络上查找离自身和三个随机节点最近的若干个节点。
Table
的lookup()方法用来实现节点查找目标节点,它的实现就是Kademlia
协议,经过节点间的接力,一步一步接近目标。
当一个节点启动后,它会首先向配置的静态节点发起链接,发起链接的过程称为Dial,源码中经过建立dialTask跟踪这个过程
dialTask表示一次向其余节点主动发起链接的任务
p2p/dial.go type dialTask struct { flags connFlag dest *discover.Node ...... }
在Server
启动时,会调用newDialState()根据预配置的StaticNodes初始化一批dialTask, 并在Server.run()方法中,启动这些这些任务。
Dial过程须要知道目标节点(dest)的IP地址,若是不知道的话,就要先使用 recolve()解析出目标的IP地址,怎么解析?就是先要用借助Kademlia
协议在网络中查找目标节点。
当获得目标节点的IP后,下一步即是创建链接,这是经过dialTask.dial()创建链接
链接创建的握手过程分为两个阶段,在在SetupConn()中实现
第一阶段为ECDH密钥创建:
sequenceDiagram Note left of Dialer: Calc token Note left of Dialer: Generate Random Prikey\Nonce Note left of Dialer: Sign Dialer->>Receiver: AuthMsg Note right of Receiver: Calc token Note right of Receiver: Check Signature Note right of Receiver: Generate Random Prikey\Nonce Receiver->>Dialer: AuthResp
第二阶段为协议握手,互相交换支持的上层协议
sequenceDiagram Dialer->>Receiver: protoHandshake Receiver->>Dialer: protoHandshake
若是两次握手都经过,dialTask将向Server
的addpeer通道发送peer的信息
sequenceDiagram participant Server.run() participant dialTask participant Remote Node dialTask->>Remote Node:EncHandshake Remote Node->>dialTask:EncHandshake dialTask->>Server.run(): posthandshake dialTask->>Remote Node:ProtoHandshake Remote Node->>dialTask:ProtoHandshake dialTask->>Server.run(): addpeer Note over Server.run(): go runPeer()
协议运行并不仅仅指某个特定的协议,准确地说应该是若干个独立的协议同时在两个节点间运行。在p2p节点发现提到过,节点间创建链接的时候会通过两次握手,其中的第二次握手,节点间会交换自身所支持的协议。最终两个节点间生效的协议为两个节点支持的协议的交集。
功能主要涉及 Peer protoRW 这几个数据结构,其关系如图
Peer.run()负责链接创建后启动运行上层协议,它自身运行在一个独立的go routine,具备本身的事件处理循环,除此以外,它还会额外建立2+n个go routine, 其中2包括一个用于保活的pingLoop() go routine和一个用于接收协议数据的readLoop() go routine ,而 n 为运行于其上的n个协议的go routine,即每一个协议调用本身的Run()方法运行在本身单独的go routine
Run 每种协议自身的运行入口,以新的go routine形式启动.
Kademlia
分布式路由存储协议来进行网络拓扑维护,将不一样距离的peer节点放在不一样的bucket中。