以太坊P2P 流程(1)

(建议先看总体流程梳理 https://blog.csdn.net/laorenmen/article/details/85079678)

以太坊P2P代码分为2部分,一部分是基于UDP的节点发现协议,另一部分是基于TCP的传输协议

NodeTable中贯穿第一部分流程:

本端:doDiscovery ---> 随机选择一个node ---> doDiscover ---> 获得该Node附近的节点 --> 发送FindNode包

对端:收到FindNode包 ---> 获取该Node附近节点 ---> 发送Neighbours包

本端:收到Neighbours包 --> 加入到本地node列表 ---> 发送Ping包

对端:收到Ping包 --> 加入到本地node列表 --->发送Pong包

本端:收到Pong包 --> 加入K桶 --> appendEvent

借用网上的图来说下整个流程

下面仔细分析下NodeTable这个类:

从注释可以先了解下NodeTable的作用

/**
 * NodeTable using modified kademlia for node discovery and preference.
 * Node table requires an IO service, creates a socket for incoming
 * UDP messages and implements a kademlia-like protocol. Node requests and
 * responses are used to build a node table which can be queried to
 * obtain a list of potential nodes to connect to, and, passes events to
 * Host whenever a node is added or removed to/from the table.
 */

NodeTable是改进型的kad算法,用来发现node,基于UDP。下面是NodeTable的具体内容,很多注释写的很清楚,一路了然

class NodeTable : UDPSocketEvents
{
    friend std::ostream& operator<<(std::ostream& _out, NodeTable const& _nodeTable);
    using NodeSocket = UDPSocket<NodeTable, 1280>;  //UDP socket,报文长度1280字节
    using TimePoint = std::chrono::steady_clock::time_point;	///< Steady time point.
    using NodeIdTimePoint = std::pair<NodeID, TimePoint>;

    /**
     * NodeValidation is used to record the timepoint of sent PING,
     * time of sending and the new node ID to replace unresponsive node.
     */
    struct NodeValidation    //用来记录ping老节点的时间信息和可能会被替换的新节点的node id
    {
        TimePoint pingSendTime;
        h256 pingHash;
        boost::optional<NodeID> replacementNodeID;
    };

public:
    enum NodeRelation { Unknown = 0, Known };
    enum DiscoverType { Random = 0 };
    
    /// Constructor requiring host for I/O, credentials, and IP Address and port to listen on.
    NodeTable(ba::io_service& _io, KeyPair const& _alias, NodeIPEndpoint const& _endpoint, bool _enabled = true);
    ~NodeTable();

    /// Returns distance based on xor metric two node ids. Used by NodeEntry and NodeTable.
    // 这里就是Kad算法中逻辑距离的计算,这里计算的是XOR后,最高位为1的bit位数
    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; }

    /// Set event handler for NodeEntryAdded and NodeEntryDropped events.
    //设置event的处理函数,add、drop
    void setEventHandler(NodeTableEventHandler* _handler) { m_nodeEventHandler.reset(_handler); }

    /// Called by implementation which provided handler to process NodeEntryAdded/NodeEntryDropped events. Events are coalesced by type whereby old events are ignored.
    //执行event处理函数
    void processEvents();

    /// Add node. Node will be pinged.
    // 增加节点,这里很重要
    void addNode(Node const& _node, NodeRelation _relation = NodeRelation::Unknown);

    /// Returns list of node ids active in node table.
    std::list<NodeID> nodes() const;

    /// Returns node count.
    unsigned count() const
    {
        Guard l(x_nodes);
        return m_allNodes.size();
    }

    /// Returns snapshot of table.
    std::list<NodeEntry> snapshot() const;

    /// Returns true if node id is in node table.
    bool haveNode(NodeID const& _id)
    {
        Guard l(x_nodes);
        return m_allNodes.count(_id) > 0;
    }

    /// Returns the Node to the corresponding node id or the empty Node if that id is not found.
    Node node(NodeID const& _id);

// protected only for derived classes in tests
protected:
    /// Constants for Kademlia, derived from address space.
    // Kad算法中涉及到的参数:ID的bit位数,这里是256bit
    static constexpr unsigned s_addressByteSize = h256::size;					///< Size of address type in bytes.
    static constexpr unsigned s_bits = 8 * s_addressByteSize;					///< Denoted by n in [Kademlia].
    // K-桶中list的个数,0~255
    static constexpr unsigned s_bins = s_bits - 1;  ///< Size of m_buckets (excludes root, which is us).
    // 最多查找个数是 log2(N),即log2(256)
    static constexpr unsigned s_maxSteps = boost::static_log2<s_bits>::value;	///< Max iterations of discovery. (discover)

    /// Chosen constants
    // Kad算法中的k值,即一个list中最多保存的node个数,16个
    static constexpr unsigned s_bucketSize = 16;			///< Denoted by k in [Kademlia]. Number of nodes stored in each bucket.
    // findNode的时候并行发送的个数,这里是3
    static constexpr unsigned s_alpha = 3;				///< Denoted by \alpha in [Kademlia]. Number of concurrent FindNode requests.

    /// Intervals
    //一些定时器的定时周期:退出节点的等待时间75ms,查询节点等待时间300ms,k-桶刷新时间7200ms
    /// Interval at which eviction timeouts are checked.
    static constexpr std::chrono::milliseconds c_evictionCheckInterval{75};
    /// How long to wait for requests (evict, find iterations).
    static constexpr std::chrono::milliseconds c_reqTimeout{300};
    /// Refresh interval prevents bucket from becoming stale. [Kademlia]
    static constexpr std::chrono::milliseconds c_bucketRefresh{7200};
    // Period during which we consider last PONG results to be valid before sending new PONG
    static constexpr uint32_t c_bondingTimeSeconds{12 * 60 * 60};

    struct NodeBucket //k-桶的结构,每个distance下有多个node,
    {
        unsigned distance;
        std::list<std::weak_ptr<NodeEntry>> nodes;
    };

    /// Used ping known node. Used by node table when refreshing buckets and as part of eviction process (see evict).
    // ping已知的节点,用于k-桶刷新的时候
    void ping(NodeEntry const& _nodeEntry, boost::optional<NodeID> const& _replacementNodeID = {});

    /// Used by asynchronous operations to return NodeEntry which is active and managed by node table.
    std::shared_ptr<NodeEntry> nodeEntry(NodeID _id);

    /// Used to discovery nodes on network which are close to the given target.
    /// Sends s_alpha concurrent requests to nodes nearest to target, for nodes nearest to target, up to s_maxSteps rounds.
    // 用来发现节点,同时发送s_alpha个请求,最多s_maxSteps轮
    void doDiscover(NodeID _target, unsigned _round = 0, std::shared_ptr<std::set<std::shared_ptr<NodeEntry>>> _tried = std::shared_ptr<std::set<std::shared_ptr<NodeEntry>>>());

    /// Returns nodes from node table which are closest to target.
    // 返回距离_target最近的节点列表
    std::vector<std::shared_ptr<NodeEntry>> nearestNodeEntries(NodeID _target);

    /// Asynchronously drops _leastSeen node if it doesn't reply and adds _new node, otherwise _new node is thrown away.
    // 处理最老的节点,如果最老节点没有回应,丢弃掉,并加入最新的节点,否则丢弃最新节点
    void evict(NodeEntry const& _leastSeen, NodeEntry const& _new);

    /// Called whenever activity is received from a node in order to maintain node table.
    void noteActiveNode(Public const& _pubk, bi::udp::endpoint const& _endpoint);

    /// Used to drop node when timeout occurs or when evict() result is to keep previous node.
    // 丢弃节点
    void dropNode(std::shared_ptr<NodeEntry> _n);

    /// Returns references to bucket which corresponds to distance of node id.
    /// @warning Only use the return reference locked x_state mutex.
    // TODO p2p: Remove this method after removing offset-by-one functionality.
    NodeBucket& bucket_UNSAFE(NodeEntry const* _n);

    /// General Network Events

    /// Called by m_socket when packet is received.
    void onPacketReceived(
        UDPSocketFace*, bi::udp::endpoint const& _from, bytesConstRef _packet) override;

    /// Called by m_socket when socket is disconnected.
    void onSocketDisconnected(UDPSocketFace*) override {}

    /// Tasks

    /// Called by evict() to ensure eviction check is scheduled to run and terminates when no evictions remain. Asynchronous.
    void doCheckEvictions();

    /// Looks up a random node at @c_bucketRefresh interval.
    void doDiscovery();

    void doHandleTimeouts();

    std::unique_ptr<NodeTableEventHandler> m_nodeEventHandler;		///< Event handler for node events.

    NodeID const m_hostNodeID;
    NodeIPEndpoint m_hostNodeEndpoint;
    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<NodeID, std::shared_ptr<NodeEntry>> m_allNodes;  ///< Known Node Endpoints

    mutable Mutex x_state;											///< LOCK x_state first if both x_nodes and x_state locks are required.
    // K-桶,即 NodeBucket[s_bins] m_buckets
    std::array<NodeBucket, s_bins> m_buckets;                       ///< State of p2p node network.
    
    Mutex x_findNodeTimeout;
    std::list<NodeIdTimePoint> m_findNodeTimeout;					///< Timeouts for FindNode requests.

    std::shared_ptr<NodeSocket> m_socket;							///< Shared pointer for our UDPSocket; ASIO requires shared_ptr.

    // The info about PING packets we've sent to other nodes and haven't received PONG yet
    std::unordered_map<NodeID, NodeValidation> m_sentPings;

    mutable Logger m_logger{createLogger(VerbosityDebug, "discov")};

    DeadlineOps m_timers; ///< this should be the last member - it must be destroyed first
};

上面说了是实现了Kad算法,下面汇总下简化的处理代码

1. 定义K-桶结构
struct NodeBucket
{
   unsigned distance;
   std::list<std::weak_ptr<NodeEntry>> nodes;
};

2. 定义K-桶
std::array<NodeBucket, s_bins> m_buckets;

3. 更新K-桶
    3.1 根据与新节点的distance 找到该distance下的已有的node list
    
    odeBucket& s = bucket_UNSAFE(newNode.get());
    auto& nodes = s.nodes;
    
    3.2 再已知的node list中查找newNode, 

    // check if the node is already in the bucket
    auto it = std::find(nodes.begin(), nodes.end(), newNode);
         3.2.1 如果已经在node list中,则把它移到队尾
            if (it != nodes.end())
            {
                // if it was in the bucket, move it to the last position
                nodes.splice(nodes.end(), nodes, it);
            }
          3.2.2 如果不在node list中,
            else
            {
                3.2.2.1 如果该distance下的node数量不足K个,直接把newNode增加到队尾
                if (nodes.size() < s_bucketSize)
                {
                    // if it was not there, just add it as a most recently seen node
                    // (i.e. to the end of the list)
                    nodes.push_back(newNode);
                    if (m_nodeEventHandler)
                        m_nodeEventHandler->appendEvent(newNode->id, NodeEntryAdded);
                }
                3.2.2.2 如果满了16个,对node list中对头,即最老节点进行退出检查操作
                else
                {
                    // if bucket is full, start eviction process for the least recently seen node
                    nodeToEvict = nodes.front().lock();
                    // It could have been replaced in addNode(), then weak_ptr is expired.
                    // If so, just add a new one instead of expired
                    if (!nodeToEvict) //因为有个addNode流程也会更新node
                    {
                        nodes.pop_front();
                        nodes.push_back(newNode);
                        if (m_nodeEventHandler)
                            m_nodeEventHandler->appendEvent(newNode->id, NodeEntryAdded);
                    }
                }
            }

下面详细看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<NodeSocket>(_io, *reinterpret_cast<UDPSocketEvents*>(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();//调用UDP的接口,可以接受别的节点发过来的链接请求了
        doDiscovery();//发现节点
    }
    catch (std::exception const& _e)
    {
        cwarn << "Exception connecting NodeTable socket: " << _e.what();
        cwarn << "Discovery disabled.";
    }
}

connect函数 socket绑定本地端口 端口号默认30303,并开始接收外面的数据包

doDiscovery() 很重要,看后续代码

 

void NodeTable::doDiscovery()
{
    //定时器,7200ms,刷新一次bucket
    m_timers.schedule(c_bucketRefresh.count(), [this](boost::system::error_code const& _ec)
    {
        if (_ec)
            // we can't use m_logger here, because captured this might be already destroyed
            clog(VerbosityDebug, "discov")
                << "Discovery timer was probably cancelled: " << _ec.value() << " "
                << _ec.message();

        if (_ec.value() == boost::asio::error::operation_aborted || m_timers.isStopped())
            return;

        LOG(m_logger) << "performing random discovery";
        NodeID randNodeId;//随机选择一个nodeid,进行发现
        crypto::Nonce::get().ref().copyTo(randNodeId.ref().cropped(0, h256::size));
        crypto::Nonce::get().ref().copyTo(randNodeId.ref().cropped(h256::size, h256::size));
        doDiscover(randNodeId);
    });
}

启动定时器 每个7200ms刷新一下bucket,用lambda函数执行,最下面随机选择了一个节点,然后调用doDiscover函数

void NodeTable::doDiscover(NodeID _node, unsigned _round, shared_ptr<set<shared_ptr<NodeEntry>>> _tried)
{
    // NOTE: ONLY called by doDiscovery!

    if (!m_socketPointer->isOpen())
        return;

    if (_round == s_maxSteps)
    {
        LOG(m_logger) << "Terminating discover after " << _round << " rounds.";
        doDiscovery(); //又注册刷新bucket的定时器
        return;
    }
    else if (!_round && !_tried)
        // initialized _tried on first round
        _tried = make_shared<set<shared_ptr<NodeEntry>>>();

    auto nearest = nearestNodeEntries(_node);//找到最近的节点
    list<shared_ptr<NodeEntry>> tried;//记录每次尝试获取的节点list
    for (unsigned i = 0; i < nearest.size() && tried.size() < s_alpha; i++) //每次请求数目为s_alpha个,并发数
        if (!_tried->count(nearest[i]))//如果节点已经探测过,跳过去
        {
            auto r = nearest[i];
            tried.push_back(r); //放入tried,不是_tried
            FindNode p(r->endpoint, _node);
            p.sign(m_secret);
            DEV_GUARDED(x_findNodeTimeout)
                m_findNodeTimeout.push_back(make_pair(r->id, chrono::steady_clock::now()));
            m_socketPointer->send(p);//发送findnode数据包
        }

    if (tried.empty())//如果没有可以连接的最近节点了,退出当前discover流程,并生成随机节点探测
    {
        LOG(m_logger) << "Terminating discover after " << _round << " rounds.";
        doDiscovery();
        return;
    }

    while (!tried.empty()) //把前面发现的节点(在tried中)放到_tried中
    {
        _tried->insert(tried.front());
        tried.pop_front();
    }

    //定时检查请求时间 600ms
    m_timers.schedule(c_reqTimeout.count() * 2, [this, _node, _round, _tried](boost::system::error_code const& _ec)
    {
        if (_ec)
            // we can't use m_logger here, because captured this might be already destroyed
            clog(VerbosityDebug, "discov")
                << "Discovery timer was probably cancelled: " << _ec.value() << " "
                << _ec.message();

        if (_ec.value() == boost::asio::error::operation_aborted || m_timers.isStopped())
            return;

        // error::operation_aborted means that the timer was probably aborted.
        // It usually happens when "this" object is deallocated, in which case
        // subsequent call to doDiscover() would cause a crash. We can not rely on
        // m_timers.isStopped(), because "this" pointer was captured by the lambda,
        // and therefore, in case of deallocation m_timers object no longer exists.

        doDiscover(_node, _round + 1, _tried);//进行下一次循环
    });
}

前面一部分是达到最大round 8次之后,回到前面等待7200ms超时再次刷新,重新选择随机节点

最后一部分是等待300*2ms后,再进行一次doDiscover  嵌套执行,round会加1

中间部分是doDiscover真正的内容,

首先是nearestNodeEntries

map<unsigned, list<shared_ptr<NodeEntry>>> found;

/*
* 中间代码是查找离着_target最近的节点,放入found
* found是map类型,已经自动以距离为key 从小到大排序
*/

//然后从found取出最小的s_bucketSize  16个 放入ret,返回
vector<shared_ptr<NodeEntry>> ret;
for (auto& nodes: found)
   for (auto const& n: nodes.second)
       if (ret.size() < s_bucketSize && !!n->endpoint && n->endpoint.isAllowed())
           ret.push_back(n);
return ret;

这个过程就是找出距离targetID 距离最近的k个节点,

接下来对节点发送FindNode消息,这个消息的返回会带有节点的详细信息,这样的话,就可以保存到自己的nodetable中,具体看下收到FindNode消息后的处理

void NodeTable::onPacketReceived(
    UDPSocketFace*, bi::udp::endpoint const& _from, bytesConstRef _packet)
{
    try {
        unique_ptr<DiscoveryDatagram> packet = DiscoveryDatagram::interpretUDP(_from, _packet);
       ......
        // 根据收到的消息类型进行处理
        switch (packet->packetType())
        {
            case Pong::type:
            {
                ......
            }

            case Neighbours::type:
            {
                auto const& in = dynamic_cast<Neighbours const&>(*packet);
                bool expected = false;
                auto now = chrono::steady_clock::now();
                DEV_GUARDED(x_findNodeTimeout)
                {
                    m_findNodeTimeout.remove_if([&](NodeIdTimePoint const& _t) noexcept {
                        if (_t.first != in.sourceid)
                            return false;
                        if (now - _t.second < c_reqTimeout)
                            expected = true;
                        return true;
                    });
                }
                if (!expected)
                {
                    cnetdetails << "Dropping unsolicited neighbours packet from "
                                << _from.address();
                    break;
                }

                for (auto const& n : in.neighbours)
                    addNode(Node(n.node, n.endpoint));
                break;
            }

            case FindNode::type:
            {
                auto const& in = dynamic_cast<FindNode const&>(*packet);
                vector<shared_ptr<NodeEntry>> nearest = nearestNodeEntries(in.target);
                static unsigned constexpr nlimit = (NodeSocket::maxDatagramSize - 109) / 90;
                for (unsigned offset = 0; offset < nearest.size(); offset += nlimit)
                {
                    Neighbours out(_from, nearest, offset, nlimit);
                    LOG(m_logger) << out.typeName() << " to " << in.sourceid << "@" << _from;
                    out.sign(m_secret);
                    if (out.data.size() > 1280)
                        cnetlog << "Sending truncated datagram, size: " << out.data.size();
                    m_socket->send(out);
                }
                break;
            }

            case PingNode::type:
            {
               ......
            }
        }

        noteActiveNode(packet->sourceid, _from);
    }
    ......
}

 这里可以看出,一共有4中消息 PING PONG  FindNode  和 Neighbours,

收到FindNode消息后,查找本地距离target最近的K个Node,然后每个Node,回应Neighbours消息

收到回复的Neighbours消息后,调用addNode, 主要是对新的节点进项ping操作,具体处理后面再看,这里要看到无论收到哪个消息都会调用noteActiveNode节点,即无论对端是主动还是被动的消息,都进行active的标记,先来看下这个消息

void NodeTable::noteActiveNode(Public const& _pubk, bi::udp::endpoint const& _endpoint)
{
    ......

    shared_ptr<NodeEntry> newNode = nodeEntry(_pubk); //这里是只在已知的nodes中查找,如果是未知的node,可以由前面addNode,进行更新
    if (newNode && RLPXDatagramFace::secondsSinceEpoch() <
                       newNode->lastPongReceivedTime + c_bondingTimeSeconds)
    {
        LOG(m_logger) << "Active node " << _pubk << '@' << _endpoint;
        newNode->endpoint.setAddress(_endpoint.address());
        newNode->endpoint.setUdpPort(_endpoint.port()); // 更新查到的节点的信息

        //这里就是启动更新K-桶的操作,有新的节点,就会有老的节点要替换,或者重新激活
        shared_ptr<NodeEntry> nodeToEvict;
        {
            Guard l(x_state);
            // Find a bucket to put a node to
            NodeBucket& s = bucket_UNSAFE(newNode.get());
            auto& nodes = s.nodes;

            // check if the node is already in the bucket
            auto it = std::find(nodes.begin(), nodes.end(), newNode);
            if (it != nodes.end())
            {
                // if it was in the bucket, move it to the last position
                nodes.splice(nodes.end(), nodes, it);
            }
            else
            {
                if (nodes.size() < s_bucketSize)
                {
                    // if it was not there, just add it as a most recently seen node
                    // (i.e. to the end of the list)
                    nodes.push_back(newNode);
                    if (m_nodeEventHandler)
                        m_nodeEventHandler->appendEvent(newNode->id, NodeEntryAdded);
                }
                else
                {
                    // if bucket is full, start eviction process for the least recently seen node
                    nodeToEvict = nodes.front().lock();
                    // It could have been replaced in addNode(), then weak_ptr is expired.
                    // If so, just add a new one instead of expired
                    if (!nodeToEvict)
                    {
                        nodes.pop_front();
                        nodes.push_back(newNode);
                        if (m_nodeEventHandler)
                            m_nodeEventHandler->appendEvent(newNode->id, NodeEntryAdded);
                    }
                }
            }
        }

        if (nodeToEvict) //如果有要替换,则进行替换流程
            evict(*nodeToEvict, *newNode);
    }
}

代码中的注释很清楚了,就是从已知的节点里找到这个节点,再用该节点进行更新K-桶操作。这里有个addNode流程的影响,如果是一个未知的临近节点,会进行ping pong操作,进而加入到已知的节点中,ping pong消息也会激活noteActive流程,进而把最开始未知的新节点更新进K-桶

整个Kad算法,就剩下两个流程:ping-pong后,加入到已知node列表中;再一个就是更新k-桶时的替换过程,具体看下:

Ping Pong操作:

case PingNode::type:
{
    auto& in = dynamic_cast<PingNode&>(*packet);
    in.source.setAddress(_from.address());
    in.source.setUdpPort(_from.port());
    addNode(Node(in.sourceid, in.source));
                
    Pong p(in.source);
    LOG(m_logger) << p.typeName() << " to " << in.sourceid << "@" << _from;
    p.echo = in.echo;
    p.sign(m_secret);
    m_socket->send(p);
    break;
 }

 case Pong::type:
 {
    auto const& pong = dynamic_cast<Pong const&>(*packet);
    auto const& sourceId = pong.sourceid;

    // validate pong
    auto const sentPing = m_sentPings.find(sourceId);
 
    ......
    auto const sourceNodeEntry = nodeEntry(sourceId);
    assert(sourceNodeEntry.get());
    sourceNodeEntry->lastPongReceivedTime = RLPXDatagramFace::secondsSinceEpoch();

    / Valid PONG received, so we don't want to evict this node,
    // and we don't need to remember replacement node anymore
    auto const& optionalReplacementID = sentPing->second.replacementNodeID;
    if (optionalReplacementID)
        if (auto replacementNode = nodeEntry(*optionalReplacementID))
             dropNode(move(replacementNode));

     m_sentPings.erase(sentPing);

     // update our endpoint address and UDP port
     DEV_GUARDED(x_nodes)
     {
        if ((!m_hostNodeEndpoint || !m_hostNodeEndpoint.isAllowed()) &&
             isPublicAddress(pong.destination.address()))
                m_hostNodeEndpoint.setAddress(pong.destination.address());
                m_hostNodeEndpoint.setUdpPort(pong.destination.udpPort());
      }
      break;
 }

也不是很复杂,Ping收到后,还是addNode,然后回应Pong

Pong收到后,先判断是不是从淘汰节点流程中发出的ping,如果是的话,就不退出该节点,dropNode(move(replacementNode))。

这里又涉及到了最后一个流程,节点淘汰,节点淘汰调用evict 然后很简单,也是直接ping该节点,再函数Ping中,会起一个定时器,并把ping的节点记录下来

void NodeTable::ping(NodeEntry const& _nodeEntry, boost::optional<NodeID> const& _replacementNodeID)
{
    //启动定时器
    m_timers.schedule(0, [this, _nodeEntry, _replacementNodeID](
                             boost::system::error_code const& _ec) {
        if (_ec || m_timers.isStopped())
            return;

        NodeIPEndpoint src;
        src = m_hostNodeEndpoint;
        PingNode p(src, _nodeEntry.endpoint);
        auto const pingHash = p.sign(m_secret);
        LOG(m_logger) << p.typeName() << " to " << _nodeEntry.id << "@" << p.destination;
        m_socket->send(p);
        
        //发送完ping包后,记录下该节点了,后面有两个地方会读取 1,收到pong包,2.定时器超时
        m_sentPings[_nodeEntry.id] = {chrono::steady_clock::now(), pingHash, _replacementNodeID};
    });
}

最后的记录,有2处会进行读取,1是收到Pong包,前面分析pong消息时看到了,另外1个就是定时器超时的处理:

void NodeTable::doHandleTimeouts()
{
    //又启动定时器 超时后继续调用该函数处理
    m_timers.schedule(c_handleTimeoutsIntervalMs, [this](boost::system::error_code const& _ec) {
        if ((_ec && _ec.value() == boost::asio::error::operation_aborted) || m_timers.isStopped())
            return;

        vector<shared_ptr<NodeEntry>> nodesToActivate;
        //对等待ping超时的节点进行淘汰处理
        for (auto it = m_sentPings.begin(); it != m_sentPings.end();)
        {
            if (chrono::steady_clock::now() >
                it->second.pingSendTime + DiscoveryDatagram::c_timeToLive)
            {
                if (auto node = nodeEntry(it->first))
                {
                    //这里就是从k-桶中淘汰掉
                    dropNode(move(node));

                    // save the replacement node that should be activated
                    if (it->second.replacementNodeID)
                        if (auto replacement = nodeEntry(*it->second.replacementNodeID))
                            nodesToActivate.emplace_back(replacement);
                }

                it = m_sentPings.erase(it);
            }
            else
                ++it;
        }

        //把新节点放进去
        // activate replacement nodes and put them into buckets
        for (auto const& n : nodesToActivate)
            noteActiveNode(n->id, n->endpoint);
        //继续执行下一次的超时处理
        doHandleTimeouts();
    });
}

至此,Ethereum实现Kad算法的整个流程来同步更新节点结束了。总结下:

1. 新的节点接进来后,会有一些公共的节点放入k-桶

2. 开始发现流程,启动定时刷新K-桶定时器,即保证已经加入到K-桶的节点的活力,发现新的节点,替换老的无响应的节点

     随机选择1个target,然后查找自己K-桶里离着target最新的K个节点,挨着对每个节点发送对target的FindNode节点消息

     对端收到后,再查找自己K-桶里离着最近的K个节点,并依次回复Neighbours消息

     收到对端的Neighbours消息后,增加节点addNode  

3. addNode中又看看这个节点是不是已知的,是的话添加到m_nodes中,否则开始发送Ping包,等待Pong包,如果收到Pong包就把它移除ping pending表中

最后所有的消息都会处罚noteActiveNode,在这里统一更新K-桶,例如上面收到Pong包,也会触发。

4. 再更新K-桶时,对于老的节点,要进行淘汰流程,很直接起个ping的定时器,发送ping包,如果如期收到Pong包,移除ping pengding状态,也不用丢弃该节点,如果收不到,会有超时处理函数,队等待Pong的所有节点进行淘汰处理,即完成最后的替换K-桶的动作

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值