導語:本文作者在前幾篇文章中展示了一個簡單的區塊鏈,包括生成塊,驗證資料,廣播通訊,PoW,PoS等。本文繼續前文,介紹了p2p網路的基本原理,並且實現了p2p網路區塊鏈。
譯者: ChainGod(孫飛)
原文連結: http://chaingod.io/article/20
在之前的文章中,我們已經知道了怎麼編寫PoW也知道了IPFS怎麼工作, 但是有一個致命的缺點,我們的服務都是中心化的,這篇文章會教你怎麼實現一個簡單的完全去中心化的P2P網路。
背景知識
什麼是P2P網路
在真正的P2P架構中,不需要中心化的服務來維護區塊鏈的狀態。例如,當你給朋友傳送比特幣時,比特幣區塊鏈的“狀態”應該更新,這樣你朋友的餘額就會增加,你的餘額就會減少。
在這個網路中,不存在一個權力高度中心化的機構來維護狀態(銀行就是這樣的中心化機構)。對於比特幣網路來說,每個節點都會維護一份完整的區塊鏈狀態,當交易發生時,每個節點的區塊鏈狀態都會得到更新。這樣,只要網路中51%的節點對區塊鏈的狀態達成一致,那麼區塊鏈網路就是安全可靠的,具體可以閱讀這篇一致性協議文章[4]。
本文將繼續之前的工作,200行Go程式碼實現區塊鏈[5], 並加入P2P網路架構。在繼續之前,強烈建議你先閱讀該篇文章,它會幫助你理解接下來的程式碼。
開始實現
編寫P2P網路可不是開開玩笑就能簡單實現的,有很多邊邊角角的情況都要改寫到,而且需要你擁有很多工程學的知識,這樣的P2P網路才是可擴充套件、高可靠的。有句諺語說得好:站在巨人肩膀上做事,那麼我們先看看巨人們提供了哪些工具吧。
喔,看看,我們發現了什麼!一個用Go語言實現的P2P庫go-libp2p[1]!如果你對新技術足夠敏銳,就會發現這個庫的作者和IPFS的作者是同一個團隊。如果你還沒看過我們的IPFS教程[2],可以看看這裡, 你可以選擇跳過IPFS教程,因為對於本文這不是必須的。
警告
目前來說,go-libp2p主要有兩個缺點:
-
安裝設定比較痛苦,它使用gx作為包管理工具,怎麼說呢,不咋好用,但是湊活用吧
-
目前專案還沒有成熟,正在緊密鑼鼓的開發中,當使用這個庫時,可能會遇到一些資料競爭(data race)
對於第一點,不必擔心,有我們呢。第二點是比較大的問題,但是不會影響我們的程式碼。假如你在使用過程中發現了資料競爭問題,記得給專案提一個issue,幫助它更好的成長!
總之,目前開源世界中,現代化的P2P庫是非常非常少的,因為我們要多給go-libp2p一些耐心和包容,而且就目前來說,它已經能很好的滿足我們的標的了。
安裝設定
最好的環境設定方式是直接clone libp2p庫,然後在這個庫的程式碼中直接開發。你也可以在自己的庫中,呼叫這個庫開發,但是這樣就需要用到gx了。這裡我們使用簡單的方式,假設你已經安裝了Go:
go get -d github.com/libp2p/go-libp2p/…
cd go-libp2p
make
make deps
這裡會透過gx包管理工具下載所有需要的包和依賴,再次申明,我們不喜歡gx,因為它打破了Go語言的很多慣例,但是為了這個很棒的庫,認慫吧。
這裡,我們在examples子目錄下進行開發,因此在go-libp2p的examples下建立一個你自己的目錄
mkdir ./examples/p2p
然後進入到p2p檔案夾下,建立main.go檔案,後面所有的程式碼都會在該檔案中。
你的目錄結構是這樣的:
好了,勇士們,拔出你們的劍,哦不,拔出你們的main.go,開始我們的征途吧!
匯入相關庫
這裡申明我們需要用的庫,大部分庫是來自於go-libp2p本身的,在教程中,你會學到怎麼去使用它們。
spew包可以很方便、優美的打印出我們的區塊鏈,因此記得安裝它:
-
go get github.com/davecgh/go-spew/spew
區塊鏈結構
記住,請先閱讀200行Go程式碼實現區塊鏈, 這樣,下麵的部分就會簡單很多。
先來申明全域性變數:
-
我們是一家健康看護公司,因此Block中存著的是使用者的脈搏速率BPM
-
Blockchain是我們的”狀態”,或者嚴格的說:最新的Blockchain,它其實就是Block的切片(slice)
-
mutex是為了防止資源競爭出現
下麵是Blockchain相關的特定函式:
-
isBlockValid檢查Block的hash是否合法
-
calculateHash使用sha256來對原始資料做hash
-
generateBlock建立一個新的Block區塊,然後新增到區塊鏈Blockchain上,同時會包含所需的事務
P2P結構
下麵我們快接近核心部分了,首先我們要寫出建立主機的邏輯。當一個節點執行我們的程式時,它可以作為一個主機,被其它節點連線。下麵一起看看程式碼:
makeBasicHost函式有3個引數,同時傳回一個host結構體
-
listenPort是主機監聽的埠,其它節點會連線該埠
-
secio表明是否開啟資料流的安全選項,最好開啟,因此它代表了”安全輸入/輸出”
-
randSeed是一個可選的命令列標識,可以允許我們提供一個隨機數種子來為我們的主機生成隨機的地址。這裡我們不會使用
函式的第一個if陳述句針對隨機種子生成隨機key,接著我們生成公鑰和私鑰,這樣能保證主機是安全的。opts部分開始構建網路地址部分,這樣其它節點就可以連線進來。
!secio部分可以繞過加密,但是我們準備使用加密,因此這段程式碼不會被觸發。
接著,建立了主機地址,這樣其他節點就可以連線進來。log.Printf可以用來在控制檯打印出其它節點的連線資訊。最後我們傳回生成的主機地址給呼叫方函式。
流處理
之前的主機需要能處理進入的資料流。當另外一個節點連線到主機時,它會想要提出一個新的區塊鏈,來改寫主機上的區塊鏈,因此我們需要邏輯來判定是否要接受新的區塊鏈。
同時,當我們往本地的區塊鏈新增區塊後,也要把相關資訊廣播給其它節點,這裡也需要實現相關邏輯。
先來建立流處理的基本框架吧:
這裡建立一個新的ReadWriter,為了能支援資料讀取和寫入,同時我們啟動了一個單獨的Go協程來處理相關讀寫邏輯。
讀取資料
首先建立readData函式:
該函式是一個無限迴圈,因為它需要永不停歇的去讀取外面進來的資料。首先,我們使用ReadString解析從其它節點傳送過來的新的區塊鏈(JSON字串)。
然後檢查進來的區塊鏈的長度是否比我們本地的要長,如果進來的鏈更長,那麼我們就接受新的鏈為最新的網路狀態(最新的區塊鏈)。
同時,把最新的區塊鏈在控制檯使用一種特殊的顏色打印出來,這樣我們就知道有新連結受了。
如果在我們主機的本地添加了新的區塊到區塊鏈上,那就需要把本地最新的區塊鏈廣播給其它相連的節點知道,這樣這些節點機會接受並更新到我們的區塊鏈版本。這裡使用writeData函式:
首先是一個單獨協程中的函式,每5秒鐘會將我們的最新的區塊鏈狀態廣播給其它相連的節點。它們收到後,如果發現我們的區塊鏈比它們的要短,就會直接把我們傳送的區塊鏈資訊丟棄,繼續使用它們的區塊鏈,反之則使用我們的區塊鏈。總之,無論哪種方法,所有的節點都會定期的同步本地的區塊鏈到最新狀態。
這裡我們需要一個方法來建立一個新的Block區塊,包含之前提到過的脈搏速率(BPM)。為了簡化實現,我們不會真的去透過物聯網裝置讀取脈搏,而是直接在終端控制臺上輸入一個脈搏速率數字。
首先要驗證輸入的BPM是一個整數型別,然後使用之前的generateBlock來生成區塊,接著使用spew.Dump輸入到終端控制檯,最後我們使用rw.WriteString把最新的區塊鏈廣播給相連的其它節點。
牛逼了我的哥,現在我們完成了區塊鏈相關的函式以及大多數P2P相關的函式。在前面,我們建立了流處理,因此可以讀取和寫入最新的區塊鏈狀態;建立了狀態同步函式,這樣節點之間可以互相同步最新狀態。
剩下的就是實現我們的main函式了:
首先是建立一個創世區塊(如果你讀了200行Go程式碼實現你的區塊鏈,這裡就不會陌生)。
其次我們使用go-libp2p的SetAllLoggers日誌函式來記錄日誌。
接著,設定了所有的命令列標識:
-
secio之前有提到,是用來加密資料流的。在我們的程式中,一定要開啟該標識
-
target指明當前節點要連線到的主機地址
-
listenF是當前節點的監聽主機地址,這樣其它節點就可以連線進來,記住,每個節點都有兩個身份:主機和客戶端, 畢竟P2P不是白叫的
-
seed是隨機數種子,用來建立主機地址時使用
然後,使用makeBasicHost函式來建立一個新的主機地址,如果我們只想做主機不想做客戶端(連線其它的主機),就使用if *target == “”。
接下來的幾行,會從target解析出我們要連線到的主機地址。然後把peerID和主機標的地址targetAddr新增到”store”中,這樣就可以持續跟蹤我們跟其它主機的連線資訊,這裡使用的是ha.Peerstore().AddAddr函式。
接著我們使用ha.NewStream連線到想要連線的節點上,同時為了能接收和傳送最新的區塊鏈資訊,建立了ReadWriter,同時使用一個Go協程來進行readData和writeData。
哇哦
終於完成了,寫文章遠比寫程式碼累!我知道之前的內容有點難,但是相比P2P的複雜性來說,你能透過一個庫來完成P2P網路,已經很牛逼了,所以繼續加油!
完整程式碼
mycoralhealth/blockchain-tutorial
執行結果
現在讓我們來試驗一下,首先開啟3個獨立的終端視窗做為獨立節點。
開始之前,請再次進入go-libp2p的根目錄執行一下make deps,確保所有依賴都正常安裝。
回到你的工作目錄examples/p2p,開啟第一個終端視窗,輸入
go run main.go -l 10000 -secio
細心的讀者會發現有一段話”Now run…”,那還等啥,繼續跟著做吧,開啟第二個終端視窗執行:
go run main.go -l 10001 -d
這是你會發現第一個終端視窗檢測到了新連線!
接著開啟第三個終端視窗,執行:
go run main.go -l 10002 -d
檢查第二終端,又發現了新連線
接著,該我們輸入BPM資料了,在第一個終端視窗中輸入”70″,等幾秒中,觀察各個視窗的列印輸出。
來看看發生了什麼:
-
終端1向本地的區塊鏈添加了一個新的區塊Block
-
終端1向終端2廣播該資訊
-
終端2將新的區塊鏈跟本地的對比,發現終端1的更長,因此使用新的區塊鏈替代了本地的區塊鏈,然後將新的區塊鏈廣播給終端3
-
同上,終端3也進行更新
所有的3個終端節點都把區塊鏈更新到了最新版本,同時沒有使用任何外部的中心化服務,這就是P2P網路的力量!
我們再往終端2的區塊鏈中新增一個區塊試試看,在終端2中輸入”80″
結果忠誠的記錄了我們的正確性,再一次歡呼吧!
下一步
先享受一下自己的工作,你剛用了區區幾百行程式碼就實現了一個全功能的P2P網路!這不是開玩笑,P2P程式設計是非常複雜的,為什麼之前沒有相關的教程,就是因為太難了。
但是,這裡也有幾個可以改進的地方,你可以挑戰一下自己:
-
之前提到過,go-libp2p是存在資料競爭的Bug的,因此如果你要在生產環境使用,需要格外小心。一旦發現Bug,請反饋給作者團隊知道
-
嘗試將本文的P2P網路跟之前的共識協議結合,例如之前的文章PoW 和PoS
-
新增持久化儲存。截止目前,為了簡化實現,我們沒有實現持久化儲存,因此節點關閉,資料就丟失了
-
本文的程式碼沒有在大量節點的環境下測試過,試著寫一個指令碼執行大量節點,看看效能會怎麼變化。如果發現Bug記得提交給我們[6]
學習一下節點發現技術。新節點是怎麼發現已經存在的節點的?這篇文章是一個很好的起點[3]
參考連結:
[1] https://github.com/libp2p/go-libp2p
[2] https://medium.com/@mycoralhealth/learn-to-securely-share-files-on-the-blockchain-with-ipfs-219ee47df54c
[3] https://bitcoin.stackexchange.com/questions/3536/how-do-bitcoin-clients-find-each-other
[4] https://medium.com/@mycoralhealth/code-your-own-blockchain-mining-algorithm-in-go-82c6a71aba1f
[5] https://medium.com/@mycoralhealth/code-your-own-blockchain-in-less-than-200-lines-of-go-e296282bcffc
[6] https://github.com/mycoralhealth/blockchain-tutorial/tree/master/p2p
相關閱讀:
特別推薦:
比特幣、以太坊、ERC20、PoW、PoS、智慧合約、閃電網路……
想深入瞭解及討論這些話題?高可用架構在知識星球(小密圈)建立了區塊鏈學習小組,共同學習區塊鏈包括數字貨幣前沿技術,歡迎點選連結加入。
本文作者 Coral Health,由ChainGod(孫飛)翻譯。轉載譯文請註明出處,技術原創及架構實踐文章,歡迎透過公眾號選單「聯絡我們」進行投稿。
高可用架構
改變網際網路的構建方式
長按二維碼 關註「高可用架構」公眾號