2018-04-20?Chain God?程序新視界
外文發(fā)表日期: 2018-04-14?
外文鏈接:https://medium.com/coinmonks/code-a-simple-p2p-blockchain-in-go-46662601f417
在之前的文章中,我們已經(jīng)知道了怎么編寫PoW也知道了IPFS怎么工作, 但是有一個致命的缺點,我們的服務(wù)都是中心化的,這篇文章會教你怎么實現(xiàn)一個簡單的完全去中心化的P2P網(wǎng)絡(luò)。
背景知識
什么是P2P網(wǎng)絡(luò)
在真正的P2P架構(gòu)中,不需要中心化的服務(wù)來維護區(qū)塊鏈的狀態(tài)。例如,當(dāng)你給朋友發(fā)送比特幣時,比特幣區(qū)塊鏈的“狀態(tài)”應(yīng)該更新,這樣你朋友的余額就會增加,你的余額就會減少。
在這個網(wǎng)絡(luò)中,不存在一個權(quán)力高度中心化的機構(gòu)來維護狀態(tài)(銀行就是這樣的中心化機構(gòu))。對于比特幣網(wǎng)絡(luò)來說,每個節(jié)點都會維護一份完整的區(qū)塊鏈狀態(tài),當(dāng)交易發(fā)生時,每個節(jié)點的區(qū)塊鏈狀態(tài)都會得到更新。這樣,只要網(wǎng)絡(luò)中51%的節(jié)點對區(qū)塊鏈的狀態(tài)達成一致,那么區(qū)塊鏈網(wǎng)絡(luò)就是安全可靠的,具體可以閱讀這篇一致性協(xié)議文章。
本文將繼續(xù)之前的工作,200行Go代碼實現(xiàn)區(qū)塊鏈, 并加入P2P網(wǎng)絡(luò)架構(gòu)。在繼續(xù)之前,強烈建議你先閱讀該篇文章,它會幫助你理解接下來的代碼。
開始實現(xiàn)
編寫P2P網(wǎng)絡(luò)可不是開開玩笑就能簡單視線的,有很多邊邊角角的情況都要覆蓋到,而且需要你擁有很多工程學(xué)的知識,這樣的P2P網(wǎng)絡(luò)才是可擴展、高可靠的。有句諺語說得好:站在巨人肩膀上做事,那么我們先看看巨人們提供了哪些工具吧。
喔,看看,我們發(fā)現(xiàn)了什么!一個用Go語言實現(xiàn)的P2P庫go-libp2p!如果你對新技術(shù)足夠敏銳,就會發(fā)現(xiàn)這個庫的作者和IPFS的作者是同一個團隊。如果你還沒看過我們的IPFS教程,可以看看這里, 你可以選擇跳過IPFS教程,因為對于本文這不是必須的。
警告
目前來說,go-libp2p主要有兩個缺點:
安裝設(shè)置比較痛苦,它使用gx作為包管理工具,怎么說呢,不咋好用,但是湊活用吧
目前項目還沒有成熟,正在緊密鑼鼓的開發(fā)中,當(dāng)使用這個庫時,可能會遇到一些數(shù)據(jù)競爭(data race)
對于第一點,不必擔(dān)心,有我們呢。第二點是比較大的問題,但是不會影響我們的代碼。假如你在使用過程中發(fā)現(xiàn)了數(shù)據(jù)競爭問題,記得給項目提一個issue,幫助它更好的成長!
總之,目前開源世界中,現(xiàn)代化的P2P庫是非常非常少的,因為我們要多給go-libp2p一些耐心和包容,而且就目前來說,它已經(jīng)能很好的滿足我們的目標(biāo)了。
安裝設(shè)置
最好的環(huán)境設(shè)置方式是直接clonelibp2p庫,然后在這個庫的代碼中直接開發(fā)。你也可以在自己的庫中,調(diào)用這個庫開發(fā),但是這樣就需要用到gx了。這里我們使用簡單的方式,假設(shè)你已經(jīng)安裝了Go:
go?get-d github.com/libp2p/go-libp2p/…
進入?go-libp2p文件夾
make
make deps
這里會通過gx包管理工具下載所有需要的包和依賴,再次申明,我們不喜歡gx,因為它打破了Go語言的很多慣例,但是為了這個很棒的庫,認(rèn)慫吧。
這里,我們在examples子目錄下進行開發(fā),因此在go-libp2p的examples下創(chuàng)建一個你自己的目錄
mkdir?./examples/p2p
然后進入到p2p文件夾下,創(chuàng)建main.go文件,后面所有的代碼都會在該文件中。
你的目錄結(jié)構(gòu)是這樣的:
好了,勇士們,拔出你們的劍,哦不,拔出你們的main.go,開始我們的征途吧!
導(dǎo)入相關(guān)庫
這里申明我們需要用的庫,大部分庫是來自于go-libp2p本身的,在教程中,你會學(xué)到怎么去使用它們。
package main
import (
? ?"bufio"
? ?"context"
? ?"crypto/rand"
? ?"crypto/sha256"
? ?"encoding/hex"
? ?"encoding/json"
? ?"flag"
? ?"fmt"
? ?"io"
? ?"log"
? ?mrand "math/rand"
? ?"os"
? ?"strconv"
? ?"strings"
? ?"sync"
? ?"time"
? ?"github.com/davecgh/go-spew/spew"
? ?golog "github.com/ipfs/go-log"
? ?libp2p "github.com/libp2p/go-libp2p"
? ?crypto "github.com/libp2p/go-libp2p-crypto"
? ?host "github.com/libp2p/go-libp2p-host"
? ?net "github.com/libp2p/go-libp2p-net"
? ?peer "github.com/libp2p/go-libp2p-peer"
? ?pstore "github.com/libp2p/go-libp2p-peerstore"
? ?ma "github.com/multiformats/go-multiaddr"
? ?gologging "github.com/whyrusleeping/go-logging"
)
spew包可以很方便、優(yōu)美的打印出我們的區(qū)塊鏈,因此記得安裝它:
go?getgithub.com/davecgh/go-spew/spew
區(qū)塊鏈結(jié)構(gòu)
記住,請先閱讀200行Go代碼實現(xiàn)區(qū)塊鏈, 這樣,下面的部分就會簡單很多。
先來申明全局變量:
// Block represents each 'item' in the blockchain
type Block struct {
? ?Index ? ? int
? ?Timestamp string
? ?BPM ? ? ? int
? ?Hash ? ? ?string
? ?PrevHash ?string
}
// Blockchain is a series of validated Blocks
var Blockchain []Block
var mutex = &sync.Mutex{}
我們是一家健康看護公司,因此Block中存著的是用戶的脈搏速率BPM
Blockchain是我們的"狀態(tài)",或者嚴(yán)格的說:最新的Blockchain,它其實就是Block的切片(slice)
mutex是為了防止資源競爭出現(xiàn)
下面是Blockchain相關(guān)的特定函數(shù):
// make sure block is valid by checking index, and comparing the hash of the previous block
func isBlockValid(newBlock, oldBlock Block) bool {
? ?if oldBlock.Index+1 != newBlock.Index {
? ? ? ?return false
? ?}
? ?if oldBlock.Hash != newBlock.PrevHash {
? ? ? ?return false
? ?}
? ?if calculateHash(newBlock) != newBlock.Hash {
? ? ? ?return false
? ?}
? ?return true
}
// SHA256 hashing
func calculateHash(block Block) string {
? ?record := strconv.Itoa(block.Index) + block.Timestamp + strconv.Itoa(block.BPM) + block.PrevHash
? ?h := sha256.New()
? ?h.Write([]byte(record))
? ?hashed := h.Sum(nil)
? ?return hex.EncodeToString(hashed)
}
// create a new block using previous block's hash
func generateBlock(oldBlock Block, BPM int) Block {
? ?var newBlock Block
? ?t := time.Now()
? ?newBlock.Index = oldBlock.Index + 1
? ?newBlock.Timestamp = t.String()
? ?newBlock.BPM = BPM
? ?newBlock.PrevHash = oldBlock.Hash
? ?newBlock.Hash = calculateHash(newBlock)
? ?return newBlock
}
isBlockValid檢查Block的hash是否合法
calculateHash使用?sha256來對原始數(shù)據(jù)做hash
generateBlock創(chuàng)建一個新的Block區(qū)塊,然后添加到區(qū)塊鏈Blockchain上,同時會包含所需的事務(wù)
P2P結(jié)構(gòu)
下面我們快接近核心部分了,首先我們要寫出創(chuàng)建主機的邏輯。當(dāng)一個節(jié)點運行我們的程序時,它可以作為一個主機,被其它節(jié)點連接。下面一起看看代碼:-)
// makeBasicHost creates a LibP2P host with a random peer ID listening on the
// given multiaddress. It will use secio if secio is true.
func makeBasicHost(listenPort int, secio bool, randseed int64) (host.Host, error) {
? ?// If the seed is zero, use real cryptographic randomness. Otherwise, use a
? ?// deterministic randomness source to make generated keys stay the same
? ?// across multiple runs
? ?var r io.Reader
? ?if randseed == 0 {
? ? ? ?r = rand.Reader
? ?} else {
? ? ? ?r = mrand.New(mrand.NewSource(randseed))
? ?}
? ?// Generate a key pair for this host. We will use it
? ?// to obtain a valid host ID.
? ?priv, _, err := crypto.GenerateKeyPairWithReader(crypto.RSA, 2048, r)
? ?if err != nil {
? ? ? ?return nil, err
? ?}
? ?opts := []libp2p.Option{
? ? ? ?libp2p.ListenAddrStrings(fmt.Sprintf("/ip4/127.0.0.1/tcp/%d", listenPort)),
? ? ? ?libp2p.Identity(priv),
? ?}
? ?if !secio {
? ? ? ?opts = append(opts, libp2p.NoEncryption())
? ?}
? ?basicHost, err := libp2p.New(context.Background(), opts...)
? ?if err != nil {
? ? ? ?return nil, err
? ?}
? ?// Build host multiaddress
? ?hostAddr, _ := ma.NewMultiaddr(fmt.Sprintf("/ipfs/%s", basicHost.ID().Pretty()))
? ?// Now we can build a full multiaddress to reach this host
? ?// by encapsulating both addresses:
? ?addr := basicHost.Addrs()[0]
? ?fullAddr := addr.Encapsulate(hostAddr)
? ?log.Printf("I am %s\n", fullAddr)
? ?if secio {
? ? ? ?log.Printf("Now run \"go run main.go -l %d -d %s -secio\" on a different terminal\n", listenPort+1, fullAddr)
? ?} else {
? ? ? ?log.Printf("Now run \"go run main.go -l %d -d %s\" on a different terminal\n", listenPort+1, fullAddr)
? ?}
? ?return basicHost, nil
}
makeBasicHost函數(shù)有3個參數(shù),同時返回一個host結(jié)構(gòu)體
listenPort是主機監(jiān)聽的端口,其它節(jié)點會連接該端口
secio表明是否開啟數(shù)據(jù)流的安全選項,最好開啟,因此它代表了"安全輸入/輸出"
randSeed是一個可選的命令行標(biāo)識,可以允許我們提供一個隨機數(shù)種子來為我們的主機生成隨機的地址。這里我們不會使用
函數(shù)的第一個if語句針對隨機種子生成隨機key,接著我們生成公鑰和私鑰,這樣能保證主機是安全的。opts部分開始構(gòu)建網(wǎng)絡(luò)地址部分,這樣其它節(jié)點就可以連接進來。
!secio部分可以繞過加密,但是我們準(zhǔn)備使用加密,因此這段代碼不會被觸發(fā)。
接著,創(chuàng)建了主機地址,這樣其他節(jié)點就可以連接進來。log.Printf可以用來在控制臺打印出其它節(jié)點的連接信息。最后我們返回生成的主機地址給調(diào)用方函數(shù)。
流處理
之前的主機需要能處理進入的數(shù)據(jù)流。當(dāng)另外一個節(jié)點連接到主機時,它會想要提出一個新的區(qū)塊鏈,來覆蓋主機上的區(qū)塊鏈,因此我們需要邏輯來判定是否要接受新的區(qū)塊鏈。
同時,當(dāng)我們往本地的區(qū)塊鏈添加區(qū)塊后,也要把相關(guān)信息廣播給其它節(jié)點,這里也需要實現(xiàn)相關(guān)邏輯。
先來創(chuàng)建流處理的基本框架吧:
func handleStream(s net.Stream) {
? ?log.Println("Got a new stream!")
? ?// Create a buffer stream for non blocking read and write.
? ?rw := bufio.NewReadWriter(bufio.NewReader(s), bufio.NewWriter(s))
? ?go readData(rw)
? ?go writeData(rw)
? ?// stream 's' will stay open until you close it (or the other side closes it).
}
這里創(chuàng)建一個新的ReadWriter,為了能支持?jǐn)?shù)據(jù)讀取和寫入,同時我們啟動了一個單獨的Go協(xié)程來處理相關(guān)讀寫邏輯。
讀取數(shù)據(jù)
首先創(chuàng)建readData函數(shù):
func readData(rw *bufio.ReadWriter) {
? ?for {
? ? ? ?str, err := rw.ReadString('\n')
? ? ? ?if err != nil {
? ? ? ? ? ?log.Fatal(err)
? ? ? ?}
? ? ? ?if str == "" {
? ? ? ? ? ?return
? ? ? ?}
? ? ? ?if str != "\n" {
? ? ? ? ? ?chain := make([]Block, 0)
? ? ? ? ? ?if err := json.Unmarshal([]byte(str), &chain); err != nil {
? ? ? ? ? ? ? ?log.Fatal(err)
? ? ? ? ? ?}
? ? ? ? ? ?mutex.Lock()
? ? ? ? ? ?if len(chain) > len(Blockchain) {
? ? ? ? ? ? ? ?Blockchain = chain
? ? ? ? ? ? ? ?bytes, err := json.MarshalIndent(Blockchain, "", " ?")
? ? ? ? ? ? ? ?if err != nil {
? ? ? ? ? ? ? ? ? ?log.Fatal(err)
? ? ? ? ? ? ? ?}
? ? ? ? ? ? ? ?// Green console color: ? ? \x1b[32m
? ? ? ? ? ? ? ?// Reset console color: ? ? \x1b[0m
? ? ? ? ? ? ? ?fmt.Printf("\x1b[32m%s\x1b[0m> ", string(bytes))
? ? ? ? ? ?}
? ? ? ? ? ?mutex.Unlock()
? ? ? ?}
? ?}
}
該函數(shù)是一個無限循環(huán),因為它需要永不停歇的去讀取外面進來的數(shù)據(jù)。首先,我們使用ReadString解析從其它節(jié)點發(fā)送過來的新的區(qū)塊鏈(JSON字符串)。
然后檢查進來的區(qū)塊鏈的長度是否比我們本地的要長,如果進來的鏈更長,那么我們就接受新的鏈為最新的網(wǎng)絡(luò)狀態(tài)(最新的區(qū)塊鏈)。
同時,把最新的區(qū)塊鏈在控制臺使用一種特殊的顏色打印出來,這樣我們就知道有新鏈接受了。
如果在我們主機的本地添加了新的區(qū)塊到區(qū)塊鏈上,那就需要把本地最新的區(qū)塊鏈廣播給其它相連的節(jié)點知道,這樣這些節(jié)點機會接受并更新到我們的區(qū)塊鏈版本。這里使用writeData函數(shù):
func writeData(rw *bufio.ReadWriter) {
? ?go func() {
? ? ? ?for {
? ? ? ? ? ?time.Sleep(5 * time.Second)
? ? ? ? ? ?mutex.Lock()
? ? ? ? ? ?bytes, err := json.Marshal(Blockchain)
? ? ? ? ? ?if err != nil {
? ? ? ? ? ? ? ?log.Println(err)
? ? ? ? ? ?}
? ? ? ? ? ?mutex.Unlock()
? ? ? ? ? ?mutex.Lock()
? ? ? ? ? ?rw.WriteString(fmt.Sprintf("%s\n", string(bytes)))
? ? ? ? ? ?rw.Flush()
? ? ? ? ? ?mutex.Unlock()
? ? ? ?}
? ?}()
? ?stdReader := bufio.NewReader(os.Stdin)
? ?for {
? ? ? ?fmt.Print("> ")
? ? ? ?sendData, err := stdReader.ReadString('\n')
? ? ? ?if err != nil {
? ? ? ? ? ?log.Fatal(err)
? ? ? ?}
? ? ? ?sendData = strings.Replace(sendData, "\n", "", -1)
? ? ? ?bpm, err := strconv.Atoi(sendData)
? ? ? ?if err != nil {
? ? ? ? ? ?log.Fatal(err)
? ? ? ?}
? ? ? ?newBlock := generateBlock(Blockchain[len(Blockchain)-1], bpm)
? ? ? ?if isBlockValid(newBlock, Blockchain[len(Blockchain)-1]) {
? ? ? ? ? ?mutex.Lock()
? ? ? ? ? ?Blockchain = append(Blockchain, newBlock)
? ? ? ? ? ?mutex.Unlock()
? ? ? ?}
? ? ? ?bytes, err := json.Marshal(Blockchain)
? ? ? ?if err != nil {
? ? ? ? ? ?log.Println(err)
? ? ? ?}
? ? ? ?spew.Dump(Blockchain)
? ? ? ?mutex.Lock()
? ? ? ?rw.WriteString(fmt.Sprintf("%s\n", string(bytes)))
? ? ? ?rw.Flush()
? ? ? ?mutex.Unlock()
? ?}
}
首先是一個單獨協(xié)程中的函數(shù),每5秒鐘會將我們的最新的區(qū)塊鏈狀態(tài)廣播給其它相連的節(jié)點。它們收到后,如果發(fā)現(xiàn)我們的區(qū)塊鏈比它們的要短,就會直接把我們發(fā)送的區(qū)塊鏈信息丟棄,繼續(xù)使用它們的區(qū)塊鏈,反之則使用我們的區(qū)塊鏈。總之,無論哪種方法,所有的節(jié)點都會定期的同步本地的區(qū)塊鏈到最新狀態(tài)。
這里我們需要一個方法來創(chuàng)建一個新的Block區(qū)塊,包含之前提到過的脈搏速率(BPM)。為了簡化實現(xiàn),我們不會真的去通過物聯(lián)網(wǎng)設(shè)備讀取脈搏,而是直接在終端控制臺上輸入一個脈搏速率數(shù)字。
首先要驗證輸入的BPM是一個整數(shù)類型,然后使用之前的generateBlock來生成區(qū)塊,接著使用spew.Dump輸入到終端控制臺,最后我們使用rw.WriteString把最新的區(qū)塊鏈廣播給相連的其它節(jié)點。
牛逼了我的哥,現(xiàn)在我們完成了區(qū)塊鏈相關(guān)的函數(shù)以及大多數(shù)P2P相關(guān)的函數(shù)。在前面,我們創(chuàng)建了流處理,因此可以讀取和寫入最新的區(qū)塊鏈狀態(tài);創(chuàng)建了狀態(tài)同步函數(shù),這樣節(jié)點之間可以互相同步最新狀態(tài)。
剩下的就是實現(xiàn)我們的main函數(shù)了:
func main() {
? ?t := time.Now()
? ?genesisBlock := Block{}
? ?genesisBlock = Block{0, t.String(), 0, calculateHash(genesisBlock), ""}
? ?Blockchain = append(Blockchain, genesisBlock)
? ?// LibP2P code uses golog to log messages. They log with different
? ?// string IDs (i.e. "swarm"). We can control the verbosity level for
? ?// all loggers with:
? ?golog.SetAllLoggers(gologging.INFO) // Change to DEBUG for extra info
? ?// Parse options from the command line
? ?listenF := flag.Int("l", 0, "wait for incoming connections")
? ?target := flag.String("d", "", "target peer to dial")
? ?secio := flag.Bool("secio", false, "enable secio")
? ?seed := flag.Int64("seed", 0, "set random seed for id generation")
? ?flag.Parse()
? ?if *listenF == 0 {
? ? ? ?log.Fatal("Please provide a port to bind on with -l")
? ?}
? ?// Make a host that listens on the given multiaddress
? ?ha, err := makeBasicHost(*listenF, *secio, *seed)
? ?if err != nil {
? ? ? ?log.Fatal(err)
? ?}
? ?if *target == "" {
? ? ? ?log.Println("listening for connections")
? ? ? ?// Set a stream handler on host A. /p2p/1.0.0 is
? ? ? ?// a user-defined protocol name.
? ? ? ?ha.SetStreamHandler("/p2p/1.0.0", handleStream)
? ? ? ?select {} // hang forever
? ? ? ?/**** This is where the listener code ends ****/
? ?} else {
? ? ? ?ha.SetStreamHandler("/p2p/1.0.0", handleStream)
? ? ? ?// The following code extracts target's peer ID from the
? ? ? ?// given multiaddress
? ? ? ?ipfsaddr, err := ma.NewMultiaddr(*target)
? ? ? ?if err != nil {
? ? ? ? ? ?log.Fatalln(err)
? ? ? ?}
? ? ? ?pid, err := ipfsaddr.ValueForProtocol(ma.P_IPFS)
? ? ? ?if err != nil {
? ? ? ? ? ?log.Fatalln(err)
? ? ? ?}
? ? ? ?peerid, err := peer.IDB58Decode(pid)
? ? ? ?if err != nil {
? ? ? ? ? ?log.Fatalln(err)
? ? ? ?}
? ? ? ?// Decapsulate the /ipfs/ part from the target
? ? ? ?// /ip4//ipfs/ becomes /ip4/
? ? ? ?targetPeerAddr, _ := ma.NewMultiaddr(
? ? ? ? ? ?fmt.Sprintf("/ipfs/%s", peer.IDB58Encode(peerid)))
? ? ? ?targetAddr := ipfsaddr.Decapsulate(targetPeerAddr)
? ? ? ?// We have a peer ID and a targetAddr so we add it to the peerstore
? ? ? ?// so LibP2P knows how to contact it
? ? ? ?ha.Peerstore().AddAddr(peerid, targetAddr, pstore.PermanentAddrTTL)
? ? ? ?log.Println("opening stream")
? ? ? ?// make a new stream from host B to host A
? ? ? ?// it should be handled on host A by the handler we set above because
? ? ? ?// we use the same /p2p/1.0.0 protocol
? ? ? ?s, err := ha.NewStream(context.Background(), peerid, "/p2p/1.0.0")
? ? ? ?if err != nil {
? ? ? ? ? ?log.Fatalln(err)
? ? ? ?}
? ? ? ?// Create a buffered stream so that read and writes are non blocking.
? ? ? ?rw := bufio.NewReadWriter(bufio.NewReader(s), bufio.NewWriter(s))
? ? ? ?// Create a thread to read and write data.
? ? ? ?go writeData(rw)
? ? ? ?go readData(rw)
? ? ? ?select {} // hang forever
? ?}
}
首先是創(chuàng)建一個創(chuàng)世區(qū)塊(如果你讀了200行Go代碼實現(xiàn)你的區(qū)塊鏈,這里就不會陌生)。
其次我們使用go-libp2p的SetAllLoggers日志函數(shù)來記錄日志。
接著,設(shè)置了所有的命令行標(biāo)識:
secio之前有提到,是用來加密數(shù)據(jù)流的。在我們的程序中,一定要打開該標(biāo)識
target指明當(dāng)前節(jié)點要連接到的主機地址
listenF是當(dāng)前節(jié)點的監(jiān)聽主機地址,這樣其它節(jié)點就可以連接進來,記住,每個節(jié)點都有兩個身份:主機和客戶端, 畢竟P2P不是白叫的
seed是隨機數(shù)種子,用來創(chuàng)建主機地址時使用
然后,使用makeBasicHost函數(shù)來創(chuàng)建一個新的主機地址,如果我們只想做主機不想做客戶端(連接其它的主機),就使用if*target==“”。
接下來的幾行,會從target解析出我們要連接到的主機地址。然后把peerID和主機目標(biāo)地址targetAddr添加到"store"中,這樣就可以持續(xù)跟蹤我們跟其它主機的連接信息,這里使用的是ha.Peerstore().AddAddr函數(shù)。
接著我們使用ha.NewStream連接到想要連接的節(jié)點上,同時為了能接收和發(fā)送最新的區(qū)塊鏈信息,創(chuàng)建了ReadWriter,同時使用一個Go協(xié)程來進行readData和writeData。
哇哦
終于完成了,寫文章遠比寫代碼累!我知道之前的內(nèi)容有點難,但是相比P2P的復(fù)雜性來說,你能通過一個庫來完成P2P網(wǎng)絡(luò),已經(jīng)很牛逼了,所以繼續(xù)加油!
完整代碼
mycoralhealth/blockchain-tutorial
運行結(jié)果
現(xiàn)在讓我們來試驗一下,首先打開3個獨立的終端窗口做為獨立節(jié)點。
開始之前,請再次進入go-libp2p的根目錄運行一下make deps,確保所有依賴都正常安裝。
回到你的工作目錄examples/p2p,打開第一個終端窗口,輸入go run main.go-l10000-secio
細心的讀者會發(fā)現(xiàn)有一段話"Now run…",那還等啥,繼續(xù)跟著做吧,打開第二個終端窗口運行:go run main.go-l10001-d-secio
這是你會發(fā)現(xiàn)第一個終端窗口檢測到了新連接!
接著打開第三個終端窗口,運行:go run main.go-l10002-d-secio
檢查第二終端,又發(fā)現(xiàn)了新連接
接著,該我們輸入BPM數(shù)據(jù)了,在第一個終端窗口中輸入"70",等幾秒中,觀察各個窗口的打印輸出。
來看看發(fā)生了什么:
終端1向本地的區(qū)塊鏈添加了一個新的區(qū)塊Block
終端1向終端2廣播該信息
終端2將新的區(qū)塊鏈跟本地的對比,發(fā)現(xiàn)終端1的更長,因此使用新的區(qū)塊鏈替代了本地的區(qū)塊鏈,然后將新的區(qū)塊鏈廣播給終端3
同上,終端3也進行更新
所有的3個終端節(jié)點都把區(qū)塊鏈更新到了最新版本,同時沒有使用任何外部的中心化服務(wù),這就是P2P網(wǎng)絡(luò)的力量!
我們再往終端2的區(qū)塊鏈中添加一個區(qū)塊試試看,在終端2中輸入"80"
結(jié)果忠誠的記錄了我們的正確性,再一次歡呼吧!
下一步
先享受一下自己的工作,你剛用了區(qū)區(qū)幾百行代碼就實現(xiàn)了一個全功能的P2P網(wǎng)絡(luò)!這不是開玩笑,P2P編程時非常復(fù)雜的,為什么之前沒有相關(guān)的教程,就是因為太難了。
但是,這里也有幾個可以改進的地方,你可以挑戰(zhàn)一下自己:
之前提到過,?go-libp2p是存在數(shù)據(jù)競爭的Bug的,因此如果你要在生產(chǎn)環(huán)境使用,需要格外小心。一旦發(fā)現(xiàn)Bug,請反饋給作者團隊知道
嘗試將本文的P2P網(wǎng)絡(luò)跟之前的共識協(xié)議結(jié)合,例如之前的文章PoW?和PoS?(PoS是中文譯文)
添加持久化存儲。截止目前,為了簡化實現(xiàn),我們沒有實現(xiàn)持久化存儲,因此節(jié)點關(guān)閉,數(shù)據(jù)就丟失了
本文的代碼沒有在大量節(jié)點的環(huán)境下測試過,試著寫一個腳本運行大量節(jié)點,看看性能會怎么變化。如果發(fā)現(xiàn)Bug記得給我們提交
學(xué)習(xí)一下節(jié)點發(fā)現(xiàn)技術(shù)。新節(jié)點是怎么發(fā)現(xiàn)已經(jīng)存在的節(jié)點的?這篇文章是一個很好的起點
更多資訊可關(guān)注微信公眾號:程序新視界
VIP一對一咨詢可關(guān)注知識星球(小密圈)
優(yōu)優(yōu)區(qū)塊鏈課堂
了解更多