以太坊做爲一個去中心化的系統,其底層個體相互間的通訊顯然很是重要,全部數據的同步,各個個體狀態的更新,都依賴於整個網絡中每一個個體相互間的通訊機制。以太坊的網絡通訊基於peer-to-peer(p2p)通訊協議,又根據自身傳輸數據類型(區塊,交易,哈希值等),網絡節點業務相關性等需求,在各方面作了特別設計。node
因爲以太坊中p2p通訊相關代碼量較大,打算分爲上下兩篇文章來加以詳解:上篇主要介紹管理p2p通訊的核心類ProtocolManager內部主要流程,以及通訊相關協議族的設計;下篇主要介紹ProtocolManager的兩個成員Fetcher和Downloader,這裏是上篇。golang
在開始介紹以太坊的p2p通訊機制以前,不妨先來看看通常意義上的p2p網絡通訊的一些特徵,如下部份內容摘自peer-to-peer_wikispring
peer-to-peer(p2p)首先是一種網絡拓撲類型,與之對比最顯著的就是client/server(C/S)架構。從TCP/IP協議族分層的角度來講,p2p網絡中實際的數據交換,依然是網絡層用IP協議,傳輸層用TCP協議;而p2p協議--若是可稱之爲協議的話,應算做應用層再往上,相似於邏輯拓撲層,畢竟著名的應用層協議之一FTP,就屬於很是典型的一種C/S架構類型。緩存
上圖是C/S架構和p2p架構的一個簡單示意圖,原圖來自wiki。左圖中C/S架構被描繪成星型拓撲,這固然僅僅是特例,你們可能在工做中遇到各類各樣拓撲形狀的C/S架構,而其核心特徵是不變的:C/S 網絡中的個體地位和功能是不平等的,client個體主要消耗資源,發起請求,server個體主要提供資源並處理請求,這使得C/S架構自然是中心化的。網絡
相比之下,p2p架構中最重要的特色在於:其網絡中的個體在地位和功能上是平等的,雖然每一個個體可能處理不一樣的請求,實際提供的資源在具體量化後可能有差別,但它們都能同時既消耗資源又提供資源。若是把整個所處網絡中的資源--此處的資源包括但不限於運算能力、存儲空間、網絡帶寬等,視爲一個總量,那麼p2p網絡中的資源分佈,是分散於各個個體中的(也許不必定均勻分佈)。因此,p2p網絡架構自然是去中心化的、分佈式的。架構
注意上圖右側p2p網絡中,並不是每一個個體與網絡中其餘同類均有通訊。這其實也是p2p網絡的一個很重要的特色:一個個體只須要與相鄰的一部分同類有通訊便可,每一個個體可與多少相鄰個體、哪些個體有通訊,是能夠加以設計的,分佈式
根據p2p網絡中節點相互之間如何聯繫,能夠將p2p網絡簡單區分爲無結構化的(unstructured),和結構化的(structured)兩大類。函數
這種p2p網絡即最普通的,不對結構做特別設計的實現方案。優勢是結構簡單易於組建,網絡局部區域內個體可任意分佈,反正此時網絡結構對此也沒有限制;特別是在應對大量新個體加入網絡和舊個體離開網絡(「churn」)時它的表現很是穩定。缺點在於在該網絡中查找數據的效率過低,由於沒有預知信息,因此每每須要將查詢請求發遍整個網絡(至少大多數個體),這會佔用很大一部分網絡資源,並大大拖慢網絡中其餘業務運行。oop
這種p2p網絡中的個體分佈通過精心設計,主要目的是爲了提升查詢數據的效率,下降查詢數據帶來的資源消耗。提升查詢效率的基本手段是對數據創建索引,結構化p2p網絡最廣泛的實現方案中使用了分佈式哈希表(Distributed Hash Table,DHT),它會對每項數據(value)分配一個key以組成(key,value)鍵值對,同時網絡中每一個個體的分佈--這裏的分佈主要指相互通訊關係-根據key鍵進行關聯和擴展。這樣,當要查找某項數據時,只要跟據其key鍵就能不斷的縮小查找區域,大大減小資源消耗。性能
儘管如此,這樣的p2p網絡缺點也很明顯:因爲每一個個體須要存有數量很多的相鄰個體列表,因此當網絡中發生大量新舊個體頻繁加入和離開的「churn」事件時,整個網絡的性能會大幅惡化,由於每一個個體的很大一部分資源消耗在相鄰列表更新上(包括自身相鄰列表的更新,和相互之間更新所儲列表),同時許多peer所在的key也須要從新定義;另外,哈希表自己容量是有使用限制的,當哈希表中存儲的數據空間大於其設計容量的一半時,哈希表就會大機率出現「碰撞」事故,這樣的限制也使得依據DHT創建的p2p網絡的總體效率大打折扣。
根據以太坊的運行特色,咱們能夠大概勾勒出以太坊個體也就是客戶端所組成網絡的一些需求特徵:
綜上所述,咱們對以太坊中的p2p網絡設計能夠有個初步思路了:
以後的章節中,咱們能夠逐步瞭解以太坊中的這個p2p網絡通訊是如何完善並實現的。
以太坊中,管理個體間p2p通訊的頂層結構體叫eth.ProtocolManager,它也是eth.Ethereum的核心成員變量之一。先來看一下它的主要UML關係:
ProtocolManager主要成員包括:
小小說明:這裏提到的"遠端"個體,即非本peer的其餘peer對象。以太坊的p2p網絡中,全部進行通訊的兩個peer都必須率先通過相互的註冊(register),並被添加到各自緩存的peer列表,也就是peerSet{}對象中,這樣的兩個peers,就能夠稱爲「相鄰」。因此,這裏提到的「遠端"個體,若是處於可通訊狀態,則一定已經「相鄰」。
在運行方面,Start()函數是ProtocolManager的啓動函數,它會在eth.Ethereum.Start()中被主動調用。ProtocolManager.Start()會啓用4個單獨線程(goroutine,協程)去分別執行4個函數,這也標誌着該以太坊個體p2p通訊的全面啓動。
由Start()啓動的四個函數在業務邏輯上各有側重,下圖是關於它們所在流程的簡單示意圖:
以上這四段相對獨立的業務流程的邏輯分別是:
以上四段流程就是ProtocolManager向相鄰peer主動發起的通訊過程。儘管上述各函數細節從文字閱讀起來容易模糊,不過最重要的內容仍是值得留意下的:本個體(peer)向其餘peer主動發起的通訊中,按照數據類型可分兩類:交易tx和區塊block;而按照通訊方式劃分,亦可分爲廣播新的單個數據和同步一組同類型數據,這樣簡單的兩兩配對,即可組成上述四段流程。
上述函數的實現中,不少地方都體現出巧妙的設計,好比BroadcastBlock()中,若是發送區塊block,因爲數據量相對重量級,則僅僅選擇一小部分相鄰peer,而若是發送hash值 + Number值,則發給全部相鄰peer;又好比txsyncLoop()中,會從map[]中隨機選擇一個peer進行發送(隨機選擇的txsync{}中包含peer)。這些細節,很好的控制了單次業務請求的資源消耗對於定向區域的傾向性,使得整個網絡資源消耗越發均衡,體現出很是全面的設計思路。
對於peer間通訊而言,除了己方須要主動向對方peer發起通訊(好比Start()中啓動的四個獨立流程)以外,還須要一種由對方peer主動調用的數據傳輸,這種傳輸不只僅是由對方peer發給己方,更多的用法是對方peer主動調用一個函數讓己方發給它們某些特定數據。這種通訊方式,在代碼實現上適合用回調(callback)來實現。
ProtocolManager.handle()就是這樣一個函數,它會在ProtocolManager對象建立時,以回調函數的方式「埋入」每一個p2p.Protocol對象中(實現了Protocol.Run()方法)。以後每當有新peer要與己方創建通訊時,若是對方可以支持該Protocol,那麼雙方就能夠順利的創建並開始通訊。如下是handle()的基本代碼:
handle()函數針對一個新peer作了以下幾件事:
剛纔提到,handle()函數以回調函數的形式被放入一個p2p.Protocol{}裏,那麼Protocol對象是如何交給新peer的呢?這部分細節,隱藏在新peer鏈接創建的過程當中。
全部遠端peer與己方之間的通訊,都是經過p2p.Server{}來管理的,Server在整個客戶端最先的啓動步驟Node.Start()中被建立並啓動,而node.Node是用來承載客戶端中全部node.<Service>實現體的容器,下圖簡單示意了Node.Start()中與Server相關的一些步驟:
Node.Start()中首先會建立p2p.Server{},此時Server中的Protocol[]仍是空的;而後將Node中載入的全部<Service>實現體中的Protocol都收集起來,一併交給Server對象,做爲Server.Protocols列表;而後啓動Server對象,並將Server對象做爲參數去逐一啓動每一個<Service>實現體。
而因爲eth.Ethereum對於<Service>.Protocols()的實現中,正是蒐集了ProtocolManager.Protocols而成,因此ProtocolManager.Protocols最終被導入了p2p.Server.Protocols.
那麼Server.Start()中作了什麼呢? 下圖是Server.Start()和run()函數體內,與新peer建立相關的主要邏輯:
能夠看到,Server.Start()中啓動一個單獨線程(listenLoop())去監聽某個端口有無主動發來的IP鏈接;另一個單獨線程啓動run()函數,在無限循環裏處理接收到的任何新消息新對象。在run()函數中,若是有遠端peer發來鏈接請求(新的p2p.conn{}),則調用Server.newPeer()生成新的peer對象,並把Server.Protocols全交給peer。
綜合這兩部分代碼邏輯,能夠發現:
一點體會:
從上述邏輯流程中能夠感覺到,對於以太坊的p2p通訊管理模塊來講,管理Protocol纔是其最重要的任務,尤爲是經過Protocol中的回調函數的設定,能夠在對方peer在發生任何事件時,己方有足夠的邏輯進行響應。這也是這個核心結構體爲什麼被命名爲ProtocolManager,而不是PeerManager的緣由。至於管理peer羣的功能,基本上用一個列表或者map結構,或者peerSet{}就夠了。
在上文的介紹中,出現了多處有關p2p通訊協議的結構類型,好比eth.peer,p2p.Peer,Server等等。這裏不妨對這些p2p通訊協議族的結構一併做個總解。以太坊中用到的p2p通訊協議族的結構類型,大體可分爲三層:
下列UML圖描繪了上述三層p2p通訊協議族中的一些主要結構,但願對於理解以太坊中p2p通訊相關代碼有所幫助。
諸如以太坊這種去中心化的數字貨幣運行系統,天生適用p2p通訊架構。不過原理雖然簡單,在系統架構的層面,依然有不少實現細節須要加以關注。