btcd的P2P通信区块链研习社金马带你定投区块链

Btcd区块在P2P网络上的传播(引子)

2018-03-06  本文已影响403人  oceanken

前面的系列文章中我们介绍了Bitcoin网络中节点对区块的存取机制,本文开始我们将介绍Btcd节点如何组成P2P网络以及区块如何在P2P网络中传播。区块在网络上的传递过程涉及节点之间的连接管理、地址管理、Peer节点的管理和Peer之间同步区块的协议等问题,与之相关的代码在如下几个包或者文件中:

这些模块之间的关系如下图所示:

接下来,我们将逐一阅读和分析各个包中的代码。我们首先来了解P2P网络是如何组网,然后再进一步了解Bitcoin网络协议的实现。btcd/peer是实现Bitcoin P2P网络的核心模块,我们先从它开始介绍。

Clone完btcd/peer的代码后,我们可以发现它包含如下一些文件:

其中主要的类型为Peer、Config和MessageListeners,Peer类型定义了Peer相关的属性和方法,Config类型定义了与Peer相关的配置,MessageListeners定义了响应Peer消息的回调函数。它们定义的成员字段比较多,我们不打算一一介绍,将在分析具体代码时解释其字段的意义。我们先从创建Peer对象的newPeerBase()方法入手来分析Peer:

//btcd/peer/peer.go

// newPeerBase returns a new base bitcoin peer based on the inbound flag.  This
// is used by the NewInboundPeer and NewOutboundPeer functions to perform base
// setup needed by both types of peers.
func newPeerBase(origCfg *Config, inbound bool) *Peer {
    // Default to the max supported protocol version if not specified by the
    // caller.
    cfg := *origCfg // Copy to avoid mutating caller.

    ......

    p := Peer{
        inbound:         inbound,
        knownInventory:  newMruInventoryMap(maxKnownInventory),
        stallControl:    make(chan stallControlMsg, 1), // nonblocking sync
        outputQueue:     make(chan outMsg, outputBufferSize),
        sendQueue:       make(chan outMsg, 1),   // nonblocking sync
        sendDoneQueue:   make(chan struct{}, 1), // nonblocking sync
        outputInvChan:   make(chan *wire.InvVect, outputBufferSize),
        inQuit:          make(chan struct{}),
        queueQuit:       make(chan struct{}),
        outQuit:         make(chan struct{}),
        quit:            make(chan struct{}),
        cfg:             cfg, // Copy so caller can't mutate.
        services:        cfg.Services,
        protocolVersion: cfg.ProtocolVersion,
    }
    return &p
}

从上面创建Peer的代码中可以看出,Peer中的关键字段包括:

上述有些字段与Peer实现的消息收发机制有关系,读者可能会有些疑惑,我们将在下面详细介绍。同时,这里涉及到了Go中的channelgoroutine,它们是golang简化并发编程的关键工具,刚开始接触Go的读者可能觉得不好理解,也会对理解Peer的代码造成一些障碍。由于篇幅原因,本文不深入介绍Go的并发编程,读者可以翻阅相关书籍。为了方便大家理解代码,我们引用golang官方对它的并发机制的总结作一个启示:

Do not communicate by sharing memory; instead, share memory by communicating.

channel就是用来在goroutine之间进行通信的数据类型。接下来,我们来看Peer的start()方法以进一步了解它的实现:

// start begins processing input and output messages.
func (p *Peer) start() error {
    log.Tracef("Starting peer %s", p)

    negotiateErr := make(chan error)
    go func() {
        if p.inbound {
            negotiateErr <- p.negotiateInboundProtocol()
        } else {
            negotiateErr <- p.negotiateOutboundProtocol()
        }
    }()

    // Negotiate the protocol within the specified negotiateTimeout.
    select {
    case err := <-negotiateErr:
        if err != nil {
            return err
        }
    case <-time.After(negotiateTimeout):
        return errors.New("protocol negotiation timeout")
    }
    log.Debugf("Connected to %s", p.Addr())

    // The protocol has been negotiated successfully so start processing input
    // and output messages.
    go p.stallHandler()
    go p.inHandler()
    go p.queueHandler()
    go p.outHandler()
    go p.pingHandler()

    // Send our verack message now that the IO processing machinery has started.
    p.QueueMessage(wire.NewMsgVerAck(), nil)
    return nil
}

上述代码主要包含:

  1. 起了一个goroutine来与Peer交换Version消息,调用goroutine与新的goroutine通过negotiateErr channel同步,调用goroutine阻塞等待Version握手完成;
  2. 如果Version握手失败或者超时,则返回错误,Peer关系建立失败;
  3. 如果握手成功,则启动5个新的goroutine来收发消息。其中,stallHandler()用于处理消息超时,inHandler()用于接收Peer消息,queueHandler()用于维护消息发送列队,outHandler用于向Peer发送消息,pingHandler()用于向Peer周期性地发送心跳;
  4. 最后,向Peer发送verack消息,双方完成握手。

Peer start()成功后,节点间的Peer关系便成功建立,可以进一步交换其他协议消息了,如果节点与不同的其他节点建立了Peer关系,其他节点又与新的节点建立Peer关系,所有节点会逐渐形成一张P2P网络。然而,节点在交换Version时从对方获取了哪些信息,为什么要等到Version交换完成后Peer关系才能正常建立?各个Handler又是如何实现收发消息的呢?我们将在下一篇文章《Btcd区块在P2P网络上的传播之Peer》中详细介绍。

上一篇下一篇

猜你喜欢

热点阅读