以太坊的p2p模块实现了一个p2p分布式网络,是实现以太坊分布式钱包的关键技术。p2p模块的说明见官方github的wiki。本文要实现的是使用以太坊的p2p模块来实现一个简单的聊天程序。
1 P2P基本原理
p2p的基本原理有一篇博客写的很清楚,详见《p2p的原理和常见的实现方式》。
2 编译并启动以太坊的bootnode
bootnode节点可以作为p2p网络的路由节点。聊天程序中的俩个p2p节点将以该bootnode作为路由。
在ubuntu环境下搭建go语言编译环境,去https://github.com/ethereum/go-ethereum下载以太坊源码,
sudo git https://github.com/ethereum/go-ethereum
将下载以太坊源码到当前目录下,下载完成后,当前目录将出现go-ethereum文录。进入该目录,使用sudo make all将在go-ethereum/build/bin目录下生成bootnode可执行文件,将该文件拷贝到一个文件夹下:
sudo cp bootnode ~/p2ptest/
进入p2ptest目录:
生成key:
启动bootnode:
注意,需要将enode字符串中将@后面的[::]改成ubuntu的IP地址。
3 p2p聊天程序
package main import ( "bufio" "fmt" "log" "os" "sync" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p/discover" "gopkg.in/urfave/cli.v1" ) var ( port int bootnode string ) const ( msgTalk = 0 msgLength = iota ) func main() { app := cli.NewApp() app.Usage = "p2p package demo" app.Action = startP2pNode app.Flags = []cli.Flag{ //命令行解析得到的port cli.IntFlag{Name: "port", Value: 11200, Usage: "listen port", Destination: &port}, //命令行解析得到bootnode cli.StringFlag{Name: "bootnode", Value: "", Usage: "boot node", Destination: &bootnode}, } if err := app.Run(os.Args); err != nil { log.Fatal(err) } } func startP2pNode(c *cli.Context) error { emitter := NewEmitter() nodeKey, _ := crypto.GenerateKey() node := p2p.Server{ Config: p2p.Config{ MaxPeers: 100, PrivateKey: nodeKey, Name: "p2pDemo", ListenAddr: fmt.Sprintf(":%d", port), Protocols: []p2p.Protocol{emitter.MyProtocol()}, }, } //从bootnode字符串中解析得到bootNode节点 bootNode, err := discover.ParseNode(bootnode) if err != nil { return err } //p2p服务器从BootstrapNodes中得到相邻节点 node.Config.BootstrapNodes = []*discover.Node{bootNode} //node.Start()开启p2p服务 if err := node.Start(); err != nil { return err } emitter.self = node.NodeInfo().ID[:8] go emitter.talk() select {} return nil } func (e *Emitter) MyProtocol() p2p.Protocol { return p2p.Protocol{ Name: "rad", Version: 1, Length: msgLength, Run: e.msgHandler, } } type peer struct { peer *p2p.Peer ws p2p.MsgReadWriter } type Emitter struct { self string peers map[string]*peer sync.Mutex } func NewEmitter() *Emitter { return &Emitter{peers: make(map[string]*peer)} } func (e *Emitter) addPeer(p *p2p.Peer, ws p2p.MsgReadWriter) { e.Lock() defer e.Unlock() id := fmt.Sprintf("%x", p.ID().String()[:8]) e.peers[id] = &peer{ws: ws, peer: p} } func (e *Emitter) talk() { for { func() { e.Lock() defer e.Unlock() inputReader := bufio.NewReader(os.Stdin) fmt.Println("Please enter some input: ") input, err := inputReader.ReadString('\n') if err == nil { fmt.Printf("The input was: %s\n", input) for _, p := range e.peers { if err := p2p.SendItems(p.ws, msgTalk, input); err != nil { log.Println("Emitter.loopSendMsg p2p.SendItems err", err, "peer id", p.peer.ID()) continue } } } }() } } func (e *Emitter) msgHandler(peer *p2p.Peer, ws p2p.MsgReadWriter) error { e.addPeer(peer, ws) for { msg, err := ws.ReadMsg() if err != nil { return err } switch msg.Code { case msgTalk: var myMessage []string if err := msg.Decode(&myMessage); err != nil { log.Println("decode msg err", err) } else { log.Println("read msg:", myMessage[0]) } default: log.Println("unkown msg code") } } return nil }
4 运行
在windows环境下编译以上程序生成exe可执行文件p2pTest.exe,开启一个cmd客户端,执行:
p2pText.exe --port 3401 --bootnode "第2步中bootnode节点的enode"
开启第二个cmd客户端,执行:
p2pText.exe --port 3402 --bootnode "第2步中bootnode节点的enode"
可以聊天了:
cmd1:
cmd2:
刚开始连接过程有点慢,等了一小会俩个客户端聊天互相才有反应。