以太坊源码分析之 P2P网络(二、节点发现流程)

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

上一篇文章简单介绍了下一些基础的类型定义,从这一篇开始我们将描述p2p网络的更多细节。从关于节点的定义来看,其实不同定义是有不同含义的,Node代表的是一个孤立的节点,这个节点不代表我们和他会建立连接,而Peer是肯定会去连接的,但是不代表一定会建立出连接,只有建立连接以后才会生成session,在session上才进行了以太坊的数据的交换。

对于了解p2p系统的人来说,肯定对区块链p2p底层有一种疑惑,为什么呢?因为在中心化的p2p网络中,会有一个server用来搜集peer信息,这样在数据交互过程中,每个peer一般情况下是先通过这个server拿到一定数量的peer列表,然后挨个去建立连接,最后进行数据交互。但众所周知的是,区块链是一个去中心化的系统,这种server的存在将会彻底破坏区块链可信任的基础,那么以太坊是如何解决节点获取问题的呢?答案就是Kademlia算法,这是一种分布式存储及路由的算法,能够保证经过最多n步后找到需要的数据,具体的算法可以参考 https://www.jianshu.com/p/f2c31e632f1d 这篇文章,比较通俗易懂。

在这里我们更加关注以太坊中关于节点发现的实现,这部分的逻辑都是在NodeTable中,我们先来看下NodeTable类的成员变量和函数,然后再根据代码逻辑详细说明整个流程,后续如果有时间我会再补个流程图。

NodeTable类

// NodeTable类负责以太坊p2p网络底层节点发现的所有管理
// 节点发现是通过udp来完成,因此这里继承了UDPSocketEvents,来响应一些事件
class NodeTable: UDPSocketEvents, public std::enable_shared_from_this
{
    friend std::ostream& operator<<(std::ostream& _out, NodeTable const& _nodeTable);
    using NodeSocket = UDPSocket;              // UDPSocket,这是在UDP.h中定义,1280表示的是最大数据报大小
    using TimePoint = std::chrono::steady_clock::time_point;    // < Steady time point.
    using NodeIdTimePoint = std::pair;
    struct EvictionTimeout                                      // 用于记录淘汰的节点的timepoint,以及用于替代他的新节点id
    { 
        NodeID newNodeID;
        TimePoint evictedTimePoint;
    };

public:
    enum NodeRelation { Unknown = 0, Known };   // 判断节点的关系,在部分函数参数中需要
    enum DiscoverType { Random = 0 }; 
    NodeTable(ba::io_service& _io, KeyPair const& _alias, NodeIPEndpoint const& _endpoint, bool _enabled = true);   //构造函数需要一个用于io的host,证书以及要监听的ip地址和端口
    ~NodeTable();
    //返回两个nodeid基于异或计算的距离,这就是NodeEntry中的distance,也是判断两个节点逻辑上“距离”的计算方法,可不用关注细节
    static int distance(NodeID const& _a, NodeID const& _b) { u256 d = sha3(_a) ^ sha3(_b); unsigned ret; for (ret = 0; d >>= 1; ++ret) {}; return ret; }
    void setEventHandler(NodeTableEventHandler* _handler) { m_nodeEventHandler.reset(_handler); }   //为NodeEntryAdded和NodeEntryDropped事件设置事件句柄,实际上这两个事件都会在上层被处理,这里暂不关注
    void processEvents();                      // 这个函数也是在上层被调用的,这样上层就可以来处理setEventHandler设置的事件了
    std::shared_ptr addNode(Node const& _node, NodeRelation _relation = NodeRelation::Unknown);  //添加节点,这部分内容较多,会在后面流程介绍细说
    std::list nodes() const;           // 返回node table中活跃的node id的列表
    unsigned count() const { return m_nodes.size(); }  // 返回节点数量
    std::list snapshot() const;             //返回节点快照,这里可以发现关注的都是NodeEntry,这是因为node table需要关心distance
    bool haveNode(NodeID const& _id) { Guard l(x_nodes); return m_nodes.count(_id) > 0; }  // 判断节点是否已经存在
    Node node(NodeID const& _id);              // 返回该node id对应的node,如果不存在返回空节点
    // 下面就是Kademlia算法需要配置的一些常量
    static unsigned const s_addressByteSize = h256::size;                   // < Size of address type in bytes. 32位
    static unsigned const s_bits = 8 * s_addressByteSize;                   // < Denoted by n in [Kademlia].256个bit
    static unsigned const s_bins = s_bits - 1;                              // < Size of m_state (excludes root, which is us). 255个槽位
    static unsigned const s_maxSteps = boost::static_log2::value;   // < Max iterations of discovery. (discover), discovery的最大迭代次数,n取log
    // 可选的参数
    static unsigned const s_bucketSize = 16;            // < Denoted by k in [Kademlia]. Number of nodes stored in each bucket. 每一个bucket保存的node数
    static unsigned const s_alpha = 3;                  // < Denoted by \alpha in [Kademlia]. Number of concurrent FindNode requests. findNode请求的并发数
    // 一些定时器间隔
    std::chrono::milliseconds const c_evictionCheckInterval = std::chrono::milliseconds(75);      // 淘汰超时检测的间隔
    std::chrono::milliseconds const c_reqTimeout = std::chrono::milliseconds(300);                // 每个请求的等待时间
    std::chrono::milliseconds const c_bucketRefresh = std::chrono::milliseconds(7200);            // 更新bucket的时间,避免node数据变得老旧
    struct NodeBucket   //槽位,每个不同的distance都会包含若干个节点,最多不超过上面的s_bucketSize,也就是16个
    {
        unsigned distance;
        std::list> nodes;
    };
    void ping(NodeIPEndpoint _to) const;     // ping, 连接某个端点
    void ping(NodeEntry* _n) const;          // 用来ping已知节点,这是node table在更新buckets或者淘汰过程中调用
    NodeEntry center() const { return NodeEntry(m_node.id, m_node.publicKey(), m_node.endpoint); }
    std::shared_ptr nodeEntry(NodeID _id);
    void doDiscover(NodeID _target, unsigned _round = 0, std::shared_ptr>> _tried =  std::shared_ptr>>());    // 用于发现给定目标距离近的节点
    std::vector> nearestNodeEntries(NodeID _target);          //返回距离target最近的节点列表
    void evict(std::shared_ptr _leastSeen, std::shared_ptr _new);  // 异步丢弃不响应的_leastSeen节点,并添加_new节点,否则丢弃_new
    void noteActiveNode(Public const& _pubk, bi::udp::endpoint const& _endpoint);        //为了维持节点table,无论何时从一个节点获取到activity,都会调用这个noteActiveNode
    void dropNode(std::shared_ptr _n);     //当超时出现后,调用
    NodeBucket& bucket_UNSAFE(NodeEntry const* _n);   //这是返回bucket的引用,后面可以看到,这是唯一添加node到bucket的入口
    void onReceived(UDPSocketFace*, bi::udp::endpoint const& _from, bytesConstRef _packet); //当m_socket收到数据包,调用该函数,这是继承的UDPSocketEvents里函数
    void onDisconnected(UDPSocketFace*) {}            //当socket端口后调用,也是继承的UDPSocketEvents里函数
    void doCheckEvictions();                          // 被evict调用确认淘汰检查被调度,并且在没有淘汰剩余时停止,异步操作
    void doDiscovery();                               // 在c_bucketRefresh间隔内查询随机node
    std::unique_ptr m_nodeEventHandler;      // < Event handler for node events. node事件的事件句柄
    Node m_node;                                                    // < This node. LOCK x_state if endpoint access or mutation is required. Do not modify id. 当前自己这个节点
    Secret m_secret;                                                // < This nodes secret key. 当前节点的私钥
    mutable Mutex x_nodes;                                          // < LOCK x_state first if both locks are required. Mutable for thread-safe copy in nodes() const.
    std::unordered_map> m_nodes; // 已知的节点endpoints,m_nodes记录的是建立过连接的node信息
    mutable Mutex x_state;                                          // < LOCK x_state first if both x_nodes and x_state locks are required.
    std::array m_state;                         // p2p节点网络的状态, m_state是记录了不同bucket的节点,不代表就能连上,在noteActiveNode这个函数中添加
    Mutex x_evictions;                                              // < LOCK x_evictions first if both x_nodes and x_evictions locks are required.
    std::unordered_map m_evictions;        // < Eviction timeouts. 
    Mutex x_pubkDiscoverPings;                                    

你可能感兴趣的:(区块链,区块链,p2p,节点发现)