3

以太坊的peer to peer (go-ethereum/p2p)模块能够让你便捷地在p2p网络上开发任何应用。这个p2p 包采用现代化的模块设计,能够很容易地在其之上扩展自己的额外通信协议。

开始一个p2p服务需要你先从构造一个p2p.Server{}实例开始:

import "github.com/ethereum/go-ethereum/crypto"
import "github.com/ethereum/go-ethereum/p2p"

nodekey, _ := crypto.GenerateKey()
srv := p2p.Server{
    MaxPeers:   10,
    PrivateKey: nodekey,
    Name:       "my node name",
    ListenAddr: ":30300",
    Protocols:  []p2p.Protocol{},
}
srv.Start()

如果你想要在该网络上扩展自己的协议,你需要在p2p.Protocol{}中传入一个子协议:

func MyProtocol() p2p.Protocol {
    return p2p.Protocol{ // 1.
        Name:    "MyProtocol",                                                    // 2.
        Version: 1,                                                               // 3.
        Length:  1,                                                               // 4.
        Run:     func(peer *p2p.Peer, ws p2p.MsgReadWriter) error { return nil }, // 5.
    }
}
  1. 一个子协议对象被称为 Protocol{} 每次一个能够处理该协议的Peer 发起连接时会用到该对象;
  2. 这个服务在网络上发布的名称;
  3. 这个协议的版本;
  4. 这个协议需要依赖的信息数目,因为p2p网络是可扩展的,因此其需要具有能够发送随意个数的信息的能力(需要携带type,在下文中我们能够看到说明),p2p的handler需要知道应该预留多少空间以用来服务你的协议。这是也是共识信息能够通过message ID到达各个peer并实现协商的保障。我们的协议仅仅支持一个message(详见下文);
  5. 在你的协议主要的handler中,我们现在故意将其留空。这个peer变量是指代连接到当前节点,其携带了一些peer本身的信息。其ws变量是reader和writer允许你同该peer进行通信,如果信息能够发送到当前节点,则反之也能够从本节点发送到对端peer节点。

现在让我们将前面留空的handler代码实现,以让它能够同别的peer通信:

const messageId = 0   // 1.
type Message string   // 2.

func msgHandler(peer *p2p.Peer, ws p2p.MsgReadWriter) error {
    for {
        msg, err := ws.ReadMsg()   // 3.
        if err != nil {            // 4.
            return err // if reading fails return err which will disconnect the peer.
        }

        var myMessage [1]Message
        err = msg.Decode(&myMessage) // 5.
        if err != nil {
            // handle decode error
            continue
        }
        
        switch myMessage[0] {
        case "foo":
            err := p2p.SendItems(ws, messageId, "bar")  // 6.
            if err != nil {
                return err // return (and disconnect) error if writing fails.
            }
         default:
             fmt.Println("recv:", myMessage)
         }
    }

    return nil
}
  1. 其中有且唯一的已知信息ID;
  2. 将Messages alias 为string类型;
  3. ReadMsg将一直阻塞等待,直到其收到了一条新的信息,一个错误或者EOF
  4. 如果在读取流信息的过程当中收到了一个错误,最好的解决实践是将其返回给p2p server进行处理。这种错误通常是对端节点已经断开连接;
  5. msg包括两个属性和一个decode方法

    1. Code 包括了信息ID,Code == messageId (i.e.0)
    2. Payload 是信息的内容
    3. Decode(<ptr>) 是一个工具方法:取得 msg.Payload并将其解码,并将其内容设置到传入的message指针中,如果失败了则返回一个error
  6. 如果解码出来的信息是foo将发回一个NewMessage并用messageId标记信息类型,信息内容是bar;而bar信息在被对端收到之后将被defaultcase捕获。

现在,我们将上述的所有部分整合起来,得到下面的p2p样例代码:

package main

import (
    "fmt"
    "os"

    "github.com/ethereum/go-ethereum/crypto"
    "github.com/ethereum/go-ethereum/p2p"
)

const messageId = 0

type Message string

func MyProtocol() p2p.Protocol {
    return p2p.Protocol{
        Name:    "MyProtocol",
        Version: 1,
        Length:  1,
        Run:     msgHandler,
    }
}

func main() {
    nodekey, _ := crypto.GenerateKey()
    srv := p2p.Server{
        MaxPeers:   10,
        PrivateKey: nodekey,
        Name:       "my node name",
        ListenAddr: ":30300",
        Protocols:  []p2p.Protocol{MyProtocol()},
    }

    if err := srv.Start(); err != nil {
        fmt.Println(err)
        os.Exit(1)
    }

    select {}
}

func msgHandler(peer *p2p.Peer, ws p2p.MsgReadWriter) error {
    for {
        msg, err := ws.ReadMsg()
        if err != nil {
            return err
        }

        var myMessage Message
        err = msg.Decode(&myMessage)
        if err != nil {
            // handle decode error
            continue
        }

        switch myMessage {
        case "foo":
            err := p2p.SendItems(ws, messageId, "bar"))
            if err != nil {
                return err
            }
        default:
            fmt.Println("recv:", myMessage)
        }
    }

    return nil
}

原文: Peer to Peer

未经允许不得转载。


terasum
453 声望51 粉丝

Blockchain从业者,Go, JavaScript, Haskell爱好者,函数式编程,高性能并发。