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