以太坊源码分析之 P2P网络(三、UDP底层通信)

区块链特辑 :https://blog.csdn.net/fusan2004/article/details/80879343,欢迎查阅,原创作品,转载请标明!

这周工作有点小忙,部门区块链基础平台的开发开始进入节奏了,和上一篇间隔间隔有点久了,以后还是要坚持,不能刚开始就犯毛病了。上篇讲的是以太坊p2p网络的一个重点部分——节点发现,在介绍的时候提过,节点发现是通过udp的方式来进行的,这一篇就介绍下udp通信的详细细节,这部分不是很多,算是个过渡吧。

回头再看下NodeTable类,可以看到这个类继承了UDPSocketEvents类,也就是这里和udp通信建立了关联,下面我们先看下这个类都干了些啥。。。

struct UDPSocketEvents
{
    virtual ~UDPSocketEvents() = default;
    virtual void onDisconnected(UDPSocketFace*) {}
    virtual void onReceived(UDPSocketFace*, bi::udp::endpoint const& _from, bytesConstRef _packetData) = 0;
};

可以看到,这个类只提供了两个api接口,分别是onDisconnected和onReceived,其中onDisconnected有一个定义,空的函数体,在看代码的时候也能发现这个函数目前的代码并没有起到什么作用,可暂时忽略;另外的onReceived是一个纯虚函数,也就意味着NodeTable必须要实现这个函数,再回头去看NodeTable类中关于这个函数的详细编码,可以看到这是整个NodeTable更新发现的重要起点,也就是在这里解析了前面所描述的ping\pong\findnode\neighbours这四类消息,具体这四类消息,我们后面再细说,现在继续说说UDPSocketEvents是如何跟udp关联上的,看看NodeTable的构造函数实现。。。

NodeTable::NodeTable(ba::io_service& _io, KeyPair const& _alias, NodeIPEndpoint const& _endpoint, bool _enabled):
    m_node(Node(_alias.pub(), _endpoint)),
    m_secret(_alias.secret()),
    m_socket(make_shared(_io, *reinterpret_cast(this), (bi::udp::endpoint)m_node.endpoint)),
    m_socketPointer(m_socket.get()),
    m_timers(_io)
{
    for (unsigned i = 0; i < s_bins; i++)
        m_state[i].distance = i;
    
    if (!_enabled)
        return;
    
    try
    {
        m_socketPointer->connect(); //开启连接,这时候就可以接受外界发来的消息了,m_socketPointer指定了回调句柄就是NodeTable
        doDiscovery();  //节点发现
    }
    catch (std::exception const& _e)
    {
        cwarn << "Exception connecting NodeTable socket: " << _e.what();
        cwarn << "Discovery disabled.";
    }
}

可以看到m_socket就是一个NodeSocket的实例,这个NodeSocket的构造函数中需要this指针传递,后面都将通过这个指针完成onReceived的回调,逻辑很清晰啦,继续看下NodeSocket吧。。。

template 
class UDPSocket: UDPSocketFace, public std::enable_shared_from_this>
{
public:
    enum { maxDatagramSize = MaxDatagramSize };
    static_assert((unsigned)maxDatagramSize < 65507u, "UDP datagrams cannot be larger than 65507 bytes");
    /// Create socket for specific endpoint.
    //为指定的endpoint创建socket
    UDPSocket(ba::io_service& _io, UDPSocketEvents& _host, bi::udp::endpoint _endpoint): m_host(_host), m_endpoint(_endpoint), m_socket(_io) { m_started.store(false); m_closed.store(true); };
    /// Create socket which listens to all ports.
    UDPSocket(ba::io_service& _io, UDPSocketEvents& _host, unsigned _port): m_host(_host), m_endpoint(bi::udp::v4(), _port), m_socket(_io) { m_started.store(false); m_closed.store(true); };
    virtual ~UDPSocket() { disconnect(); }
    /// Socket will begin listening for and delivering packets
    // 开始监听并传送数据包
    void connect();
    /// Send datagram. 发送数据报
    bool send(UDPDatagram const& _datagram);
    /// Returns if socket is open.
    bool isOpen() { return !m_closed; }
    /// Disconnect socket.
    void disconnect() { disconnectWithError(boost::asio::error::connection_reset); }
protected:
    void doRead(); //进行读操作
    void doWrite(); //进行写操作
    void disconnectWithError(boost::system::error_code _ec); //断开
    std::atomic m_started; ///< Atomically ensure connection is started once. Start cannot occur unless m_started is false. Managed by start and disconnectWithError.
    std::atomic m_closed;	 ///< Connection availability.
    UDPSocketEvents& m_host;	///< Interface which owns this socket.也就是NodeTable
    bi::udp::endpoint m_endpoint; ///< Endpoint which we listen to. 没有监听一说,其实就是一直从这个endpoint上读数据
    Mutex x_sendQ;
    std::deque m_sendQ;		        ///< Queue for egress data. 发送数据的队列
    std::array m_recvData;	///< Buffer for ingress data. 接受数据的队列
    bi::udp::endpoint m_recvEndpoint;		        ///< Endpoint data was received from. 接受数据的来源
    bi::udp::socket m_socket;				///< Boost asio udp socket.
    Mutex x_socketError;				///< Mutex for error which can be set from host or IO thread.
    boost::system::error_code m_socketError;		///< Set when shut down due to error.
};

这里面直接看函数其实没啥意思,我们还是从流程上来观摩这些代码,在上面的构造函数中,当socket创建完毕之后,紧接着就调用了connect函数,这个函数里就是定义了可以从这个udp socket进行异步读事件,详细可以看下这个函数的代码。。。

template 
void UDPSocket::connect()
{
    bool expect = false;
    if (!m_started.compare_exchange_strong(expect, true))
        return;
    m_socket.open(bi::udp::v4());
    try
    {
        m_socket.bind(m_endpoint); //绑定本地地址和端口
    }
    catch (...)
    {
        m_socket.bind(bi::udp::endpoint(bi::udp::v4(), m_endpoint.port()));
    }
    //因为connect只会调用一次,为了不发送旧消息,需要清理写数据的队列
    Guard l(x_sendQ);
    m_sendQ.clear();
    m_closed = false;
    doRead();  //上面只是绑定了端点信息,这里面真正定义了异步读事件
}

可以看到,这个函数目前还没有实质性的,继续看下doRead函数。。。

template 
void UDPSocket::doRead()
{
    if (m_closed)
        return;
    auto self(UDPSocket::shared_from_this());
    //异步读读事件,这个函数是说,如果有数据来了,请把数据放到m_recvData中,m_recvEndpoint会记录数据来源的端点信息,读取完成后回调函数
    m_socket.async_receive_from(boost::asio::buffer(m_recvData), m_recvEndpoint, [this, self](boost::system::error_code _ec, size_t _len)
    {
        if (m_closed)
            return disconnectWithError(_ec);      
        if (_ec != boost::system::errc::success)
            cnetlog << "Receiving UDP message failed. " << _ec.value() << " : " << _ec.message();
        if (_len)  //每次上传给上层,由上层处理响应数据
            m_host.onReceived(this, m_recvEndpoint, bytesConstRef(m_recvData.data(), _len));
        doRead();
    });
}

终于看到了onReceived了,可以看到如果有数据过来了,只要读取正常了,udp是不负责数据完整性的,直接会把数据和读取到的长度信息抛给了上层,也就是NodeTable来处理,这也是为啥NodeTable的onReceived的代码里有很大一部分是来处理数据校验的,上层处理完毕之后,继续doRead来进行下一次的数据请求的监听,这个循环就介绍完了;有读就有写呀,写操作都是上层调用了send函数来触发的,看下send函数。。。

template 
bool UDPSocket::send(UDPDatagram const& _datagram)
{
    if (m_closed)
        return false;
    Guard l(x_sendQ);
    m_sendQ.push_back(_datagram);  //先放到队列中去
    if (m_sendQ.size() == 1) //一旦有了,立马进行doWrite
        doWrite();
    return true;
}

大家可能会奇怪,为啥是队列大小为1的时候才进行doWrite,这里解释下,可以看到在队列进行push的时候是有锁的,也就是说队列push肯定是串行的,但是doWrite立马待会可以看到,是一个努力的小伙子,不把队列立马的全清空不罢休,所以也就是说只有队列里有数据,doWrite是一直被调用的,只有这个socket闲下来了,突然来了一个数据报,才需要重新调用doWrite,好吧,来看看勤奋的小伙子怎么做的。。。

template 
void UDPSocket::doWrite()
{
    if (m_closed)
        return;
    const UDPDatagram& datagram = m_sendQ[0]; //取出队列中第一个数据,而且第一个数据肯定存在,由前面保证的
    auto self(UDPSocket::shared_from_this());
    bi::udp::endpoint endpoint(datagram.endpoint()); //数据报中包含了目标地址
    m_socket.async_send_to(boost::asio::buffer(datagram.data), endpoint, [this, self, endpoint](boost::system::error_code _ec, std::size_t)
    {
        if (m_closed) //关闭了
            return disconnectWithError(_ec);        
        if (_ec != boost::system::errc::success)
            cnetlog << "Failed delivering UDP message. " << _ec.value() << " : " << _ec.message();
        Guard l(x_sendQ);
        m_sendQ.pop_front();  //发送成功,将队列头数据剔除
        if (m_sendQ.empty())  //勤奋的小伙子完成了工作,退出
            return;
        doWrite(); //没有完成,继续写
    });
}

好了,这就是udp连接、接受、发送的全部流程啦,其实也就是udp的几个方法的使用,很简单。前面提到了,节点发现包含了4中消息类型,下面介绍下这些消息的格式,首先这4个消息都有一些共同的父类,慢慢来看,先看下最最上层的UDPDatagram类。。。

class UDPDatagram
{
public:
    UDPDatagram(bi::udp::endpoint const& _ep): locus(_ep) {}
    UDPDatagram(bi::udp::endpoint const& _ep, bytes _data): data(_data), locus(_ep) {}
    bi::udp::endpoint const& endpoint() const { return locus; }
    bytes data;  //携带的消息体
protected:
    bi::udp::endpoint locus;  //待发送的目的地址
};

很简单的,不需要多说啥,继续。。。

struct RLPXDatagramFace: public UDPDatagram
{
    static uint32_t futureFromEpoch(std::chrono::seconds _sec) { return static_cast(std::chrono::duration_cast((std::chrono::system_clock::now() + _sec).time_since_epoch()).count()); }
    static uint32_t secondsSinceEpoch() { return static_cast(std::chrono::duration_cast((std::chrono::system_clock::now()).time_since_epoch()).count()); }
    static Public authenticate(bytesConstRef _sig, bytesConstRef _rlp);

    RLPXDatagramFace(bi::udp::endpoint const& _ep): UDPDatagram(_ep) {}
    virtual ~RLPXDatagramFace() = default;

    virtual h256 sign(Secret const& _from);
    virtual uint8_t packetType() const = 0;

    virtual void streamRLP(RLPStream&) const = 0;
    virtual void interpretRLP(bytesConstRef _bytes) = 0;
};

这个类是个纯虚类,也就是说没干啥具体的事情,只不过从名字我们可以看出来,udp通信的数据采用了rlp来进行了编解码,streamRLP就是rlp编码,interpretRLP就是rlp解码,继续。。。

struct DiscoveryDatagram: public RLPXDatagramFace
{
    /// Constructor used for sending. 这是用来发送数据报时调用的构造函数
    DiscoveryDatagram(bi::udp::endpoint const& _to): RLPXDatagramFace(_to), ts(futureFromEpoch(std::chrono::seconds(60))) {}
    /// Constructor used for parsing inbound packets. 这是接受到数据报时定义的构造函数
    DiscoveryDatagram(bi::udp::endpoint const& _from, NodeID const& _fromid, h256 const& _echo): RLPXDatagramFace(_from), sourceid(_fromid), echo(_echo) {}
    // These two are set for inbound packets only. 下面定义的变量只有在接受的数据报中才会用到
    NodeID sourceid; // sender public key (from signature) 发送方的公钥
    h256 echo;       // hash of encoded packet, for reply tracking 编码过的数据报的哈希值,用来回复响应的跟踪
    // All discovery packets carry a timestamp, which must be greater 所有用来节点发现的数据报都包含了一个时间戳,
    // than the current local time. This prevents replay attacks. 这个时间戳必须大于当前时间,防止重复攻击
    uint32_t ts = 0;
    bool isExpired() const { return secondsSinceEpoch() > ts; } //是否过期,ts里面定义的是发送时刻+60s
    /// Decodes UDP packets. 解码udp数据报
    static std::unique_ptr interpretUDP(bi::udp::endpoint const& _from, bytesConstRef _packet);
};

这里面定义了节点发现消息的直接父类,后面的四个消息类型都是基于这个类定义的,从这个类中我们总结几个事情,一个是发送消息和接受消息的定义是不一样,接受消息需要看下节点id和echo,来知道是什么人发的数据,已经发的是不是我要的,二是定义了过期策略,这个也就是在udp实现中需要考虑,因为udp是无连接的,必须要自己控制,接下来正经看看那四个消息吧。。。

/**
 * Ping packet: Sent to check if node is alive.
 * PingNode is cached and regenerated after ts + t, where t is timeout.
 * ping数据报,用于检测node是否是活的,被缓存,过期后重新生成
 * Ping is used to implement evict. When a new node is seen for / ping用来完成淘汰,一个给定bucket的新node到来时如果满了的话
 * a given bucket which is full, the least-responsive node is pinged.  // 最少回复的node会被ping
 * If the pinged node doesn't respond, then it is removed and the new
 * node is inserted. 如果被ping的node没有被回复,那么将会被移除,新节点会被插入
 */
struct PingNode: DiscoveryDatagram
{
    using DiscoveryDatagram::DiscoveryDatagram;
    PingNode(NodeIPEndpoint const& _src, NodeIPEndpoint const& _dest): DiscoveryDatagram(_dest), source(_src), destination(_dest) {}
    PingNode(bi::udp::endpoint const& _from, NodeID const& _fromid, h256 const& _echo): DiscoveryDatagram(_from, _fromid, _echo) {}
    static const uint8_t type = 1;
    uint8_t packetType() const { return type; }
    unsigned version = 0;  //版本号
    NodeIPEndpoint source;  //源节点
    NodeIPEndpoint destination;  //目的节点

    void streamRLP(RLPStream& _s) const //生成rlp编码数据
    {
        _s.appendList(4);
        _s << dev::p2p::c_protocolVersion;  //协议版本号
        source.streamRLP(_s);  //写入源节点
        destination.streamRLP(_s); //写入目的节点
        _s << ts; //超时时间节点
    }
    void interpretRLP(bytesConstRef _bytes)  //rlp解码
    {
        RLP r(_bytes, RLP::AllowNonCanon|RLP::ThrowOnFail);
        version = r[0].toInt();  //版本好
        source.interpretRLP(r[1]);  //源节点
        destination.interpretRLP(r[2]); //目的节点
        ts = r[3].toInt(); //时间
    }
};

从上面这段代码可以看到ping数据组成形式为:版本|源地址|目的地址|超时时间,继续看下响应的pong数据报结构。。。

/**
 * Pong packet: Sent in response to ping,ping的回复
 */
struct Pong: DiscoveryDatagram
{
    Pong(NodeIPEndpoint const& _dest): DiscoveryDatagram((bi::udp::endpoint)_dest), destination(_dest) {}
    Pong(bi::udp::endpoint const& _from, NodeID const& _fromid, h256 const& _echo): DiscoveryDatagram(_from, _fromid, _echo) {}
    static const uint8_t type = 2;
    uint8_t packetType() const { return type; }
    NodeIPEndpoint destination;  //目的地址
    void streamRLP(RLPStream& _s) const  // rlp编码
    {
        _s.appendList(3);
        destination.streamRLP(_s);  //目的地址
        _s << echo; //ping包的哈希值,貌似后面也没有用到
        _s << ts;  //时间
    }
    void interpretRLP(bytesConstRef _bytes)
    {
        RLP r(_bytes, RLP::AllowNonCanon|RLP::ThrowOnFail);
        destination.interpretRLP(r[0]);
        echo = (h256)r[1];
        ts = r[2].toInt();
    }
};

可以看到pong数据报更简单,只包括了目的地址|echo|过期时间,其实代码里只关系了,这个节点所携带的node_id,继续看findnode的数据报结构。。。

struct FindNode: DiscoveryDatagram
{
    FindNode(bi::udp::endpoint _to, h512 _target): DiscoveryDatagram(_to), target(_target) {}
    FindNode(bi::udp::endpoint const& _from, NodeID const& _fromid, h256 const& _echo): DiscoveryDatagram(_from, _fromid, _echo) {}
    static const uint8_t type = 3;
    uint8_t packetType() const { return type; }
    h512 target;  //待寻找的目标节点
    void streamRLP(RLPStream& _s) const
    {
        _s.appendList(2); _s << target << ts; //更简单了,只有目标节点和超时时间
    }
    void interpretRLP(bytesConstRef _bytes)
    {
        RLP r(_bytes, RLP::AllowNonCanon|RLP::ThrowOnFail);
        target = r[0].toHash();
        ts = r[1].toInt();
    }
};

FindNode的数据结构也很简单,只包括了目标节点id和超时时间,最后看下Neighbours的数据报。。。

struct Neighbours: DiscoveryDatagram
{   //Neighbours数据报,可能包含0个或多个Neighbour节点
    Neighbours(bi::udp::endpoint _to, std::vector> const& _nearest, unsigned _offset = 0, unsigned _limit = 0): DiscoveryDatagram(_to)
    {
        auto limit = _limit ? std::min(_nearest.size(), (size_t)(_offset + _limit)) : _nearest.size();  
        for (auto i = _offset; i < limit; i++)
            neighbours.push_back(Neighbour(*_nearest[i]));
    }
    Neighbours(bi::udp::endpoint const& _to): DiscoveryDatagram(_to) {}  //发送方
    Neighbours(bi::udp::endpoint const& _from, NodeID const& _fromid, h256 const& _echo): DiscoveryDatagram(_from, _fromid, _echo) {}

    struct Neighbour  //定义一个Neighbour结构
    {
        Neighbour(Node const& _node): endpoint(_node.endpoint), node(_node.id) {}  //包含端点信息,节点id
        Neighbour(RLP const& _r): endpoint(_r) { node = h512(_r[3].toBytes()); }
        NodeIPEndpoint endpoint;
        NodeID node;
        void streamRLP(RLPStream& _s) const { _s.appendList(4); endpoint.streamRLP(_s, NodeIPEndpoint::StreamInline); _s << node; }
    };

    static const uint8_t type = 4;
    uint8_t packetType() const { return type; }

    std::vector neighbours;

    void streamRLP(RLPStream& _s) const
    {
        _s.appendList(2);
        _s.appendList(neighbours.size());
        for (auto const& n: neighbours)
            n.streamRLP(_s);
        _s << ts;
    }
    void interpretRLP(bytesConstRef _bytes)
    {
        RLP r(_bytes, RLP::AllowNonCanon|RLP::ThrowOnFail);
        for (auto const& n: r[0])
            neighbours.emplace_back(n);
        ts = r[1].toInt();
    }
};

Neighbours数据报主要包括三部分数据,节点数 | 节点信息 | 超期时间。上述就是节点通信过程的四种数据类型,结构比较简单,就是发送方与接收方对节点的初始化是不一样的,这点看的时候可能比较绕,但是理解清楚了之后就明白了。最后还要介绍一点事情,就是这四个数据报的结构是转化成rlp数据再通过udp发送的,在发送前,会对这些数据包进行了签名的操作,这部分代码是在RLPXDatagramFace中sign函数完成的,这个函数的详细定义是在udp.cpp中,最后看下这个函数来结束这篇博客吧。。。

h256 RLPXDatagramFace::sign(Secret const& _k) //签名算法
{
    assert(packetType());
    
    RLPStream rlpxstream;
//	rlpxstream.appendRaw(toPublic(_k).asBytes()); // for mdc-based signature
    rlpxstream.appendRaw(bytes(1, packetType())); // prefix by 1 byte for type,第一个字节是类型
    streamRLP(rlpxstream); //继续往里面添加数据,得到rlp数据
    bytes rlpxBytes(rlpxstream.out()); //放到rlpxBytes中
    
    bytesConstRef rlpx(&rlpxBytes); 
    h256 sighash(dev::sha3(rlpx)); // H(type||data),计算hash
    Signature sig = dev::sign(_k, sighash); // S(H(type||data)) 对哈希值签名,用_k私钥
    
    data.resize(h256::size + Signature::size + rlpx.size()); //数据报的数据,
    bytesRef rlpxHash(&data[0], h256::size);
    bytesRef rlpxSig(&data[h256::size], Signature::size);
    bytesRef rlpxPayload(&data[h256::size + Signature::size], rlpx.size());
    
    sig.ref().copyTo(rlpxSig);
    rlpx.copyTo(rlpxPayload);
    
    bytesConstRef signedRLPx(&data[h256::size], data.size() - h256::size);
    dev::sha3(signedRLPx).ref().copyTo(rlpxHash);
    // data
    //   h256:: size      Signature::size     rlpx.size()
    // 0 -> h256::size                | h256::size + Signature::size         | hash::size + Signature + rlp.size
    // 对后面的数据又进行了一次哈希的哈希值 | 对类型+数据的rlp的bytes进行哈希后的签名   | 类型+数据的rlp的bytes
    return sighash;
}

你可能感兴趣的:(区块链,开源代码)