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中。