[以太坊源代码分析] VI. 基于p2p的底层通信(上篇)
以太坊作為一個去中心化的系統(tǒng),其底層個體相互間的通信顯然非常重要,所有數(shù)據(jù)的同步,各個個體狀態(tài)的更新,都依賴于整個網(wǎng)絡(luò)中每個個體相互間的通信機制。以太坊的網(wǎng)絡(luò)通信基于peer-to-peer(p2p)通信協(xié)議,又根據(jù)自身傳輸數(shù)據(jù)類型(區(qū)塊,交易,哈希值等),網(wǎng)絡(luò)節(jié)點業(yè)務(wù)相關(guān)性等需求,在各方面做了特別設(shè)計。
由于以太坊中p2p通信相關(guān)代碼量較大,打算分為上下兩篇文章來加以詳解:上篇主要介紹管理p2p通信的核心類ProtocolManager內(nèi)部主要流程,以及通信相關(guān)協(xié)議族的設(shè)計;下篇主要介紹ProtocolManager的兩個成員Fetcher和Downloader,這里是上篇。
1. 一般意義上的p2p網(wǎng)絡(luò)
在開始介紹以太坊的p2p通信機制之前,不妨先來看看一般意義上的p2p網(wǎng)絡(luò)通信的一些特征,以下部分內(nèi)容摘自peer-to-peer_wiki
peer-to-peer(p2p)首先是一種網(wǎng)絡(luò)拓?fù)漕愋?#xff0c;與之對比最顯著的就是client/server(C/S)架構(gòu)。從TCP/IP協(xié)議族分層的角度來說,p2p網(wǎng)絡(luò)中實際的數(shù)據(jù)交換,依然是網(wǎng)絡(luò)層用IP協(xié)議,傳輸層用TCP協(xié)議;而p2p協(xié)議--如果可稱之為協(xié)議的話,應(yīng)算作應(yīng)用層再往上,類似于邏輯拓?fù)鋵?#xff0c;畢竟著名的應(yīng)用層協(xié)議之一FTP,就屬于非常典型的一種C/S架構(gòu)類型。
上圖是C/S架構(gòu)和p2p架構(gòu)的一個簡單示意圖,原圖來自wiki。左圖中C/S架構(gòu)被描繪成星型拓?fù)?#xff0c;這當(dāng)然僅僅是特例,大家可能在工作中遇到各種各樣拓?fù)湫螤畹腃/S架構(gòu),而其核心特征是不變的:C/S 網(wǎng)絡(luò)中的個體地位和功能是不平等的,client個體主要消耗資源,發(fā)起請求,server個體主要提供資源并處理請求,這使得C/S架構(gòu)天然是中心化的。
相比之下,p2p架構(gòu)中最重要的特點在于:其網(wǎng)絡(luò)中的個體在地位和功能上是平等的,雖然每個個體可能處理不同的請求,實際提供的資源在具體量化后可能有差異,但它們都能同時既消耗資源又提供資源。如果把整個所處網(wǎng)絡(luò)中的資源--此處的資源包括但不限于運算能力、存儲空間、網(wǎng)絡(luò)帶寬等,視為一個總量,那么p2p網(wǎng)絡(luò)中的資源分布,是分散于各個個體中的(也許不一定均勻分布)。所以,p2p網(wǎng)絡(luò)架構(gòu)天然是去中心化的、分布式的。
注意上圖右側(cè)p2p網(wǎng)絡(luò)中,并非每個個體與網(wǎng)絡(luò)中其他同類均有通信。這其實也是p2p網(wǎng)絡(luò)的一個很重要的特點:一個個體只需要與相鄰的一部分同類有通信即可,每個個體可與多少相鄰個體、哪些個體有通信,是可以加以設(shè)計的,
無結(jié)構(gòu)化的和有結(jié)構(gòu)化的p2p網(wǎng)絡(luò)
根據(jù)p2p網(wǎng)絡(luò)中節(jié)點相互之間如何聯(lián)系,可以將p2p網(wǎng)絡(luò)簡單區(qū)分為無結(jié)構(gòu)化的(unstructured),和結(jié)構(gòu)化的(structured)兩大類。
無結(jié)構(gòu)化的
這種p2p網(wǎng)絡(luò)即最普通的,不對結(jié)構(gòu)作特別設(shè)計的實現(xiàn)方案。優(yōu)點是結(jié)構(gòu)簡單易于組建,網(wǎng)絡(luò)局部區(qū)域內(nèi)個體可任意分布,反正此時網(wǎng)絡(luò)結(jié)構(gòu)對此也沒有限制;特別是在應(yīng)對大量新個體加入網(wǎng)絡(luò)和舊個體離開網(wǎng)絡(luò)(“churn”)時它的表現(xiàn)非常穩(wěn)定。缺點在于在該網(wǎng)絡(luò)中查找數(shù)據(jù)的效率太低,因為沒有預(yù)知信息,所以往往需要將查詢請求發(fā)遍整個網(wǎng)絡(luò)(至少大多數(shù)個體),這會占用很大一部分網(wǎng)絡(luò)資源,并大大拖慢網(wǎng)絡(luò)中其他業(yè)務(wù)運行。
結(jié)構(gòu)化的
這種p2p網(wǎng)絡(luò)中的個體分布經(jīng)過精心設(shè)計,主要目的是為了提高查詢數(shù)據(jù)的效率,降低查詢數(shù)據(jù)帶來的資源消耗。提高查詢效率的基本手段是對數(shù)據(jù)建立索引,結(jié)構(gòu)化p2p網(wǎng)絡(luò)最普遍的實現(xiàn)方案中使用了分布式哈希表(Distributed Hash Table,DHT),它會對每項數(shù)據(jù)(value)分配一個key以組成(key,value)鍵值對,同時網(wǎng)絡(luò)中每個個體的分布--這里的分布主要指相互通信關(guān)系-根據(jù)key鍵進行關(guān)聯(lián)和擴展。這樣,當(dāng)要查找某項數(shù)據(jù)時,只要跟據(jù)其key鍵就能不斷的縮小查找區(qū)域,大大減少資源消耗。
盡管如此,這樣的p2p網(wǎng)絡(luò)缺點也很明顯:由于每個個體需要存有數(shù)量不少的相鄰個體列表,所以當(dāng)網(wǎng)絡(luò)中發(fā)生大量新舊個體頻繁加入和離開的“churn”事件時,整個網(wǎng)絡(luò)的性能會大幅惡化,因為每個個體的很大一部分資源消耗在相鄰列表更新上(包括自身相鄰列表的更新,和相互之間更新所儲列表),同時許多peer所在的key也需要重新定義;另外,哈希表本身容量是有使用限制的,當(dāng)哈希表中存儲的數(shù)據(jù)空間大于其設(shè)計容量的一半時,哈希表就會大概率出現(xiàn)“碰撞”事故,這樣的限制也使得依據(jù)DHT建立的p2p網(wǎng)絡(luò)的整體效率大打折扣。
對于以太坊通信機制的借鑒
根據(jù)以太坊的運行特點,我們可以大概勾勒出以太坊個體也就是客戶端所組成網(wǎng)絡(luò)的一些需求特征:
綜上所述,我們對以太坊中的p2p網(wǎng)絡(luò)設(shè)計可以有個初步思路了:
- 不需要結(jié)構(gòu)化,經(jīng)過改進的非結(jié)構(gòu)化(比如設(shè)計好相鄰個體列表)網(wǎng)絡(luò)模型可以滿足需求;
- 個體間的相互同步更新需要仔細(xì)設(shè)計;
之后的章節(jié)中,我們可以逐步了解以太坊中的這個p2p網(wǎng)絡(luò)通信是如何完善并實現(xiàn)的。
2. p2p通信的管理模塊ProtocolManager
以太坊中,管理個體間p2p通信的頂層結(jié)構(gòu)體叫eth.ProtocolManager,它也是eth.Ethereum的核心成員變量之一。先來看一下它的主要UML關(guān)系:
ProtocolManager主要成員包括:
- peertSet{}類型成員用來緩存相鄰個體列表,peer{}表示網(wǎng)絡(luò)中的一個遠(yuǎn)端個體。
- 通過各種通道(chan)和事件訂閱(subscription)的方式,接收和發(fā)送包括交易和區(qū)塊在內(nèi)的數(shù)據(jù)更新。當(dāng)然在應(yīng)用中,訂閱也往往利用通道來實現(xiàn)事件通知。
- ProtocolManager用到的這些通道的另一端,可能是其他的個體peer,也可能是系統(tǒng)內(nèi)單例的數(shù)據(jù)源比如txPool,或者是事件訂閱的管理者比如event.Mux。
- Fetcher類型成員累積所有其他個體發(fā)送來的有關(guān)新數(shù)據(jù)的宣布消息,并在自身對照后,安排相應(yīng)的獲取請求。
- Downloader類型成員負(fù)責(zé)所有向相鄰個體主動發(fā)起的同步流程。
小小說明:這里提到的"遠(yuǎn)端"個體,即非本peer的其他peer對象。以太坊的p2p網(wǎng)絡(luò)中,所有進行通信的兩個peer都必須率先經(jīng)過相互的注冊(register),并被添加到各自緩存的peer列表,也就是peerSet{}對象中,這樣的兩個peers,就可以稱為“相鄰”。所以,這里提到的“遠(yuǎn)端"個體,如果處于可通信狀態(tài),則必定已經(jīng)“相鄰”。
在運行方面,Start()函數(shù)是ProtocolManager的啟動函數(shù),它會在eth.Ethereum.Start()中被主動調(diào)用。ProtocolManager.Start()會啟用4個單獨線程(goroutine,協(xié)程)去分別執(zhí)行4個函數(shù),這也標(biāo)志著該以太坊個體p2p通信的全面啟動。
Start():全面啟動p2p通信
由Start()啟動的四個函數(shù)在業(yè)務(wù)邏輯上各有側(cè)重,下圖是關(guān)于它們所在流程的簡單示意圖:
以上這四段相對獨立的業(yè)務(wù)流程的邏輯分別是:
- 廣播新出現(xiàn)的交易對象。txBroadcastLoop()會在txCh通道的收端持續(xù)等待,一旦接收到有關(guān)新交易的事件,會立即調(diào)用BroadcastTx()函數(shù)廣播給那些尚無該交易對象的相鄰個體。
- 廣播新挖掘出的區(qū)塊。minedBroadcastLoop()持續(xù)等待本個體的新挖掘出區(qū)塊事件,然后立即廣播給需要的相鄰個體。當(dāng)不再訂閱新挖掘區(qū)塊事件時,這個函數(shù)才會結(jié)束等待并返回。很有意思的是,在收到新挖掘出區(qū)塊事件后,minedBroadcastLoop()會連續(xù)調(diào)用兩次BroadcastBlock(),兩次調(diào)用僅僅一個bool型參數(shù)@propagate不一樣,當(dāng)該參數(shù)為true時,會將整個新區(qū)塊依次發(fā)給相鄰區(qū)塊中的一小部分;而當(dāng)其為false時,僅僅將新區(qū)塊的Hash值和Number發(fā)送給所有相鄰列表。
- 定時與相鄰個體進行區(qū)塊全鏈的強制同步。syncer()首先啟動fetcher成員,然后進入一個無限循環(huán),每次循環(huán)中都會向相鄰peer列表中“最優(yōu)”的那個peer作一次區(qū)塊全鏈同步。發(fā)起上述同步的理由分兩種:如果有新登記(加入)的相鄰個體,則在整個peer列表數(shù)目大于5時,發(fā)起之;如果沒有新peer到達(dá),則以10s為間隔定時的發(fā)起之。這里所謂"最優(yōu)"指的是peer中所維護區(qū)塊鏈的TotalDifficulty(td)最高,由于Td是全鏈中從創(chuàng)世塊到最新頭塊的Difficulty值總和,所以Td值最高就意味著它的區(qū)塊鏈?zhǔn)亲钚碌?/span>,跟這樣的peer作區(qū)塊全鏈同步,顯然改動量是最小的,此即"最優(yōu)"。
- 將新出現(xiàn)的交易對象均勻的同步給相鄰個體。txsyncLoop()主體也是一個無限循環(huán),它的邏輯稍微復(fù)雜一些:首先有一個數(shù)據(jù)類型txsync{p, txs},包含peer和tx列表;通道txsyncCh用來接收txsync{}對象;txsyncLoop()每次循環(huán)時,如果從通道txsyncCh中收到新數(shù)據(jù),則將它存入一個本地map[]結(jié)構(gòu),k為peer.ID,v為txsync{},并將這組tx對象發(fā)送給這個peer;每次向peer發(fā)送tx對象的上限數(shù)目100*1024,如果txsync{}對象中有剩余tx,則該txsync{}對象繼續(xù)存入map[]并更新tx數(shù)目;如果本次循環(huán)沒有新到達(dá)txsync{},則從map[]結(jié)構(gòu)中隨機找出一個txsync對象,將其中的tx組發(fā)送給相應(yīng)的peer,重復(fù)以上循環(huán)。
以上四段流程就是ProtocolManager向相鄰peer主動發(fā)起的通信過程。盡管上述各函數(shù)細(xì)節(jié)從文字閱讀起來容易模糊,不過最重要的內(nèi)容還是值得留意下的:本個體(peer)向其他peer主動發(fā)起的通信中,按照數(shù)據(jù)類型可分兩類:交易tx和區(qū)塊block;而按照通信方式劃分,亦可分為廣播新的單個數(shù)據(jù)和同步一組同類型數(shù)據(jù),這樣簡單的兩兩配對,便可組成上述四段流程。
上述函數(shù)的實現(xiàn)中,很多地方都體現(xiàn)出巧妙的設(shè)計,比如BroadcastBlock()中,如果發(fā)送區(qū)塊block,由于數(shù)據(jù)量相對重量級,則僅僅選擇一小部分相鄰peer,而如果發(fā)送hash值 + Number值,則發(fā)給所有相鄰peer;又比如txsyncLoop()中,會從map[]中隨機選擇一個peer進行發(fā)送(隨機選擇的txsync{}中包含peer)。這些細(xì)節(jié),很好的控制了單次業(yè)務(wù)請求的資源消耗對于定向區(qū)域的傾向性,使得整個網(wǎng)絡(luò)資源消耗愈加均衡,體現(xiàn)出非常全面的設(shè)計思路。
handle():交給其他peer的回調(diào)函數(shù)
對于peer間通信而言,除了己方需要主動向?qū)Ψ絧eer發(fā)起通信(比如Start()中啟動的四個獨立流程)之外,還需要一種由對方peer主動調(diào)用的數(shù)據(jù)傳輸,這種傳輸不僅僅是由對方peer發(fā)給己方,更多的用法是對方peer主動調(diào)用一個函數(shù)讓己方發(fā)給它們某些特定數(shù)據(jù)。這種通信方式,在代碼實現(xiàn)上適合用回調(diào)(callback)來實現(xiàn)。
ProtocolManager.handle()就是這樣一個函數(shù),它會在ProtocolManager對象創(chuàng)建時,以回調(diào)函數(shù)的方式“埋入”每個p2p.Protocol對象中(實現(xiàn)了Protocol.Run()方法)。之后每當(dāng)有新peer要與己方建立通信時,如果對方能夠支持該Protocol,那么雙方就可以順利的建立并開始通信。以下是handle()的基本代碼:
[plain]?view plain?copy建立新peer連接和傳遞Protocol[]
剛才提到,handle()函數(shù)以回調(diào)函數(shù)的形式被放入一個p2p.Protocol{}里,那么Protocol對象是如何交給新peer的呢?這部分細(xì)節(jié),隱藏在新peer連接建立的過程中。
所有遠(yuǎn)端peer與己方之間的通信,都是通過p2p.Server{}來管理的,Server在整個客戶端最早的啟動步驟Node.Start()中被創(chuàng)建并啟動,而node.Node是用來承載客戶端中所有node.<Service>實現(xiàn)體的容器,下圖簡單示意了Node.Start()中與Server相關(guān)的一些步驟:
Node.Start()中首先會創(chuàng)建p2p.Server{},此時Server中的Protocol[]還是空的;然后將Node中載入的所有<Service>實現(xiàn)體中的Protocol都收集起來,一并交給Server對象,作為Server.Protocols列表;然后啟動Server對象,并將Server對象作為參數(shù)去逐一啟動每個<Service>實現(xiàn)體。
而由于eth.Ethereum對于<Service>.Protocols()的實現(xiàn)中,正是搜集了ProtocolManager.Protocols而成,所以ProtocolManager.Protocols最終被導(dǎo)入了p2p.Server.Protocols.
那么Server.Start()中做了什么呢? 下圖是Server.Start()和run()函數(shù)體內(nèi),與新peer創(chuàng)建相關(guān)的主要邏輯:
可以看到,Server.Start()中啟動一個單獨線程(listenLoop())去監(jiān)聽某個端口有無主動發(fā)來的IP連接;另外一個單獨線程啟動run()函數(shù),在無限循環(huán)里處理接收到的任何新消息新對象。在run()函數(shù)中,如果有遠(yuǎn)端peer發(fā)來連接請求(新的p2p.conn{}),則調(diào)用Server.newPeer()生成新的peer對象,并把Server.Protocols全交給peer。
綜合這兩部分代碼邏輯,可以發(fā)現(xiàn):
一點體會:
從上述邏輯流程中可以感受到,對于以太坊的p2p通信管理模塊來說,管理Protocol才是其最重要的任務(wù),尤其是通過Protocol中的回調(diào)函數(shù)的設(shè)定,可以在對方peer在發(fā)生任何事件時,己方有足夠的邏輯進行響應(yīng)。這也是這個核心結(jié)構(gòu)體為何被命名為ProtocolManager,而不是PeerManager的原因。至于管理peer群的功能,基本上用一個列表或者map結(jié)構(gòu),或者peerSet{}就夠了。
3. p2p通信協(xié)議族的結(jié)構(gòu)設(shè)計
在上文的介紹中,出現(xiàn)了多處有關(guān)p2p通信協(xié)議的結(jié)構(gòu)類型,比如eth.peer,p2p.Peer,Server等等。這里不妨對這些p2p通信協(xié)議族的結(jié)構(gòu)一并作個總解。以太坊中用到的p2p通信協(xié)議族的結(jié)構(gòu)類型,大致可分為三層:
- 第一層處于pkg eth中,可以直接被eth.Ethereum,eth.ProtocolManager等頂層管理模塊使用,在類型聲明上也明顯考慮了eth.Ethereum的使用特點。典型的有eth.peer{}, eth.peerSet{},其中peerSet是peer的集合類型,而eth.peer代表了遠(yuǎn)端通信對象和其所有通信操作,它封裝更底層的p2p.Peer對象以及讀寫通道等。
- 第二層屬于pkg p2p,可認(rèn)為是泛化的p2p通信結(jié)構(gòu),比較典型的結(jié)構(gòu)類型包括代表遠(yuǎn)端通信對象的p2p.Peer{}, 封裝自更底層連接對象的conn{},通信用通道對象protoRW{}, 以及啟動監(jiān)聽、處理新加入連接或斷開連接的Server{}。這一層中,各種數(shù)據(jù)類型的界限比較清晰,盡量不出現(xiàn)揉雜的情況,這也是泛化結(jié)構(gòu)的需求。值得關(guān)注的是p2p.Protocol{},它應(yīng)該是針對上層應(yīng)用特意開辟的類型,主要作用包括容納應(yīng)用程序所要求的回調(diào)函數(shù)等,并通過p2p.Server{}在新連接建立后,將其傳遞給通信對象peer。從這個類型所起的作用來看,命名為Protocol還是比較貼切的,盡管不應(yīng)將其與TCP/IP協(xié)議等既有概念混淆。
- 第三層處于golang自帶的網(wǎng)絡(luò)代碼包中,也可分為兩部分:第一部分pkg net,包括代表網(wǎng)絡(luò)連接的<Conn>接口,代表網(wǎng)絡(luò)地址的<Addr>以及它們的實現(xiàn)類;第二部分pkg syscall,包括更底層的網(wǎng)絡(luò)相關(guān)系統(tǒng)調(diào)用類等,可視為封裝了網(wǎng)絡(luò)層(IP)和傳輸層(TCP)協(xié)議的系統(tǒng)實現(xiàn)。
下列UML圖描繪了上述三層p2p通信協(xié)議族中的一些主要結(jié)構(gòu),希望對于理解以太坊中p2p通信相關(guān)代碼有所幫助。
小結(jié):
諸如以太坊這種去中心化的數(shù)字貨幣運行系統(tǒng),天生適用p2p通信架構(gòu)。不過原理雖然簡單,在系統(tǒng)架構(gòu)的層面,依然有很多實現(xiàn)細(xì)節(jié)需要加以關(guān)注。
原文:http://blog.csdn.net/teaspring/article/details/78455046
總結(jié)
以上是生活随笔為你收集整理的[以太坊源代码分析] VI. 基于p2p的底层通信(上篇)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [以太坊源代码分析] IV. 椭圆曲线密
- 下一篇: 分布式一致性与共识算法