搭建简单P2P网络
背景
什么是点对点P2P?
在真实的P2P结构中,你不需要一个中心化的服务来维持区块链的状态.
举个例子,当你转账了一些比特币给你朋友,比特币区块链的状态需要被更新.
所以你朋友的余额会增加,你的余额会减少.
这里没有一个像银行一样的中心权威机构去维持区块链的状态.
取而代之的是,在比特币网络中的所有想要维持比特币备份的节点,
都会去更新他们的区块链备份,包含你的交易.
这样一来,只要网络中51%以上的节点同意区块链的状态,它就保持了它的准确性.
开始编写代码
编写P2P网络代码并不简单,有非常多的极端例子,
并且需要非常多的工程来让它保持扩展性和可靠性.
像任何一个好的工程一样,我们来先看一下需要哪些工具.
幸运的是,有一个出色的P2Pgo语言库叫做go-libp2p.
同时,它也是发明IPFS的那群人开发的.
警告
据我们所知,go-libp2p包有2个缺点.
1.安装非常痛苦.它使用了gx作为包管理工具,gx并不是很方便.
2.它依然在开发中,当我们使用的时候,可能会遇到一些数据冲突.它们有办法消除.
不要担心第一个缺点,我们会帮助你解决它.
第二个问题比较麻烦,但是并不影响我们的代码.
但是,如果你遇到了数据冲突,它们很有可能来自这个包里的底层代码.
现在可用的P2P包比较少,特别是go语言的包.
总的来说,go-libp2p还是不错的,也适合我们的目标.
安装
最好的办法是克隆整个包然后直接在里面编写代码.
你可以在环境外开发但是你需要知道如何使用gx.
go get -d github.com/libp2p/go-libp2p/...
打开目录
make
make deps
这能帮助你获取到所有的包和依赖.
我们准备在examples子目录中进行开发.
所以我们可以在examples目录下建一个p2p文件夹
mkdir ./examples/p2p
然后我们打开p2p文件夹,创建一个main.go文件
我们所有的代码都在这个文件中编写.
// 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作为每个block的数据点.
Blockchain是我们的区块链状态,是一个Block切片.
我们声明了mutex用来控制和防止冲突的情况.
// 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是来检查区块链中的区块的hash是否一致.
calculateHash是用sha256来对数据进行hash运算.
generateBlock是用来创建区块来添加到区块链中.
P2P
Host-主机
首先我们需要编写代码来创建主机.
当一个节点运行我们的go程序,
它应该成为一个主机来让其他节点进行连接.
// 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方法需要3个参数,并返回host和error
listenPort是其他节点需要连接的端口
secio是boolean用来打开和关闭安全数据流.
randSeed是一个可选的命令行标志,让我们提供一个种子去创建一个随机地址.
方法的第一个if语句决定了seed是否被提供了以及是否创建了相应的key.
然后我们会创建公钥和私钥来让主机保持安全.
opts部分来构建让其他节点连接的地址.
!secio部分绕开了加密,但是我们会使用secio,所以这行代码现在不适用.
然后我们创建host,完成其他节点可以连接的地址.
log.Printf的部分是告诉新节点如何连接主机