区块链特辑 :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<NodeTable>
{
friend std::ostream& operator<<(std::ostream& _out, NodeTable const& _nodeTable);
using NodeSocket = UDPSocket<NodeTable, 1280>; // UDPSocket,这是在UDP.h中定义,1280表示的是最大数据报大小
using TimePoint = std::chrono::steady_clock::time_point; // < Steady time point.
using NodeIdTimePoint = std::pair<NodeID, TimePoint>;
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<NodeEntry> addNode(Node const& _node, NodeRelation _relation = NodeRelation::Unknown); //添加节点,这部分内容较多,会在后面流程介绍细说
std::list<NodeID> nodes() const; // 返回node table中活跃的node id的列表
unsigned count() const { return m_nodes.size(); } // 返回节点数量
std::list<NodeEntry> 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<s_bits>::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<std::weak_ptr<NodeEntry>> 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> nodeEntry(NodeID _id);
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>>>()); // 用于发现给定目标距离近的节点
std::vector<std::shared_ptr<NodeEntry>> nearestNodeEntries(NodeID _target); //返回距离target最近的节点列表
void evict(std::shared_ptr<NodeEntry> _leastSeen, std::shared_ptr<NodeEntry> _new); // 异步丢弃不响应的_leastSeen节点,并添加_new节点,否则丢弃_new
void noteActiveNode(Public const& _pubk, bi::udp::endpoint const& _endpoint); //为了维持节点table,无论何时从一个节点获取到activity,都会调用这个noteActiveNode
void dropNode(std::shared_ptr<NodeEntry> _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<NodeTableEventHandler> 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<NodeID, std::shared_ptr<NodeEntry>> 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<NodeBucket, s_bins> 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<NodeID, EvictionTimeout> m_evictions; // < Eviction timeouts.
Mutex x_pubkDiscoverPings;