ZeroTier 源码解析 (7) 拓扑 (Topology)

在上一章 数据包 (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 对象(无论是行星还是卫星)主要包含:

  1. 根服务器列表 : 一个或多个 Root 结构的列表。
  2. 每个 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)的呢?

sequenceDiagram participant Node_A as 新节点 A participant Topology_A as A 的 Topology 对象 participant Planet as 行星服务器 (根服务器) participant Node_B as 目标节点 B Node_A->>Topology_A: 启动时,构造 Topology 对象 Topology_A->>Topology_A: 加载内置的"行星"世界地图 Topology_A-->>Node_A: 我知道行星服务器的地址了 Node_A->>Planet: HELLO! 我是新来的节点A,这是我的地址 Planet-->>Node_A: 欢迎!我记住你了 Note over Node_A, Planet: 节点A向行星服务器注册自己的存在 Node_A->>Planet: WHOIS? 我想找节点B,你知道它在哪吗? Planet-->>Node_A: B最近在[B的公网地址]出现过 Note over Node_A, Planet: 行星服务器充当了介绍人 Node_A->>Node_B: (直接向B的公网地址) HELLO! 我是A Node_B-->>Node_A: HELLO! 我收到你的消息了 Note over Node_A, Node_B: A和B尝试建立直接的 Peer 连接 Node_A->>Topology_A: 我和B联系上了,请为它创建一个 Peer 对象 Topology_A-->>Topology_A: 将 B 的 Peer 对象存入通讯录
  1. 加载地图 : 当节点A启动时,它的 Topology 对象被创建。构造函数会立即加载内置的、硬编码的"行星" World 定义。现在,Topology 知道了几个行星服务器的公网IP地址。
  2. 向导引者报到 : 节点A会向这些行星服务器发送 HELLO 数据包 (Packet),宣告自己的到来。行星服务器收到后,会记下节点A的公网地址。
  3. 问路 : 当A想联系B时,它会向行星服务器发送一个 VERB_WHOIS 数据包,询问B的下落。
  4. 获取线索 : 行星服务器会查找自己的记录,告诉A:"我最近在 [B的公网地址] 这个地方见过B"。
  5. 建立直接联系 : 节点A拿到B的公网地址后,就会尝试直接向该地址发送 HELLO 包。同时,这个过程也会帮助A和B建立起一个直接的对等节点 (Peer)关系。
  6. 更新通讯录 : 一旦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 的"引导"秘密:

  1. 代码中有一个名为 ZT_DEFAULT_WORLD 的静态字节数组。这个数组实际上就是"行星" World 对象的序列化表示,它包含了当前所有官方根服务器的身份 (Identity)和它们的公网IP地址。
  2. Topology 在被创建时,会立即从这个硬编码的数组中反序列化出一个 World 对象。
  3. 然后,它调用 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)调用。它的逻辑很简单:

  1. 遍历 _peers 哈希表中的所有 Peer 对象。
  2. 对每一个 Peer,调用 isAlive(now) 方法检查它是否在最近一段时间内有过通信。
  3. 如果一个 Peer 长时间不活跃,并且它不是一个必须保持联系的根服务器,Topology 就会先把它当前的状态信息序列化并保存到磁盘上(为了下次能快速加载),然后从内存中将其删除。

这个机制确保了Topology的"通讯录"既能保持最新,又不会因为包含了太多早已下线的节点而变得臃肿。

总结

在本章中,我们探索了 ZeroTier 节点的"全球通讯录和GPS导航系统"------拓扑 (Topology)

  • Topology 是节点对整个 ZeroTier 网络的宏观视图,它管理着所有已知的对等节点 (Peer)。
  • 它的核心是**World的概念,即网络的 根服务器**(行星卫星)定义。这份"地图"为新节点提供了最初的引导,使其能够找到并融入网络。
  • Topology 通过内置的默认"行星"定义,实现了 ZeroTier 的开箱即用特性。
  • 它还负责管理Peer对象的生命周期 ,包括添加、查找以及清理不活跃的Peer,维持了整个系统的健康。

我们现在已经了解了节点如何通过 TopologyPeer 来管理虚拟的连接关系。但是,这些虚拟连接最终都要通过真实的互联网来传输数据。这些真实的、物理的网络连接在 ZeroTier 中是如何被表示和管理的呢?

相关推荐
小马敲马12 分钟前
[4.2-2] NCCL新版本的register如何实现的?
开发语言·c++·人工智能·算法·性能优化·nccl
soilovedogs1 小时前
百度之星2024初赛第二场 BD202411染色
c++·算法·百度之星
啊阿狸不会拉杆2 小时前
《算法导论》第 15 章 - 动态规划
数据结构·c++·算法·排序算法·动态规划·代理模式
2401_858286112 小时前
CD64.【C++ Dev】多态(3): 反汇编剖析单继承下的虚函数表
开发语言·c++·算法·继承·面向对象·虚函数·反汇编
重启的码农4 小时前
ZeroTier 源码解析 (6) 数据包 (Packet)
c++·网络协议
芥子须弥Office15 小时前
从C++0基础到C++入门 (第二十五节:指针【所占内存空间】)
c语言·开发语言·c++·笔记
啊阿狸不会拉杆15 小时前
《算法导论》第 14 章 - 数据结构的扩张
数据结构·c++·算法·排序算法
Q741_14717 小时前
如何判断一个数是 2 的幂 / 3 的幂 / 4 的幂 / n 的幂 位运算 总结和思考 每日一题 C++的题解与思路
开发语言·c++·算法·leetcode·位运算·总结思考
慕y27418 小时前
Java学习第一百二十二部分——HTTPS
网络协议·学习·https