K桶的实现
这里借用一下别人的图:
可以看到这个就是一个二维数组,可以简单定义为:
int bucket[256][16]
其中第一维是从0-255表示距离,第二维记录16个节点。在代码中实际的K桶定义是:
std::array<NodeBucket, s_bins> m_state;
这个定义等同于
NodeBucket[s_bins] m_state;
其中s_bins
值为255,根据上面的图,s_bins应该是256,为什么是255呢?这里其实是把距离为0的那些节点排除掉了,所以为255。再来看NodeBucke
t的定义:
struct NodeBucket
{
unsigned distance;
std::list<std::weak_ptr<NodeEntry>> nodes;
};
这里的nodes
就是实际存放在K桶里的节点了,在这里还看不出来nodes
数组的大小,不过在实际添加节点的时候会有大小判断:
if (nodes.size() < s_bucketSize)
{
// ...
nodes.push_back(newNode);
// ...
}
s_bucketSize
的值正是16.
noteActiveNode
noteActiveNode
这个函数在每收到一个UDP包都会被调用,其主要实现代码为:
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);
}
}
}
这部分是目前我贴出来的最长的代码了,代码虽然长一点,但是可读性非常好,而且有详细的注释。这部分涉及到K桶的操作,收到一个节点后判断是否已经在K桶中了,如果在的话就移动它到nodes
的末尾,如果不存在就放到K桶中,这里需要重点注意两行代码:
if (m_nodeEventHandler)
m_nodeEventHandler->appendEvent(newNode->id, NodeEntryAdded);
这段代码在节点被放到K桶中时会被调用,这两句非常重要,作用后面再说。
预设节点
这里还有个问题,不知道读者朋友有没有注意到,那就是虽然有节点发现协议来源源不断地发现新节点,但是初始化的时候是只有本节点一个节点的,我从哪里去获得其他节点的IP呢?
还记得上一节的nearestNodeEntries()
这个函数吗?这个函数是从K桶中取得节点,按距离从小到大排列,然后向这些节点发送FindNode
消息的,但是初始情况下K桶是空的,我们要向哪发消息呢?
巧妇也难为无米之炊啊,因此这里必然隐藏了一个特殊的绿色通道,用来直接放入初始节点的。
这个绿色通道就是NodeTable::addNode()
函数,这个函数在多处出现,大部分情况下为正常调用,但是注意到这个函数在Host::addNodeToNodeTable()
函数里调用了,我们再来顺藤摸瓜,看看Host::addNodeToNodeTable()
在哪里调用,也有多个地方,但是值得注意的是在Host::requirePeer()
里的调用。因为这个函数在aleth\main.cpp
里被调用到了,我把调用的地方贴出来:
for (auto const& i: Host::pocHosts())
web3.requirePeer(i.first, i.second);
这段代码看起来就是在添加一些节点,web3
为dev::WebThreeDirect
类,而 web3.requirePeer()
会调用Host::requirePeer()
,至此这些预设节点被添加到NodeTable
中,相当于给机器一个初始的力,于是机器开始正常运转起来。
有兴趣的读者还可以去Host::pocHosts()
看看是哪些初始节点。
添加节点到传输网络
以太坊的P2P模块其实分为两部分,第一部分就是目前说到的UDP节点发现协议,另外一部分是TCP的节点间数据传输协议,这两部分之间是通过什么交互的呢?也就是节点是怎么从第一部分被发现到第二部分和本节点间传输数据呢?其实前面这两句看起来不起眼的代码就是连接两部分之间的桥梁。
NodeTable
类内置一个事件处理器:
std::unique_ptr<NodeTableEventHandler> m_nodeEventHandler;
NodeTableEventHandler
类通过appendEvent()
来添加事件,并通过processEvents()
来处理每个事件,那么事件是怎么处理的呢?
但是processEvent()
事件处理函数是一个纯虚函数:
virtual void processEvent(NodeID const& _n, NodeTableEventType const& _e) = 0;
因此要到它的子类去找,可以看到libp2p\Host.h
文件中定义了子类:HostNodeTableHandler
,也实现了processEvent()
函数:
void HostNodeTableHandler::processEvent(NodeID const& _n, NodeTableEventType const& _e)
{
m_host.onNodeTableEvent(_n, _e);
}
这里又调用了Host::onNodeTableEvent()
函数。
void Host::onNodeTableEvent(NodeID const& _n, NodeTableEventType const& _e)
{
if (_e == NodeEntryAdded)
{
LOG(m_logger) << "p2p.host.nodeTable.events.nodeEntryAdded " << _n;
if (Node n = nodeFromNodeTable(_n))
{
shared_ptr<Peer> p;
// ...
if (peerSlotsAvailable(Egress))
connect(p);
}
}
else if (_e == NodeEntryDropped)
{
// ...
if (m_peers.count(_n) && m_peers[_n]->peerType == PeerType::Optional)
m_peers.erase(_n);
}
}
这里同样精简了代码,值得注意的是connect(p)
;这一句,从字面上看是连接节点,实际代码是通过boost::asio库实现了底层网络传输。
现在事情已经比较清楚了,那么NodeTableEventHandler::processEvent()
函数是由谁来调用的呢?其实是Host
里的一个定时器调用的,有兴趣的读者可以去看Host::run()
函数,其实就是Host
类与NodeTable
类之间的相互调用。