基于tutk方案的p2p源码_以太坊源码分析--p2p节点发现
p2p(peer to peer)負責以太坊節點間的通信,主要包括底層節點發現(discover)和上層協議運行兩大塊,本文主要描述其中節點發現部分的實現
數據結構
節點發現功能主要涉及 Server \ Table \ udp 這幾個數據結構,它們有獨自的事件響應循環,節點發現功能便是它們互相協作完成的。其中,每個以太坊客戶端啟動后都會在本地運行一個Server,并將網絡拓撲中相鄰的節點視為Node,而Table是Node的容器,udp則是負責維持底層的連接。下面重點描述它們中重要的字段和事件循環處理的關鍵部分。
Server
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.listenLoop()
Server的監聽循環,啟動底層監聽socket,當收到連接請求時,Accept后調用setupConn()開始連接建立過程
Server.run()
Server的主要事件處理和功能實現循環
進行主動的節點發現,詳見之后的節點發現部分
posthandshake channel 接收已經完成第一階段的連接,這些連接的身份已經被確認,但還需要驗證
addpeer channel 接收已經完成第二階段的連接,這些連接已經驗證,調用runPeer()運行本節點與Peer連接上的協議,詳見[TODO]p2p協議運行
Node
Node唯一表示網絡上的一個節點
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 - 用于節點間的距離計算
Table
Table主要用來管理與本節點與其他節點的連接的建立\更新\刪除
p2p/discover/table.go
type Table struct {
bucket [nBuckets]* bucket
refreshReq chan chan struct{}
......
}
bucket - 所有peer按與本節點的距離遠近放在不同的桶(bucket)中,詳見之后的節點維護
refreshReq - 更新Table請求通道
Table.loop()
Table的主要事件循環,主要負責控制refresh和revalidate過程。
refresh.C - 定時(30s)啟動Peer刷新過程的定時器
refreshReq - 接收其他線程投遞到Table的刷新Peer連接的通知,當收到該通知時啟動更新,詳見之后的更新鄰居關系
revalidate.C - 定時重新檢查以連接節點的有效性的定時器,詳見之后的探活檢測
udp
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.loop()
udp的處理循環,負責控制消息的向上遞交和收發控制
addpending 接收其他線程投遞來的pending需求
gotreply 接收udp.readLoop()投遞過來的pending的回復
udp.readLoop()
udp的底層接受數據包循環,負責接收其他節點的packet
接受其他節點發送的packet并解析,如果是回復包則投遞到udp.loop()
節點維護
以太坊使用Kademlia分布式路由存儲協議來進行網絡拓撲維護,了解該協議建議先閱讀易懂分布式。更權威的資料可以查看wiki。總的來說該協議:
使用UDP進行節點間消息通信,有 4 種消息
ping - 用于探測其他節點是否還存在
store - 接收者受到后,將信息中key/value對存儲在本節點
findnode - 接受者向發送者返回 k 個它知道的與目標結點距離最近的節點
findvalue - 和findnode 差不多,區別是如果接收者本地存在與目標結點對應的value,那么就回復這個值給發送者。
每個節點根據與鄰居節點距離之間的距離(NodeID的差距),分別放到不同的桶(bucket)中。
本文說的距離,均是指兩個節點NodeID的距離,計算方式可見p2p/discover/node.go的logdist()方法
源碼中由Table結構保存所有bucket,bucket結構如下
p2p/discover/table.go
type bucket struct {
entries []*Node
replacemenets []*Node
ips netutil.DistinctNetSet
}
entries 數組中保存經過bond的節點,并且其順序是越新bond通過了探活檢測(Revalidate)的節點位置越靠前。
replacemenets數組中保存候補節點,如果entries 數組數量滿了,之后的節點會被加入該數組
節點可以在entries和replacements互相轉化,一個entries節點如果Validate失敗,那么它會被原本將一個原本在replacements數組的節點替換。
探活檢測(Revalidate)
有效性檢測就是利用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
dialTask表示一次向其他節點主動發起連接的任務
p2p/dial.go
type dialTask struct {
flags connFlag
dest *discover.Node
......
}
在Server啟動時,會調用newDialState()根據預配置的StaticNodes初始化一批dialTask, 并在Server.run()方法中,啟動這些這些任務。
diatask (1).png
Dial過程需要知道目標節點(dest)的IP地址,如果不知道的話,就要先使用 recolve()解析出目標的IP地址,怎么解析?就是先要用借助Kademlia協議在網絡中查找目標節點。
resolve (1).png
當得到目標節點的IP后,下一步便是建立連接,這是通過dialTask.dial()建立連接
連接建立
連接建立的握手過程分為兩個階段,在在SetupConn()中實現
第一階段為ECDH密鑰建立:
enchand.png
第二階段為協議握手,互相交換支持的上層協議
dial-receive.png
如果兩次握手都通過,dialTask將向Server的addpeer通道發送peer的信息
serverrun-dial-remote.png
總結
p2p節點發現(discover)負責管理以太坊網絡中各個節點間的連接建立,更新和刪除,Server是p2p功能的入口,Table負責記錄peer節點信息, udp負責底層通信
以太坊使用Kademlia分布式路由存儲協議來進行網絡拓撲維護,將不同距離的peer節點放在不同的bucket中。
總結
以上是生活随笔為你收集整理的基于tutk方案的p2p源码_以太坊源码分析--p2p节点发现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 天天生鲜—创建数据库
- 下一篇: 【神经网络】LSTM理论介绍