(建议先看总体流程梳理 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-桶的动作