为了账号安全,请及时绑定邮箱和手机立即绑定

我用C语言实现基于Chord协议的DHT算法的经历

目录:
  1. 介绍
  2. 想法
  3. 实际实现
  • DHT

  • Chord 算法介绍

  • 我自己实现的节点和数据

  • find_successor

  • 创建/加入网络

  • 稳定机制

  • fix_fingers()

  • stabilize()notify()

  • check_predecessor()

  • 完整流程
    1. 额外资源和参考资料
    2. 我是谁?
    3. Hack Club 的 Arcade 🟧
简介

在开始之前,我想先说明一下我的经历。这篇文章既可以被视为我的个人经历记录,也可以被视为用C语言实现Chord协议的指南。希望您喜欢这篇文章!

我最近创建了一个点对点文件分享系统(你可以在这里找到完整的代码库链接here,如果你觉得不错,别忘了给我点个⭐️哦:D),我是用C语言编写的,我打算分享一下我为何开始这个项目,我又是如何实现它的,以及它又是如何使我成为更好的程序员的。

想法是

我是一名高中生,同时也是一名网页开发者,特别对云计算感兴趣。有一段时间了,我开始接触C语言,想要深入理解计算机底层工作原理以及如何有效地进行操作。为了真正理解计算机在底层的工作原理以及如何有效地与之交互,因此我决定做一些底层的网络项目,比如我用C语言实现了HTTP协议(https://github.com/alessandrofoglia07/minimalist-http-server)以及一个简单的聊天应用(https://github.com/alessandrofoglia07/chat-application-c)。无疑,这个项目是最具挑战的一个

最初,我开始实施一个由无序节点组成的分布式网络,就像我在编码这些项目时所学到的那样。然而不久之后,我意识到我无意中选择的方法非常低效,不适合大规模网络。因此,我开始寻找更优化的解决方案,后来我发现了分布式哈希表(DHT)和Chord算法。作为一名高中生,我首先向ChatGPT问道,“在p2p应用程序中实现DHT有哪些好处?”它用诸如“去中心化”、“容错性”、“高效性”和“鲁棒性”这样的专业术语回答了我;然而,它也提到了一个缺点:“实现复杂性”。作为一个几乎不懂如何实现简单数据结构的学生,我可以肯定地说,它确实是事实 😅。

实际操作

DHTs (分布式哈希表)

所以,准确地说……分布式哈希表(DHT)到底是什么
像名字暗示的那样,DHT是一种hash表,它分布在网络中的多个节点上。通过一致性哈希,任意节点都可以找到包含特定键值的节点的位置(例如IP地址和端口),然后联系该节点并获取该值。

关于Chord算法的介绍

那么,Chord是如何有效构建DHT的呢?
Chord(在2001年作为最初的四种DHT协议之一被引入的算法)创建了一个环形网络,在这个网络中,每个节点根据通过哈希函数生成的ID占据一个特定位置(我个人使用的是SHA-1函数)。每个节点都有一个后继和一个前驱:后继是指顺时针方向的下一个节点,前驱是指逆时针方向的前一个节点。环中最多可以有2的M次方个节点,其中M是ID的位数(如果使用SHA-1函数,M就是160位)。

哈希值(Key) = 125
   向下
n1 ---------- n2 
(100)        (200)
 |            |
 |            |
 |            |
 n4 ---------- n3
(400)        (300)

全屏模式退出全屏

最终目标是让系统实现一个 find_successor(hash(key)) 函数,用于查找键在DHT里的位置。这个函数会找到并返回负责该键的节点(也就是对等点)的位置。

将键值进行哈希处理 (Hash(键)) 和找到后续节点 (FindSuccessor(Hash(键))),如图所示:
        Hash(键) FindSuccessor(Hash(键))
            ↓         ↓
        n1 ---------- n2 
      (100)          (200)
        |              |
        |              |
        |              |
        n4 ---------- n3
      (400)          (300)
如图所示,n1 和 n2 的哈希值分别为100和200,而它们之间的连线表示数据传输。同样,n4 和 n3 的哈希值分别为400和300,它们之间也有数据传输。

切换到全屏模式,退出全屏

这些酷炫的插图由@arush15june提供。Chord算法建议使用所谓的指针数组,该数组包含M个节点的数据,以避免线性搜索,转而采用更快的查找方法。

我的节点/数据的实现

从这一点起,我将展示伪代码版本,以及其下方我的C语言实现版的简化版。

具体来说,比如这是我如何定义 Node 的。

    typedef struct Node {
        uint8_t id[20]; // SHA-1 哈希值
        char ip[16]; // 例如:127.0.0.1
        int port; // 例如:5000
        struct Node *successor; // 后继
        struct Node *predecessor; // 前驱
        struct Node *finger[M]; // 指针表(节点数组)
        struct FileEntry *files; // 负责的文件
        struct FileEntry *uploaded_files; // 上传的文件
        int sockfd; // 套接字文件描述符
    } Node;

点击全屏/点击窗口

这里是 FileEntry 结构:

    typedef struct 文件条目 {
        uint8_t id[20]; // 文件的哈希值(用于查找)
        char filename[256];
        char filepath[512];
        char owner_ip[16];
        int owner_port; // 所有者的端口
        struct 文件条目 *next;
    } 文件条目;

切换到全屏 切换回正常模式

在我的实现里,每个节点只存储文件的元数据,这样,寻找文件的对等方可以直接从最初分享该文件的节点下载。我的查询流程如下:

    - 节点A 请求负责节点B 提供文件
    - B 用 FileEntry 对象回应(回传给 A)
    - A 联系 C(通过 file.owner_ip 和 file.owner_port 这两个地址)并下载文件。

进入全屏 退出全屏

查找后继 函数

如我之前所说,算法最重要的功能是find_successor函数。该函数首先检查是否是本地节点,通过将其ID与后继节点的ID以及给定的ID进行比较。如果不是,节点然后在其本地指纹表中查找最接近的前驱的ID。

下面的代码是 find_successor 函数的代码。

    n.find_successor(id)
        // 这是一个半闭区间。
        如果 id 属于 (n, successor] 区间, 
            返回 successor
        否则
            // 将查询沿着环转发
            n0 := n.closest_preceding_node(id)
            返回 n0.find_successor(id)

    n.closest_preceding_node(id)
        对于 i 从 m 到 1 递减循环, 
            如果 finger[i] 属于 (n, id) 区间, 
                返回 finger[i]
        返回 n

点击进入全屏 点击退出全屏

// 检查 id 是否在半开区间 (a, b)
int is_in_interval(const uint8_t *id, const uint8_t *a, const uint8_t *b) {
    if (memcmp(a, b, HASH_SIZE) < 0) {
        return memcmp(id, a, HASH_SIZE) > 0 && memcmp(id, b, HASH_SIZE) < 0;
    }
    return memcmp(id, a, HASH_SIZE) > 0 || memcmp(id, b, HASH_SIZE) < 0;
}

Node *find_successor(Node *n, const uint8_t *id) {
    if (memcmp(id, n->id, HASH_SIZE) > 0 && memcmp(id, n->successor->id, HASH_SIZE) <= 0) {
        return n->successor;
    }

    const Node *n0 = closest_preceding_node(n, id);

    // 避免向自己发送消息
    if (n0 == n) {
        return n->successor;
    }

    // 向 n0 发送 FIND_SUCCESSOR
    send_message(n, n0->ip, n0->port, "FIND_SUCCESSOR");

    // 接收响应
    const Node response = receive_message();

    Node *successor = n->successor;
    memcpy(successor->id, response->id, HASH_SIZE);
    strcpy(successor->ip, response->ip);
    successor->port = response->port;

    return successor;
}

Node *closest_preceding_node(Node *n, const uint8_t *id) {
    // 从后向前搜索手指表,找到最接近的节点
    for (int i = M - 1; i >= 0; i--) {
        if (is_in_interval(n->finger[i]->id, n->id, id)) {
            return n->finger[i];
        }
    }
    return n;
}

进入全屏模式,退出全屏模式

创建网络/加入网络

节点可以将其前驱设为nil/NULL,后继设为自己来创建一个新环,如下:

在初始化时,我们调用 n.create(),并设置前驱节点为 nil,后继节点为 n

点击全屏按钮进入全屏模式,再点击退出按钮。

// 创建一个环,使节点的前驱和后继都指向自身
void create_ring(Node *n) {
    n->前驱 = NULL;
    n->后继 = n;
}

点击这里全屏观看,点击这里退出全屏

如果一个节点想加入现有的环,它需要知道至少一个其他节点的位置。

    n.join(n0);
    predecessor := 空;
    successor := n0.find_successor(n);

切换到全屏模式,退出全屏

    void join_ring(Node *n, const char *existing_ip, const int existing_port) {
        // 给现有节点发 JOIN 消息
        send_message(n, existing_ip, existing_port, "JOIN");

        // 响应的消息中包含新节点的后继
        Message response = receive_message();

        // 创建一个新的节点作为后继
        Node *successor = create_node(response->ip, response->port);

        // 设置新节点的后继为返回的节点
        n->predecessor = NULL;
        n->successor = successor;
    }

点击进入全屏/退出全屏

稳定过程

此时,算法在这里遇到了一个难题。我们需要一种方法来确保在网络节点加入或离开后,网络结构保持不变。网络需要动态更新以允许节点随意加入,同时移除不再使用的节点,重新稳定环形结构。我们可以通过一个稳定程序来解决这个问题。我们需要定期检查这些内容。

  • 当前的继任者依然没有变化,并且在这两者之间没有新的节点加入;
  • 前驱节点仍然存活;
  • finger table已经被更新。

我们可以用这些函数来完成这个,比如 fix_fingers()stabilize()notify()check_predecessor()

修复手指() # 技术术语,意指解决问题或调试

    // 定期调用。刷新指针表中的条目。
    // next 存储需要修复的指针的索引
    n.fix_fingers()
        next := next + 1
        如果 next > m,则
            next := 1
        finger[next] := find_successor(n+2*next-1);  // 找到并更新相应的指针条目

点击全屏 点击退出全屏

    void 修正指针(Node *n, int *下一个指针) {
        *下一个指针 = (*下一个指针 + 1) % 模数;
        // 找到下一个指针指向的节点
        Node *finger = find_successor(n, n->finger[*下一个指针]->id);
        // 更新指针指向新的节点
        n->finger[*下一个指针] = finger;
    }

全屏模式 退出全屏

稳定状态()通知变化()

// 定期执行。n 查询其后继者关于其前驱者,验证 n 的直接后继者是否一致,并将 n 的信息告知后继者
n.stabilize()
    x = 后继者的前驱者
    if x ∈ (n, successor) 且 x 不等于 n then
        successor := x
    后继者通知 n

n 通知 n0
    if predecessor 不存在或 n0 在 predecessor 和 n 之间 then
        predecessor := n0

进入全屏模式。退出全屏模式。

    /*

* 定期调用。向后继节点请求其前驱节点,

* 验证前驱节点是否为自身。如果不是,则说明新节点位于自身和该后继之间。

* 因此,将新节点设为新的后继节点,并通知新节点其新的前驱。
     */
    void stabilize(Node *n) {
        // 向后继节点发送一个 STABILIZE 消息
        send_message(n, n->successor->ip, n->successor->port, "STABILIZE");

        // 接收并验证后继节点的前驱节点信息
        Message response = receive_message();

        // x 是后继节点的前驱节点
        Node *x = create_node(response->ip, response->port);

        // 如果 x 在区间 (n, 后继) 内,则更新后继
        if (is_in_interval(x->id, n->id, n->successor->id)) {
            n->successor = x;
        }

        // 通知新后继更新其前驱
        notify(n->successor, n);
    }

    void notify(Node *n, Node *n_prime) {
        // 向 n 发送一个 NOTIFY 消息
        // 通知 n_prime 可能成为其新的前任节点
        send_message(n_prime, n->ip, n->port, "NOTIFY");

        if (n->predecessor == NULL || is_in_interval(n->id, n->predecessor->id, n->id)) {
            n->predecessor = n_prime;
        }
    }

点击全屏/退出全屏

检查前驱()

// 检查前驱节点的状态

    // 定期检查。 
    n.check_predecessor()
    // 检查前驱是否已失效
    if 前驱已失效 then
        // 将前驱设置为 nil
        predecessor := nil

全屏模式,退出全屏

    void 检查前继(Node *n) {
        if (!n->前继) {
            return;
        }
        // 向前继发送心跳
        // 如果前继没回应,说明它挂了 -> 设为 NULL
        if (发送心跳(n, n->前继->ip, n->前继->port, "HEARTBEAT") < 0) {
            n->前继 = NULL;
        }
    }

全屏模式 退出全屏

完整日常

我建立了一个多线程环境(使用pthread API接口),并创建了一个每15秒运行一次此例程的线程,如官方Chord论文中的建议。

void *node_thread(void *arg) {
    Node *n = (Node *) arg;
    while (1) {
        stabilize(n); // 稳定节点
        fix_fingers(n, &(int){0}); // 修复手指
        check_predecessor(n); // 检查前驱
        sleep(15); // 暂停15秒
    }
}

全屏模式,退出全屏。

附加资源和其他参考资料

如果你想更深入地了解DHT和Chord算法是如何工作的,我可以给你推荐一些我发现特别有用的资源:

我是谁呀?

我是对 Web 开发感兴趣的意大利的高中生 🧙‍♂️。如果你想支持我,可以在这里和在我的 GitHub 关注我,我会非常感激你的支持 💜

Hack Club 的 Arcade 🟧

我想感谢Hack ClubGitHub Education,他们激励我们这些高中生编写代码,学习新事物,并一起创造令人惊叹的项目。如果你是一名高中生编程爱好者,我强烈建议你加入Hack Club,找到志同道合的人,并申请GitHub Education套餐,提升你的编程水平。我确信你不会后悔的!🚀

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
微信客服

购课补贴
联系客服咨询优惠详情

帮助反馈 APP下载

慕课网APP
您的移动学习伙伴

公众号

扫描二维码
关注慕课网微信公众号

举报

0/150
提交
取消