在上一章 数据包 (Packet)中,我们学习了 ZeroTier 网络中用于通信的"数字信封"。我们知道了所有信息,无论是普通数据还是控制指令,都被封装在标准的、安全的 Packet
中进行传输。
这带来了一个更高层次的问题:当一个节点 (Node)想要给网络中的另一个节点发送一个 Packet
时,它如何知道对方的存在?它如何找到对方?更进一步,当一个节点刚启动,对整个网络一无所知时,它应该联系谁来"问路"呢?
为了解决这些问题,每个节点都需要维护一份关于整个 ZeroTier 网络的"地图"。这份地图就是 拓扑 (Topology)
的核心概念。
什么是拓扑 (Topology)?
Topology
是一个节点 (Node)下的整个 ZeroTier 网络地图。你可以把它想象成一个节点的**"全球通讯录和GPS导航系统"**。
这个系统不仅仅是一个简单的联系人列表,它包含了导航和发现新朋友所需的一切:
- 联系人总管 (Peer Manager) :
Topology
的核心是一个庞大的通讯录,它管理着所有已知的对等节点 (Peer)对象。每当你与一个新的节点建立联系,它的"名片"(Peer
对象)就会被存入Topology
中,以备将来快速查找。 - 世界地图 (World Definition) : 新节点刚启动时,通讯录是空的。它如何迈出第一步?
Topology
内置了一份"世界地图",即World
对象。这份地图上标记了 ZeroTier 网络的官方根服务器 (我们称之为行星 (Planet))的地址。这些行星服务器是全球公开、稳定可靠的"引路人"。 - 私人地标 (Moons) : 除了全球通用的"行星",用户还可以创建自己的私有根服务器,我们称之为卫星 (Moon) 。这就像在你的私人地图上添加一些只有你和你的朋友知道的秘密集合点。
Topology
同样负责管理这些"卫星"的信息。
当一个节点需要寻找另一个节点时,它首先会查看自己的通讯录 (Topology
中的 Peer
列表)。如果找不到,它就会向地图上的"行星"或"卫星"发消息求助,让这些根服务器帮忙介绍和引路。因此,Topology
提供了网络的骨架结构,是所有节点能够互相发现和通信的基础。
World
:网络的根基
World
对象是 Topology
中一个至关重要的概念。它定义了一组根服务器,这些服务器是网络的"锚点"。
一个 World
对象(无论是行星还是卫星)主要包含:
- 根服务器列表 : 一个或多个
Root
结构的列表。 - 每个
Root
的信息 :Identity
: 根服务器节点的身份 (Identity)。stableEndpoints
: 一个或多个稳定的、公开的 IP 地址和端口号。这是可以找到该根服务器的具体"门牌号"。
让我们看看 World
在代码中的样子:
cpp
// 文件: node/World.hpp
// Root 代表一个根服务器
struct Root
{
Identity identity; // 根服务器的身份
std::vector<InetAddress> stableEndpoints; // 它的稳定联系地址列表
};
// World 代表一个完整的根服务器集合
class World
{
private:
uint64_t _id; // World 的唯一ID
Type _type; // 类型:是行星(PLANET)还是卫星(MOON)
std::vector<World::Root> _roots; // 根服务器列表
// ... 其他元数据,如签名和时间戳 ...
};
- 行星 (Planet) : 有一个默认的、全球共享的
World
,它的根服务器由 ZeroTier 公司运营。这个World
的定义被硬编码在 ZeroTier 客户端中,确保任何新安装的客户端都能立即找到回家的路。 - 卫星 (Moon) : 用户可以自行搭建根服务器,并创建一个自定义的
World
定义文件。当一个节点被配置使用这个"卫星"时,它就会把这个自定义的World
加载到自己的Topology
中。
Topology
的工作流程:新节点的寻路之旅
想象一下,你在一台全新的电脑上安装并启动了 ZeroTier。这个新节点(我们称之为A)是如何找到并连接到你的另一台设备(节点B)的呢?
- 加载地图 : 当节点A启动时,它的
Topology
对象被创建。构造函数会立即加载内置的、硬编码的"行星"World
定义。现在,Topology
知道了几个行星服务器的公网IP地址。 - 向导引者报到 : 节点A会向这些行星服务器发送
HELLO
数据包 (Packet),宣告自己的到来。行星服务器收到后,会记下节点A的公网地址。 - 问路 : 当A想联系B时,它会向行星服务器发送一个
VERB_WHOIS
数据包,询问B的下落。 - 获取线索 : 行星服务器会查找自己的记录,告诉A:"我最近在
[B的公网地址]
这个地方见过B"。 - 建立直接联系 : 节点A拿到B的公网地址后,就会尝试直接向该地址发送
HELLO
包。同时,这个过程也会帮助A和B建立起一个直接的对等节点 (Peer)关系。 - 更新通讯录 : 一旦A和B成功通信,节点A的
Topology
就会为B创建一个Peer
对象,并将其保存在内部的哈希表中。从此以后,A就可以直接在自己的"通讯录"里找到B,而无需每次都去问行星服务器了。
代码中的 Topology
Topology
类是管理网络视图的核心。它的定义可以在 node/Topology.hpp
中找到。
cpp
// 文件: node/Topology.hpp
class Topology
{
private:
// 用哈希表存储所有已知的 Peer,方便快速查找
Hashtable< Address,SharedPtr<Peer> > _peers;
Mutex _peers_m; // 保护 Peer 列表的锁
// 存储"行星"和"卫星"的世界定义
World _planet;
std::vector<World> _moons;
Mutex _upstreams_m; // 保护 World 定义的锁
public:
// 构造函数
Topology(const RuntimeEnvironment *renv, void *tPtr);
// 根据 ZeroTier 地址获取一个 Peer 对象
SharedPtr<Peer> getPeer(void *tPtr, const Address &zta);
// 添加一个新的或更新的 World 定义 (行星或卫星)
bool addWorld(void *tPtr, const World &newWorld, bool alwaysAcceptNew);
// 执行周期性任务,如清理不活跃的 Peer
void doPeriodicTasks(void *tPtr, int64_t now);
// ... 其他辅助方法
};
_peers
: 一个哈希表,键是节点的 ZeroTier 地址,值是指向对等节点 (Peer)对象的智能指针。这是"联系人总管"的核心数据结构。_planet
和_moons
: 分别存储了行星和所有卫星的World
定义。这就是"世界地图"和"私人地标"。getPeer()
: 这是最常用的功能之一,允许代码的其他部分(如交换机 (Switch))通过地址快速找到对应的Peer
对象。addWorld()
: 当收到来自网络的World
更新,或用户手动添加"卫星"时,此方法用于更新Topology
的地图信息。doPeriodicTasks()
: 一个内务管理功能,用于定期清理那些长时间没有联系的Peer
对象,以释放内存资源。
深入代码:开箱即用的"世界地图"
一个新节点是如何做到"开箱即用",无需任何配置就能连接到 ZeroTier 全球网络的呢?答案就藏在 Topology
的构造函数中。
cpp
// 文件: node/Topology.cpp
// 一个硬编码的字节数组,包含了默认"行星"世界的序列化数据
static const unsigned char ZT_DEFAULT_WORLD[...] = { ... };
Topology::Topology(const RuntimeEnvironment *renv, void *tPtr)
{
// ... (尝试从本地缓存加载已有的星球信息) ...
// 创建一个 World 对象来存放默认的星球定义
World defaultPlanet;
{
// 将硬编码的字节数组包装成一个 Buffer 对象
Buffer<ZT_DEFAULT_WORLD_LENGTH> wtmp(ZT_DEFAULT_WORLD, ...);
// 从 Buffer 中反序列化,填充 defaultPlanet 对象
defaultPlanet.deserialize(wtmp, 0);
}
// 将这个默认的星球定义添加到拓扑中
addWorld(tPtr, defaultPlanet, false);
}
这段代码揭示了 ZeroTier 的"引导"秘密:
- 代码中有一个名为
ZT_DEFAULT_WORLD
的静态字节数组。这个数组实际上就是"行星"World
对象的序列化表示,它包含了当前所有官方根服务器的身份 (Identity)和它们的公网IP地址。 Topology
在被创建时,会立即从这个硬编码的数组中反序列化出一个World
对象。- 然后,它调用
addWorld()
将这个World
对象加载到自己的_planet
成员变量中。
就这样,仅仅通过执行构造函数,Topology
就已经拥有了第一份、也是最重要的一份地图,知道了该去哪里"问路"。
深入代码:清理不活跃的联系人
Topology
不仅要添加新的Peer
,还要负责清理那些"失联"的联系人,以防内存被无限占用。这个工作在 doPeriodicTasks()
方法中完成。
cpp
// 文件: node/Topology.cpp
void Topology::doPeriodicTasks(void *tPtr, int64_t now)
{
{
Mutex::Lock _l1(_peers_m); // 锁定 Peer 列表以确保线程安全
// ...
while (i.next(a, p)) { // 遍历哈希表中的每一个 Peer
// 如果 Peer 不再存活 (isAlive() 返回 false)
// 并且它不是一个不能被删除的根服务器 (isUpstream)
if ( (!(*p)->isAlive(now)) && (std::find(...) == ...) ) {
_savePeer(tPtr, *p); // 将 Peer 的状态保存到磁盘,以备下次快速恢复
_peers.erase(*a); // 从内存的哈希表中移除
}
}
}
// ...
}
这个函数会定期被节点 (Node)调用。它的逻辑很简单:
- 遍历
_peers
哈希表中的所有Peer
对象。 - 对每一个
Peer
,调用isAlive(now)
方法检查它是否在最近一段时间内有过通信。 - 如果一个
Peer
长时间不活跃,并且它不是一个必须保持联系的根服务器,Topology
就会先把它当前的状态信息序列化并保存到磁盘上(为了下次能快速加载),然后从内存中将其删除。
这个机制确保了Topology
的"通讯录"既能保持最新,又不会因为包含了太多早已下线的节点而变得臃肿。
总结
在本章中,我们探索了 ZeroTier 节点的"全球通讯录和GPS导航系统"------拓扑 (Topology)
。
Topology
是节点对整个 ZeroTier 网络的宏观视图,它管理着所有已知的对等节点 (Peer)。- 它的核心是**
World
的概念,即网络的 根服务器**(行星 和卫星)定义。这份"地图"为新节点提供了最初的引导,使其能够找到并融入网络。 Topology
通过内置的默认"行星"定义,实现了 ZeroTier 的开箱即用特性。- 它还负责管理
Peer
对象的生命周期 ,包括添加、查找以及清理不活跃的Peer
,维持了整个系统的健康。
我们现在已经了解了节点如何通过 Topology
和 Peer
来管理虚拟的连接关系。但是,这些虚拟连接最终都要通过真实的互联网来传输数据。这些真实的、物理的网络连接在 ZeroTier 中是如何被表示和管理的呢?