以太坊源码分析之 P2P网络(一)

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

从这篇开始,我们将会围绕着以太坊和eos的源码对一个区块链平台系统的不同组成展开进一步的源码级分析,之所以选择P2P开始我们的征程,主要有两个原因,一是我们部门目前正在进行内部区块链平台的开发,我本人正好负责了其中p2p及网络部分的研发任务,二是p2p作为区块链底层通信的重要组成部分,对于无论公链还是联盟链都有着极其重要的作用,但是p2p网络和其他组件的耦合度恰恰是最少的,从p2p网络开始,有利于我们熟悉以太坊的代码风格,较少的耦合也使得代码的阅读更加流畅自然。

为了更好更清晰的让大家了解p2p的源码,我本来打算用类图和流程图的方式来介绍,但是从我画出的类图来看,源码很复杂,过多的类图其实并不能很好的诠释代码的细节,因此我计划后面无论介绍哪一部分,可大致分为以下几个部分来说明:

  1. 类型或类说明,这部分单独拎出来介绍主要是因为很庞杂,如果没有一个快速检索的地方,后面介绍代码的时候需要来回查看,降低了效率
  2. 重点环节说明,至少会将主干流程完整的按照代码走一遍,可能涉及很多代码,如果只是说函数名走完流程的话,其实是丧失了很多细节,而且也无法体会作者的创新独到之处
  3. 整体结构分析及总结,这一点与我们分析各个部件来完成整个项目的了解如出一辙,对不同类、不同环节了解清楚之后,会自然而然对整个架构设计有了进一步的了解,由点及面,然后再从宏观角度去审视,可以感受到不一样的风景

我们查看的以太坊源码为c++版本的,关于p2p相关的文件均保存在cpp-ethereum中libp2p目录下,下面我们将会把该目录下重要的类或类型的代码贴到下方,并添加说明,便于大家后期检索。

首先从Common.h文件开始:

using NodeID = h512;  			              // 固定长度hash,h表示哈希,512表示长度

using CapDesc = std::pair<std::string, u256>;         // p2p上层功能描述类型,一个数据对
using CapDescSet = std::set<CapDesc>;                 // 描述集合
using CapDescs = std::vector<CapDesc>;                // 描述列表

enum PacketType                                       // 消息包的类型
{
    HelloPacket = 0,		                      // hello包
    DisconnectPacket,                                 // 断开连接
    PingPacket,                                       // ping
    PongPacket,                                       // pong, ping的回应
    GetPeersPacket,                                   // 获取其他peers的请求
    PeersPacket,                                      // 回复peers
    UserPacket = 0x10                                 // 自定义
};

enum DisconnectReason                                 // 断开连接的原因
{
    DisconnectRequested = 0,                          // 请求断开
    TCPError,                                         // tcp错误
    BadProtocol,                                      // 协议版本不对
    UselessPeer,                                      // 无效peer,一般是没有注册任何响应事件
    TooManyPeers,                                     // 一般是当前节点已经连接了很多peer,超过了限制,断开新连接
    DuplicatePeer,                                    // 重复peer连接,因p2p双向通信,两个peer只需要保持一个连接即可
    IncompatibleProtocol,                             // 协议版本不一致
    NullIdentity,                                   
    ClientQuit,
    UnexpectedIdentity,
    LocalIdentity,
    PingTimeout,
    UserReason = 0x10,
    NoDisconnect = 0xffff
};

// p2p网络中节点的类型,分别是可选和必须,这一不同点在节点连接过程中比较重要
enum class PeerType
{
    Optional,
    Required
};

// PeerSessionInfo是在peer连接成功后建立session的时候创建的,用来记录协商后的信息
// 该数据结构会保存在Session
struct PeerSessionInfo     
{
    NodeID const id;                                 // session另一端的node id 
    std::string const clientVersion;                 // 版本
    std::string const host;                          // 另一端的地址
    unsigned short const port;                       // 另一端的端口
    std::chrono::steady_clock::duration lastPing;    // 最近ping的时间
    std::set<CapDesc> const caps;                    // 另一端的node支持的功能集合
    unsigned socketId; 
    std::map<std::string, std::string> notes;
    unsigned const protocolVersion;  // 协议版本号
};
using PeerSessionInfos = std::vector<PeerSessionInfo>;   

// NodeIPEndpoint主要用来记录节点的ip地址和端口信息(包括tcp端口和udp端口)
class NodeIPEndpoint
{
public:
    enum RLPAppend
    {
        StreamList,
        StreamInline
    };

    NodeIPEndpoint() = default;
    NodeIPEndpoint(bi::address _addr, uint16_t _udp, uint16_t _tcp): address(_addr), udpPort(_udp), tcpPort(_tcp) {}
    NodeIPEndpoint(RLP const& _r) { interpretRLP(_r); } // 从rlp消息中解析出地址和端口
    operator bi::udp::endpoint() const { return bi::udp::endpoint(address, udpPort); }
    operator bi::tcp::endpoint() const { return bi::tcp::endpoint(address, tcpPort); }  
    operator bool() const { return !address.is_unspecified() && udpPort > 0 && tcpPort > 0; }
    bool isAllowed() const { return NodeIPEndpoint::test_allowLocal ? !address.is_unspecified() : isPublicAddress(address); }
    bool operator==(NodeIPEndpoint const& _cmp) const {
        return address == _cmp.address && udpPort == _cmp.udpPort && tcpPort == _cmp.tcpPort;
    }
    bool operator!=(NodeIPEndpoint const& _cmp) const {
        return !operator==(_cmp);
    }  
    void streamRLP(RLPStream& _s, RLPAppend _append = StreamList) const;
    void interpretRLP(RLP const& _r);
    bi::address address;
    uint16_t udpPort = 0;
    uint16_t tcpPort = 0;
};

// NodeSpec是记录了node id和地址端口信息,并且可以将着三者拼成字符串enode://pubkey@)host({:port,:tcpport.udpport}
struct NodeSpec
{
    NodeSpec() {}
    /// Accepts user-readable strings of the form (enode://pubkey@)host({:port,:tcpport.udpport})
    NodeSpec(std::string const& _user);
    NodeSpec(std::string const& _addr, uint16_t _port, int _udpPort = -1):
        m_address(_addr),
        m_tcpPort(_port),
        m_udpPort(_udpPort == -1 ? _port : (uint16_t)_udpPort)
    {}
    NodeID id() const { return m_id; }
    NodeIPEndpoint nodeIPEndpoint() const;
    std::string enode() const;  //编码成字符串
private:
    std::string m_address;
    uint16_t m_tcpPort = 0;
    uint16_t m_udpPort = 0;
    NodeID m_id;
};

// Node表示的是节点的基本类型,可以从NodeSpec或者是NodeIPEndpoint中获取得到
class Node
{
public:
    Node() = default;
    virtual ~Node() = default;
    Node(Node const&);
    //id 就是PublicKey,这个很重要,要牢记
    Node(Public _publicKey, NodeIPEndpoint const& _ip, PeerType _peerType = PeerType::Optional): id(_publicKey), endpoint(_ip), peerType(_peerType) {}
    Node(NodeSpec const& _s, PeerType _peerType = PeerType::Optional);
    virtual NodeID const& address() const { return id; }
    virtual Public const& publicKey() const { return id; }  
    virtual operator bool() const { return (bool)id; }
    NodeID id;
    NodeIPEndpoint endpoint;
    std::atomic<PeerType> peerType{PeerType::Optional};  //默认可选};

Host.h

struct NodeInfo  //承载着node的信息,其实这里面我觉得很奇怪,实在是和上面的太重复了,是不是可以统一一下呢?
{
	NodeInfo() = default;
	NodeInfo(NodeID const& _id, std::string const& _address, unsigned _port, std::string const& _version):
		id(_id), address(_address), port(_port), version(_version) {}
	std::string enode() const { return "enode://" + id.hex() + "@" + address + ":" + toString(port); }
	NodeID id;
	std::string address;
	unsigned port;
	std::string version;
};

Network.h

struct NetworkPreferences  //网络参数结构体,记录下节点的网络参数信息
{
	// Default Network Preferences
	NetworkPreferences(unsigned short lp = c_defaultListenPort): listenPort(lp) {}
	// Network Preferences with specific Listen IP
	NetworkPreferences(std::string const& l, unsigned short lp = c_defaultListenPort, bool u = true): publicIPAddress(), listenIPAddress(l), listenPort(lp), traverseNAT(u) {}
	// Network Preferences with intended Public IP
	NetworkPreferences(std::string const& publicIP, std::string const& l = std::string(), unsigned short lp = c_defaultListenPort, bool u = true): publicIPAddress(publicIP), listenIPAddress(l), listenPort(lp), traverseNAT(u) { if (!publicIPAddress.empty() && !isPublicAddress(publicIPAddress)) BOOST_THROW_EXCEPTION(InvalidPublicIPAddress()); }
	/// Addressing
	std::string publicIPAddress;   // 公网ip地址
	std::string listenIPAddress;   // 监听的ip地址
	unsigned short listenPort = c_defaultListenPort;   //监听端口,默认为30303
	/// Preferences
	bool traverseNAT = true;        // 是否穿透nat
	bool discovery = true;		// 是否主动进行节点发现
	bool pin = false;	        // Only accept or connect to trusted peers. 只接受或者连接可信peer
};

class Network //静态网络库,主要是处理一些网络的静态操作和接口
{
public:
	/// 获取绑定着这个节点的地址,包括公网和内网地址
	static std::set<bi::address> getInterfaceAddresses();
	/// 尝试绑定并监听_listenPort指定端口,否则尝试自己分配
	static int tcp4Listen(bi::tcp::acceptor& _acceptor, NetworkPreferences const& _netPrefs);
	/// 返回upnp接口的公网地址. If successful o_upnpifaddr will be a private interface address and endpoint will contain public address and port.
	static bi::tcp::endpoint traverseNAT(std::set<bi::address> const& _ifAddresses, unsigned short _listenPort, bi::address& o_upnpInterfaceAddr);
	/// 解析"host:port"字符串成为一个TCP端点 
	static bi::tcp::endpoint resolveHost(std::string const& _host);
};

NodeTable.h

struct NodeEntry: public Node  //这里又一个跟node有关的,只不过这里有个distance,这是用在节点发现里面的,后面会说明
{
    NodeEntry(NodeID const& _src, Public const& _pubk, NodeIPEndpoint const& _gw);
    int const distance;	///< Node's distance (xor of _src as integer).node的距离
    bool pending = true;		///< Node will be ignored until Pong is received,除非收到pong,否则会忽略
};

enum NodeTableEventType  //节点table支持的事件,分别是添加节点和丢弃节点
{
    NodeEntryAdded,
    NodeEntryDropped
};

Peer.h

class Peer: public Node  //又一个继承节点的,不过Peer和Node是有区别的,Node只是表征单个节点的信息,而Peer这个概念实际上表明两个节点之间的连接,只不过其中一个节点是host
{
	friend class Session;		/// Allows Session to update score and rating.
	friend class Host;		/// For Host: saveNetwork(), restoreNetwork()
	friend class RLPXHandshake;
public:
	/// Construct Peer from Node.
	Peer(Node const& _node): Node(_node) {}
	Peer(Peer const&);	
	bool isOffline() const { return !m_session.lock(); }    //根据session是否还存在判断是否掉线
	virtual bool operator<(Peer const& _p) const;
	/// WIP: Returns current peer rating.
	int rating() const { return m_rating; }
	/// Return true if connection attempt should be made to this peer or false if
	bool shouldReconnect() const;
	/// Number of times connection has been attempted to peer.
	int failedAttempts() const { return m_failedAttempts; }
	/// Reason peer was previously disconnected.
	DisconnectReason lastDisconnect() const { return m_lastDisconnect; }	
	/// Peer session is noted as useful.
	void noteSessionGood() { m_failedAttempts = 0; }	
protected:
	/// Returns number of seconds to wait until attempting connection, based on attempted connection history.
	unsigned fallbackSeconds() const;
	std::atomic<int> m_score{0};									///< All time cumulative.
	std::atomic<int> m_rating{0};									///< Trending.	
	/// Network Availability
	
	std::chrono::system_clock::time_point m_lastConnected;
	std::chrono::system_clock::time_point m_lastAttempted;
	std::atomic<unsigned> m_failedAttempts{0};
	DisconnectReason m_lastDisconnect = NoDisconnect;	///< Reason for disconnect that happened last.

	/// Used by isOffline() and (todo) for peer to emit session information.
	std::weak_ptr<Session> m_session;
};
using Peers = std::vector<Peer>;
上面的这些类型定义一般只包括成员变量的定义,不涉及复杂的逻辑,主要是在节点和网络这方面,这是因为复杂的逻辑介绍背后包含了业务细节或者底层细节,不适合在这篇文章中展示,而是后面通过流程介绍时说明。从上面可以看到,关于节点这块,感觉以太坊的代码过于复杂了,是否可以更加简单明白一点呢?我在阅读源码的过程中,刚开始的时候被Node,NodeEntry等等搞晕过好几次,希望大家看后面文章的时候可以随时查看这篇文章来了解不同定义的区别。





猜你喜欢

转载自blog.csdn.net/fusan2004/article/details/80903365