超硬核区块链共识算法仿真:教你C语言彻底学会PBFT算法仿真 (第一部分) 1

小引子:

 

PBFT算法仿真:从Win11到共识算法核心(上篇)

前言:告别“前端内卷”,拥抱“硬核共识”!

老铁们,如果你点开了这篇文章,那恭喜你,你绝对是想在技术领域“搞点大事情”的狠人!尤其是那些正在从“前端码农”转型“硬核嵌入式工程师”的兄弟们,你们的眼光太毒辣了!前端再怎么“卷”,也终究是“上层建筑”,而我们今天要“掀底裤”的,是支撑区块链、分布式系统乃至未来物联网核心的共识算法——真正的“地基”!

你是不是也曾被各种“区块链原理”、“共识机制”的概念搞得云里雾里?是不是觉得这些东西太抽象,离我们C语言程序员太远?是不是市面上那些课程,要么只讲概念,要么只给个Python玩具代码,根本无法满足我们这种“不把原理抠到汇编层面不罢休”的硬核需求?

别急!今天,我就带你用最熟悉的C语言,在你的Windows 11 VS Code环境下,亲手搭建一个PBFT(Practical Byzantine Fault Tolerance,实用拜占庭容错)算法仿真器!不光PBFT,我们还要一步步深入,摸透Credit-PBFT、Tree-PBFT、优化PBFT,甚至Raft和DPOS这些主流共识算法的“底细”!

这不只是一个技术教程,更是一场思维的洗礼!它将带你:

  • 彻底搞懂PBFT的每一个细节、每一个状态、每一条消息!

  • 掌握C语言在分布式系统仿真中的“骚操作”!

  • 理解共识算法如何解决分布式系统中的“拜占庭将军问题”!

  • 为你的嵌入式开发之路,打下最坚实的“分布式系统”底层基础!

这篇是上篇,咱们先从共识算法的背景、PBFT的核心概念和消息机制开刀,并搭建一个最基础的C语言仿真框架,让你亲手跑起来!

第一章:共识算法的“前世今生”——为什么我们需要它?

在正式“开撸”PBFT之前,我们得先搞清楚一个问题:共识算法到底是个啥?为什么它在分布式系统里这么重要?

1.1 什么是分布式系统?

简单来说,分布式系统就是多台计算机协同工作,共同完成一个任务的系统。比如,你访问一个大型网站,背后可能不是一台服务器在为你服务,而是成百上千台服务器共同协作。

分布式系统的特点:

  • 并发性: 多个组件同时运行。

  • 透明性: 用户感知不到底层是多台机器。

  • 可扩展性: 容易增加新的机器来提升性能。

  • 容错性: 即使部分机器宕机,系统也能继续运行。

  • 异构性: 不同的机器可能使用不同的硬件和操作系统。

1.2 分布式系统中的“老大难”问题:一致性

分布式系统最大的挑战之一就是一致性。想象一下,多台服务器同时处理你的订单,如果它们之间的数据不一致,那你的订单可能就会出错。

一致性: 指的是分布式系统中所有节点对某个数据或状态的看法保持一致。

为了实现一致性,就引出了各种共识算法

1.3 拜占庭将军问题:共识算法的“终极挑战”

在分布式系统中,最著名的难题就是拜占庭将军问题 (Byzantine Generals Problem)

问题描述:

想象一下,一群拜占庭将军围攻一座城池。他们需要达成一个共识:是进攻还是撤退

  • 将军们通过信使传递消息。

  • 有些将军可能是叛徒(拜占庭节点),他们可能会发送虚假消息,或者不发送消息。

  • 目标: 忠诚的将军们必须达成一致的行动(要么都进攻,要么都撤退),并且这个行动必须是忠诚将军们决定的。

难点:

  • 消息传递不可靠: 信使可能被截杀、消息可能被篡改。

  • 节点可能作恶: 叛徒将军会故意发送错误信息,试图破坏共识。

  • 无法确定消息来源的真实性: 你不知道收到的消息是不是叛徒发出的。

结论: 在存在叛徒(拜占庭故障)的情况下,如何让忠诚的将军们达成一致的行动,这就是拜占庭将军问题。

面试考点:

  • “什么是拜占庭将军问题?它在分布式系统中的意义是什么?” 这是共识算法的哲学起点,必考!

  • “拜占庭容错 (BFT) 和崩溃容错 (CFT) 有什么区别?”

    • CFT: 只能容忍节点崩溃(宕机,不响应)。

    • BFT: 还能容忍节点作恶(发送错误消息、不按协议执行)。BFT的容错能力更强。

1.4 CAP 定理:分布式系统设计的“三难选择”

在分布式系统设计中,CAP定理是一个绕不开的话题。

CAP定理: 在一个分布式系统中,你最多只能同时满足以下三点中的两点:

  • 一致性 (Consistency): 所有节点在同一时间看到的数据是一致的。

  • 可用性 (Availability): 系统总是能够响应请求,即使部分节点出现故障。

  • 分区容错性 (Partition Tolerance): 即使网络出现分区(节点之间无法通信),系统也能继续运行。

面试考点:

  • “什么是CAP定理?请解释C、A、P的含义。”

  • “在实际系统中,如何进行CAP的选择和权衡?”

    • CP系统: 追求强一致性和分区容错性,但在网络分区时可能牺牲可用性(例如,为了保证一致性,部分服务可能暂停)。PBFT就属于CP系统。

    • AP系统: 追求高可用性和分区容错性,但在网络分区时可能牺牲一致性(允许数据暂时不一致,待网络恢复后同步)。例如,DNS、一些NoSQL数据库。

    • CA系统: 追求强一致性和高可用性,但无法容忍网络分区(一旦分区就可能无法工作)。这在分布式系统中几乎不可能实现。

第二章:PBFT算法核心原理——拜占庭容错的“实用方案”

PBFT算法是第一个被广泛研究和实现的实用拜占庭容错算法。它在保证强一致性的同时,兼顾了性能。

2.1 PBFT算法概述

  • 全称: Practical Byzantine Fault Tolerance (实用拜占庭容错)。

  • 提出时间: 1999年由Miguel Castro和Barbara Liskov提出。

  • 特点:

    • 强一致性: 保证所有正常节点对同一操作达成一致。

    • 活性: 只要有足够多的正常节点,系统就能持续运行。

    • 高效: 相较于其他拜占庭容错算法,PBFT在正常情况下(无故障)的性能开销较小。

    • 状态机复制: PBFT通过复制状态机(State Machine Replication)来保证一致性。所有节点都维护一个相同的状态机,并按照相同的顺序执行客户端请求。

  • 容错能力: 能够容忍最多 (n-1)/3 个拜占庭节点,其中 n 是总节点数。也就是说,如果系统有 n 个节点,那么最多可以有 f 个恶意节点,且 n >= 3f + 1

面试考点:

  • “PBFT算法的容错能力是多少?为什么是 3f+1?” 这是一个高频考点,需要理解其数学原理。

2.2 PBFT算法角色

PBFT算法中,节点分为两种角色:

  • 主节点 (Primary): 负责接收客户端请求,并协调其他节点达成共识。在每个视图(View)中,只有一个主节点。主节点是轮换的,以防止单点故障。

  • 副本节点 (Backup/Replica): 接收主节点的消息,并与其他副本节点协作,共同验证和执行请求。

2.3 PBFT算法核心流程:三阶段提交

PBFT算法的核心是一个三阶段提交协议,用于在主节点和副本节点之间达成共识。

前提:

  • 视图 (View): PBFT算法在一个称为“视图”的阶段中运行。每个视图有一个唯一的主节点。如果主节点出现故障,系统会通过视图切换 (View Change) 机制选举新的主节点。

  • 序列号 (Sequence Number): 每个客户端请求都有一个唯一的序列号,确保请求按顺序执行。

  • 消息摘要 (Digest): 对消息内容进行哈希运算得到的一个固定长度的字符串,用于验证消息的完整性。

三阶段提交流程:

  1. 预准备阶段 (Pre-prepare Phase):

    • 发起者: 主节点。

    • 消息: <PRE-PREPARE, v, n, d, m>

      • v:当前视图编号。

      • n:请求的序列号。

      • d:客户端请求 m 的消息摘要。

      • m:客户端请求内容。

    • 流程:

      1. 主节点接收到客户端请求 m

      2. 主节点为请求分配一个序列号 n,计算消息摘要 d

      3. 主节点构建 PRE-PREPARE 消息,并广播给所有副本节点。

    • 副本节点处理: 副本节点接收到 PRE-PREPARE 消息后,会验证其合法性(如视图编号、序列号是否在有效范围内、消息摘要是否正确等)。如果验证通过,则进入准备阶段。

  2. 准备阶段 (Prepare Phase):

    • 发起者: 所有副本节点(包括主节点自身,但主节点无需发送)。

    • 消息: <PREPARE, v, n, d, i>

      • v:当前视图编号。

      • n:请求的序列号。

      • d:客户端请求 m 的消息摘要。

      • i:发送此消息的节点ID。

    • 流程:

      1. 每个副本节点收到有效的 PRE-PREPARE 消息后,会向所有其他节点广播 PREPARE 消息。

      2. 每个节点(包括主节点)会收集 PREPARE 消息。当一个节点收集到 2f 个来自不同节点PREPARE 消息(包括它自己发送的,如果它是副本节点)和主节点的 PRE-PREPARE 消息,并且这些消息的 vnd 都一致时,就认为自己进入了“准备好”状态。

    • 目的: 确保所有正常节点都同意请求的顺序(序列号)和内容。

  3. 提交阶段 (Commit Phase):

    • 发起者: 所有节点(包括主节点)。

    • 消息: <COMMIT, v, n, d, i>

      • v:当前视图编号。

      • n:请求的序列号。

      • d:客户端请求 m 的消息摘要。

      • i:发送此消息的节点ID。

    • 流程:

      1. 当一个节点进入“准备好”状态后,它会向所有其他节点广播 COMMIT 消息。

      2. 每个节点会收集 COMMIT 消息。当一个节点收集到 2f + 1 个来自不同节点COMMIT 消息(包括它自己发送的)并且这些消息的 vnd 都一致时,就认为自己进入了“提交好”状态。

    • 目的: 确保所有正常节点都同意执行该请求。一旦进入“提交好”状态,节点就可以安全地执行请求并将结果返回给客户端。

  4. 回复阶段 (Reply Phase):

    • 发起者: 所有进入“提交好”状态的节点。

    • 消息: <REPLY, v, n, r, i>

      • v:当前视图编号。

      • n:请求的序列号。

      • r:执行请求后的结果。

      • i:发送此消息的节点ID。

    • 流程:

      1. 每个进入“提交好”状态的节点执行客户端请求。

      2. 将执行结果 r 返回给客户端。

    • 客户端处理: 客户端会等待接收 f+1 个来自不同节点的相同 REPLY 消息,以确认请求已被多数正常节点执行并达成一致。

思维导图:PBFT三阶段提交流程

graph TD
    A[客户端发送请求] --> B{主节点接收请求}
    B --> C[主节点: PRE-PREPARE]
    C --广播--> D{副本节点接收PRE-PREPARE}
    D --> E[副本节点: PREPARE]
    E --广播--> F{所有节点收集PREPARE消息}
    F --收集到2f个PREPARE--> G[所有节点: COMMIT]
    G --广播--> H{所有节点收集COMMIT消息}
    H --收集到2f+1个COMMIT--> I[所有节点: 执行请求]
    I --> J[所有节点: REPLY]
    J --发送给客户端--> K{客户端收集f+1个REPLY}
    K --> L[客户端: 确认请求完成]

面试考点:

  • “详细描述PBFT的三阶段提交协议,包括每个阶段的消息类型和验证条件。” 这是PBFT的核心,必须烂熟于心。

  • “为什么PBFT需要三阶段而不是两阶段提交?” 为了解决拜占庭故障,确保即使有恶意节点,正常节点也能对消息的顺序达成一致。两阶段提交无法解决主节点作恶或消息丢失的问题。

  • “PBFT算法的活性和安全性是如何保证的?”

    • 安全性: 通过三阶段提交和 2f+1 多数确认,确保所有正常节点对请求的执行顺序和结果达成一致。

    • 活性: 通过视图切换机制,解决主节点故障或作恶导致的系统停滞。

2.4 视图切换 (View Change):解决主节点故障

如果主节点出现故障(宕机或作恶),导致无法正常推进共识,系统会触发视图切换。

  • 触发条件:

    • 副本节点长时间未收到主节点的 PRE-PREPARE 消息。

    • 副本节点收到无效的 PRE-PREPARE 消息。

    • 客户端长时间未收到 REPLY 消息。

  • 流程:

    1. 当一个副本节点认为主节点有问题时,它会启动一个计时器。

    2. 计时器超时后,该副本节点会向所有其他节点广播 VIEW-CHANGE 消息。

    3. 当一个节点收集到 2f+1 个来自不同节点的 VIEW-CHANGE 消息时,它就认为需要进行视图切换。

    4. 系统会根据预设的规则(例如,新主节点 = (新视图编号) % N),选举一个新的主节点。

    5. 新的主节点会收集所有 VIEW-CHANGE 消息,并发送 NEW-VIEW 消息给所有节点,其中包含新的视图编号和一些未完成请求的信息。

    6. 所有节点进入新的视图,并继续处理未完成的请求。

面试考点:

  • “PBFT的视图切换机制是如何工作的?为什么需要视图切换?”

  • “PBFT的性能瓶颈在哪里?” O(N^2) 的消息复杂度(N是节点数),导致节点数增加时性能急剧下降。

第三章:C语言PBFT仿真环境搭建——从零开始构建你的共识网络

现在,理论知识已经“武装到牙齿”了,是时候撸起袖子,用C语言把PBFT跑起来了!

为了在Windows 11的VS Code环境下方便地进行C语言网络编程,我强烈建议你使用WSL2 (Windows Subsystem for Linux 2)。它能让你在Windows下拥有一个完整的Linux环境,享受Linux下C语言开发和网络编程的便利,同时又能使用VS Code的强大功能。

如果你还没有WSL2,可以参考微软官方文档进行安装:安装 WSL | Microsoft Learn

安装好WSL2后,在WSL2中安装GCC、make等开发工具,并在VS Code中安装“Remote - WSL”扩展,你就可以在VS Code里直接开发Linux C程序了!

3.1 仿真架构设计:多进程/多线程模拟节点

PBFT算法涉及多个节点之间的通信。在C语言中,我们可以通过以下方式模拟这些节点:

  • 多进程 (Multi-process): 每个PBFT节点作为一个独立的进程。进程之间通过套接字 (Sockets) 进行网络通信。这是最接近真实分布式系统的方式,但进程间通信和管理相对复杂。

  • 多线程 (Multi-thread): 所有PBFT节点运行在同一个进程中,每个节点作为一个独立的线程。线程之间可以通过共享内存、消息队列等方式通信,也可以通过套接字进行通信。这种方式实现起来相对简单,但无法模拟真正的网络分区和独立进程故障。

  • 单进程模拟 (Single-process Simulation): 在一个进程中,通过事件循环和消息队列来模拟多个节点的消息处理。这种方式最简单,但与真实分布式系统的行为偏差较大。

考虑到PBFT的分布式特性,以及我们希望模拟真实的网络通信,我建议采用多进程的方式来模拟PBFT节点。每个节点都是一个独立的C程序,它们通过TCP套接字相互连接和通信。

仿真环境概览:

graph LR
    subgraph Windows 11 (VS Code)
        A[VS Code IDE] --> B[WSL2 Ubuntu Terminal]
    end

    subgraph WSL2 Ubuntu Environment
        C[Node 0 Process (Primary)]
        D[Node 1 Process (Backup)]
        E[Node 2 Process (Backup)]
        F[Node 3 Process (Backup)]
        G[Client Process]

        C --TCP Socket--> D
        C --TCP Socket--> E
        C --TCP Socket--> F
        D --TCP Socket--> E
        D --TCP Socket--> F
        E --TCP Socket--> F

        G --TCP Socket--> C
        G --TCP Socket--> D
        G --TCP Socket--> E
        G --TCP Socket--> F
    end

3.2 消息结构定义:PBFT通信的“语言”

PBFT算法中,节点之间传递的消息是核心。我们需要用C语言定义这些消息的结构体。

消息类型 (Opcode):

// 定义PBFT消息类型
typedef enum {
    MSG_TYPE_CLIENT_REQUEST, // 客户端请求
    MSG_TYPE_PRE_PREPARE,    // 预准备消息
    MSG_TYPE_PREPARE,        // 准备消息
    MSG_TYPE_COMMIT,         // 提交消息
    MSG_TYPE_REPLY,          // 回复消息
    MSG_TYPE_VIEW_CHANGE,    // 视图切换消息
    MSG_TYPE_NEW_VIEW        // 新视图消息
} MessageType;

消息头 (Common Header):

所有PBFT消息都需要包含一些公共信息,例如视图编号、序列号、发送者ID等。

#include <stdint.h> // For uint32_t, uint8_t

// 定义消息摘要长度 (例如,SHA256摘要是32字节)
#define DIGEST_SIZE 32
// 定义最大消息内容长度
#define MAX_MSG_CONTENT_SIZE 256

// 消息公共头部
typedef struct {
    MessageType type;      // 消息类型
    uint32_t view_id;      // 视图编号
    uint32_t sequence_num; // 序列号
    uint8_t sender_id;     // 发送者节点ID
} MessageHeader;

具体消息结构体:

我们将根据PBFT的协议定义,为每种消息类型创建对应的结构体。

  1. 客户端请求消息 (ClientRequest):

    • 客户端发送给主节点的请求。

    // 客户端请求消息
    typedef struct {
        MessageHeader header; // 通用消息头 (类型为MSG_TYPE_CLIENT_REQUEST)
        uint8_t client_id;    // 客户端ID
        uint32_t timestamp;   // 时间戳 (用于防止重放攻击)
        char operation[MAX_MSG_CONTENT_SIZE]; // 客户端请求的操作内容
        uint8_t digest[DIGEST_SIZE]; // 请求内容的摘要
    } ClientRequest;
    
    
  2. 预准备消息 (PrePrepareMsg):

    • 主节点发送给副本节点。

    // 预准备消息 (主节点广播给副本节点)
    typedef struct {
        MessageHeader header; // 通用消息头 (类型为MSG_TYPE_PRE_PREPARE)
        uint8_t client_id;    // 客户端ID (来自原始请求)
        uint32_t timestamp;   // 时间戳 (来自原始请求)
        char operation[MAX_MSG_CONTENT_SIZE]; // 客户端请求的操作内容
        // 注意:PBFT协议中,PRE-PREPARE消息通常包含原始请求的完整内容或其摘要
        // 这里为了简化,直接包含原始请求的operation和digest
        uint8_t request_digest[DIGEST_SIZE]; // 原始客户端请求的摘要
    } PrePrepareMsg;
    
    
  3. 准备消息 (PrepareMsg):

    • 副本节点(包括主节点自身)相互发送。

    // 准备消息 (所有节点相互发送)
    typedef struct {
        MessageHeader header; // 通用消息头 (类型为MSG_TYPE_PREPARE)
        uint8_t request_digest[DIGEST_SIZE]; // 原始客户端请求的摘要
    } PrepareMsg;
    
    
  4. 提交消息 (CommitMsg):

    • 所有节点相互发送。

    // 提交消息 (所有节点相互发送)
    typedef struct {
        MessageHeader header; // 通用消息头 (类型为MSG_TYPE_COMMIT)
        uint8_t request_digest[DIGEST_SIZE]; // 原始客户端请求的摘要
    } CommitMsg;
    
    
  5. 回复消息 (ReplyMsg):

    • 所有节点发送给客户端。

    // 回复消息 (节点发送给客户端)
    typedef struct {
        MessageHeader header; // 通用消息头 (类型为MSG_TYPE_REPLY)
        uint8_t client_id;    // 客户端ID
        uint32_t timestamp;   // 时间戳 (来自原始请求)
        char result[MAX_MSG_CONTENT_SIZE]; // 执行请求后的结果
    } ReplyMsg;
    
    

统一消息结构体:

为了方便网络传输和解析,我们可以使用一个共用体(union)将所有消息类型封装到一个统一的结构体中。

// 统一消息结构体 (使用union节省内存,一次只存储一种消息)
typedef struct {
    MessageHeader common_header; // 所有消息共有的头部信息
    union {
        ClientRequest client_request;
        PrePrepareMsg pre_prepare;
        PrepareMsg prepare;
        CommitMsg commit;
        ReplyMsg reply;
        // 视图切换和新视图消息暂时省略,后续添加
    } payload; // 消息的具体内容
} PBFTMessage;

面试考点:

  • “请设计PBFT算法中的消息结构体。” 考察你对协议细节的理解和C语言结构体的使用。

  • “为什么需要消息摘要?它是如何保证消息完整性的?” 防止消息被篡改。

  • “视图编号和序列号的作用?” 视图编号用于标识主节点,序列号用于保证请求顺序。

3.3 网络通信基础:TCP套接字编程

在C语言中进行网络通信,最常用的是套接字 (Socket) 编程。PBFT算法需要可靠的连接来传输消息,因此我们选择TCP (Transmission Control Protocol) 套接字。

TCP套接字特点:

  • 面向连接: 在数据传输前需要建立连接。

  • 可靠传输: 保证数据不丢失、不重复、按序到达。

  • 流量控制和拥塞控制: 确保发送方不会压垮接收方,也不会导致网络拥塞。

核心API (Linux/WSL2环境):

  • socket() 创建套接字。

  • bind() 将套接字绑定到本地IP地址和端口。

  • listen() 服务器端套接字进入监听状态,等待客户端连接。

  • accept() 服务器端接受客户端连接,返回一个新的套接字文件描述符用于与客户端通信。

  • connect() 客户端套接字连接到服务器。

  • send() / write() 发送数据。

  • recv() / read() 接收数据。

  • close() 关闭套接字。

C语言代码:基础TCP通信(服务端与客户端)

我们先搭建一个最简单的TCP服务器和客户端,来验证网络通信的基础能力。

文件:common.h (公共头文件,定义消息结构和一些常量)

#ifndef COMMON_H
#define COMMON_H

#include <stdint.h> // For uint32_t, uint8_t
#include <stdbool.h> // For bool

// 定义PBFT系统中的节点总数 (N) 和可容忍的拜占庭节点数 (f)
// N = 3f + 1
#define NUM_NODES 4 // 例如,4个节点,f = (4-1)/3 = 1,可容忍1个拜占庭节点
#define FAULTY_NODES ((NUM_NODES - 1) / 3) // f

// 定义消息摘要长度 (例如,SHA256摘要是32字节)
#define DIGEST_SIZE 32
// 定义最大消息内容长度
#define MAX_MSG_CONTENT_SIZE 256
// 定义每个节点的监听端口
#define BASE_NODE_PORT 8000 // Node 0 监听 8000, Node 1 监听 8001, ...
// 定义客户端监听端口 (用于接收Reply)
#define CLIENT_PORT 9000

// 定义PBFT消息类型
typedef enum {
    MSG_TYPE_CLIENT_REQUEST, // 客户端请求
    MSG_TYPE_PRE_PREPARE,    // 预准备消息
    MSG_TYPE_PREPARE,        // 准备消息
    MSG_TYPE_COMMIT,         // 提交消息
    MSG_TYPE_REPLY,          // 回复消息
    MSG_TYPE_VIEW_CHANGE,    // 视图切换消息
    MSG_TYPE_NEW_VIEW,       // 新视图消息
    MSG_TYPE_HEARTBEAT       // 心跳消息 (用于检测节点存活)
} MessageType;

// 消息公共头部
typedef struct {
    MessageType type;      // 消息类型
    uint32_t view_id;      // 视图编号
    uint32_t sequence_num; // 序列号
    uint8_t sender_id;     // 发送者节点ID
} MessageHeader;

// 客户端请求消息 (由客户端发送给主节点)
typedef struct {
    MessageHeader header; // 通用消息头 (type = MSG_TYPE_CLIENT_REQUEST)
    uint8_t client_id;    // 客户端ID
    uint32_t timestamp;   // 时间戳 (用于防止重放攻击)
    char operation[MAX_MSG_CONTENT_SIZE]; // 客户端请求的操作内容 (例如 "transfer 100 to Bob")
    uint8_t digest[DIGEST_SIZE]; // 请求内容的摘要 (SHA256(operation))
} ClientRequest;

// 预准备消息 (由主节点广播给副本节点)
typedef struct {
    MessageHeader header; // 通用消息头 (type = MSG_TYPE_PRE_PREPARE)
    uint8_t client_id;    // 原始客户端ID
    uint32_t timestamp;   // 原始客户端请求时间戳
    char operation[MAX_MSG_CONTENT_SIZE]; // 原始客户端请求的操作内容
    uint8_t request_digest[DIGEST_SIZE]; // 原始客户端请求的摘要
    // 实际PBFT中,这里通常只包含原始请求的摘要,而不是整个操作内容
    // 为了简化仿真,我们直接包含操作内容,方便调试
} PrePrepareMsg;

// 准备消息 (由所有节点相互发送)
typedef struct {
    MessageHeader header; // 通用消息头 (type = MSG_TYPE_PREPARE)
    uint8_t request_digest[DIGEST_SIZE]; // 原始客户端请求的摘要
} PrepareMsg;

// 提交消息 (由所有节点相互发送)
typedef struct {
    MessageHeader header; // 通用消息头 (type = MSG_TYPE_COMMIT)
    uint8_t request_digest[DIGEST_SIZE]; // 原始客户端请求的摘要
} CommitMsg;

// 回复消息 (由节点发送给客户端)
typedef struct {
    MessageHeader header; // 通用消息头 (type = MSG_TYPE_REPLY)
    uint8_t client_id;    // 客户端ID
    uint32_t timestamp;   // 时间戳 (来自原始请求)
    char result[MAX_MSG_CONTENT_SIZE]; // 执行请求后的结果 (例如 "OK" 或 "Error")
} ReplyMsg;

// 统一消息结构体 (使用union节省内存,一次只存储一种消息)
// 方便网络传输和处理
typedef struct {
    MessageHeader common_header; // 所有消息共有的头部信息
    union {
        ClientRequest client_request;
        PrePrepareMsg pre_prepare;
        PrepareMsg prepare;
        CommitMsg commit;
        ReplyMsg reply;
        // 视图切换和新视图消息暂时省略,后续添加
    } payload; // 消息的具体内容
} PBFTMessage;

// --- 辅助函数声明 ---
// 计算消息摘要 (这里简化为简单的哈希,实际应使用SHA256等)
void calculate_digest(const char* data, size_t len, uint8_t* digest_out);

// 打印消息内容 (用于调试)
void print_message(const PBFTMessage* msg);

#endif // COMMON_H

文件:utils.c (辅助函数实现)

#include "common.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h> // For exit

// 简化版摘要计算 (实际应使用SHA256等加密哈希函数)
void calculate_digest(const char* data, size_t len, uint8_t* digest_out) {
    // 这里只是一个非常简单的模拟摘要,实际应用中绝对不能这样用!
    // 实际应使用如 OpenSSL 库中的 SHA256 函数
    uint32_t hash = 0;
    for (size_t i = 0; i < len; ++i) {
        hash = (hash * 31) + data[i]; // 简单的乘法哈希
    }
    // 将32位哈希值填充到DIGEST_SIZE字节的数组中
    memset(digest_out, 0, DIGEST_SIZE); // 先清零
    memcpy(digest_out, &hash, sizeof(uint32_t)); // 复制哈希值
}

// 打印消息内容 (用于调试)
void print_message(const PBFTMessage* msg) {
    if (!msg) {
        printf("空消息指针。\n");
        return;
    }

    printf("--- 消息详情 ---\n");
    printf("  类型: ");
    switch (msg->common_header.type) {
        case MSG_TYPE_CLIENT_REQUEST: printf("CLIENT_REQUEST\n"); break;
        case MSG_TYPE_PRE_PREPARE:    printf("PRE_PREPARE\n"); break;
        case MSG_TYPE_PREPARE:        printf("PREPARE\n"); break;
        case MSG_TYPE_COMMIT:         printf("COMMIT\n"); break;
        case MSG_TYPE_REPLY:          printf("REPLY\n"); break;
        case MSG_TYPE_VIEW_CHANGE:    printf("VIEW_CHANGE\n"); break;
        case MSG_TYPE_NEW_VIEW:       printf("NEW_VIEW\n"); break;
        case MSG_TYPE_HEARTBEAT:      printf("HEARTBEAT\n"); break;
        default:                      printf("未知类型\n"); break;
    }
    printf("  视图ID: %u\n", msg->common_header.view_id);
    printf("  序列号: %u\n", msg->common_header.sequence_num);
    printf("  发送者ID: %u\n", msg->common_header.sender_id);

    // 根据消息类型打印具体内容
    switch (msg->common_header.type) {
        case MSG_TYPE_CLIENT_REQUEST:
            printf("  客户端ID: %u\n", msg->payload.client_request.client_id);
            printf("  时间戳: %u\n", msg->payload.client_request.timestamp);
            printf("  操作: %s\n", msg->payload.client_request.operation);
            printf("  摘要: ");
            for (int i = 0; i < DIGEST_SIZE; ++i) {
                printf("%02x", msg->payload.client_request.digest[i]);
            }
            printf("\n");
            break;
        case MSG_TYPE_PRE_PREPARE:
            printf("  原始客户端ID: %u\n", msg->payload.pre_prepare.client_id);
            printf("  原始时间戳: %u\n", msg->payload.pre_prepare.timestamp);
            printf("  操作: %s\n", msg->payload.pre_prepare.operation);
            printf("  请求摘要: ");
            for (int i = 0; i < DIGEST_SIZE; ++i) {
                printf("%02x", msg->payload.pre_prepare.request_digest[i]);
            }
            printf("\n");
            break;
        case MSG_TYPE_PREPARE:
        case MSG_TYPE_COMMIT:
            printf("  请求摘要: ");
            for (int i = 0; i < DIGEST_SIZE; ++i) {
                printf("%02x", msg->payload.prepare.request_digest[i]); // prepare和commit结构体中摘要字段相同
            }
            printf("\n");
            break;
        case MSG_TYPE_REPLY:
            printf("  客户端ID: %u\n", msg->payload.reply.client_id);
            printf("  时间戳: %u\n", msg->payload.reply.timestamp);
            printf("  结果: %s\n", msg->payload.reply.result);
            break;
        default:
            // 其他消息类型暂不详细打印
            break;
    }
    printf("------------------\n");
}

文件:node.c (PBFT节点代码,包含服务端和客户端逻辑)

#include "common.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>     // For close(), read(), write()
#include <arpa/inet.h>  // For inet_ntop(), inet_pton()
#include <sys/socket.h> // For socket(), bind(), listen(), accept(), connect(), send(), recv()
#include <pthread.h>    // For pthread_create(), pthread_join()
#include <errno.h>      // For errno

// --- 节点状态信息 (简化) ---
typedef struct {
    uint8_t node_id;         // 当前节点的ID
    uint32_t current_view;   // 当前视图编号
    uint32_t last_seq_num;   // 上一个已提交的序列号
    bool is_primary;         // 是否是主节点
    // 实际PBFT节点状态会复杂得多,包括消息日志、高水位、低水位等
} NodeState;

NodeState g_node_state; // 全局节点状态

// --- 线程参数结构体 ---
typedef struct {
    int client_sock_fd; // 客户端连接的套接字
    uint8_t remote_node_id; // 远程节点的ID
} ThreadArgs;

// --- 辅助函数声明 ---
void *handle_incoming_connection(void *arg); // 处理传入连接的线程函数
void connect_to_peers(uint8_t my_id); // 连接到其他节点
void send_message_to_node(uint8_t target_node_id, const PBFTMessage* msg); // 发送消息给指定节点

// --- 全局套接字数组,用于存储与其他节点的连接 ---
// conn_sockets[i] 存储与节点i的连接
// 如果 conn_sockets[i] > 0,表示已连接
int conn_sockets[NUM_NODES];

// --- 互斥锁,保护全局变量和共享资源 ---
pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER;

/**
 * @brief 节点启动函数
 * @param node_id 当前节点的ID
 */
void start_node(uint8_t node_id) {
    g_node_state.node_id = node_id;
    g_node_state.current_view = 0; // 初始视图为0
    g_node_state.last_seq_num = 0;
    g_node_state.is_primary = (node_id == (g_node_state.current_view % NUM_NODES)); // 初始主节点是Node 0

    printf("[Node %u] 启动中... 端口: %d\n", g_node_state.node_id, BASE_NODE_PORT + g_node_state.node_id);
    if (g_node_state.is_primary) {
        printf("[Node %u] 当前是主节点。\n", g_node_state.node_id);
    } else {
        printf("[Node %u] 当前是副本节点。\n", g_node_state.node_id);
    }

    // 初始化连接套接字数组
    for (int i = 0; i < NUM_NODES; ++i) {
        conn_sockets[i] = -1; // -1 表示未连接
    }

    // 1. 启动监听线程 (作为服务器端,接收来自其他节点的连接)
    int listen_sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_sock_fd == -1) {
        perror("[Node] 创建监听套接字失败");
        exit(EXIT_FAILURE);
    }

    // 允许端口重用 (解决 Address already in use 问题)
    int optval = 1;
    if (setsockopt(listen_sock_fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) == -1) {
        perror("[Node] setsockopt SO_REUSEADDR 失败");
        close(listen_sock_fd);
        exit(EXIT_FAILURE);
    }

    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(BASE_NODE_PORT + g_node_state.node_id);
    server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有可用IP地址

    if (bind(listen_sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("[Node] 绑定监听地址失败");
        close(listen_sock_fd);
        exit(EXIT_FAILURE);
    }

    if (listen(listen_sock_fd, NUM_NODES) == -1) { // 监听队列长度为节点总数
        perror("[Node] 监听失败");
        close(listen_sock_fd);
        exit(EXIT_FAILURE);
    }

    printf("[Node %u] 正在监听连接...\n", g_node_state.node_id);

    // 启动一个线程来处理传入连接
    pthread_t listen_thread;
    if (pthread_create(&listen_thread, NULL, handle_incoming_connection, (void*)(long)listen_sock_fd) != 0) {
        perror("[Node] 创建监听线程失败");
        close(listen_sock_fd);
        exit(EXIT_FAILURE);
    }

    // 2. 连接到其他节点 (作为客户端,主动建立连接)
    // 延迟一小段时间,确保其他节点的服务端已启动并监听
    sleep(1);
    connect_to_peers(g_node_state.node_id);

    // 3. 进入主循环,处理PBFT逻辑 (这里只是一个占位符)
    printf("[Node %u] 进入主循环,等待消息...\n", g_node_state.node_id);
    while (1) {
        // 在这里实现PBFT算法的逻辑
        // 例如:主节点等待客户端请求,副本节点等待PRE-PREPARE消息
        // 接收到消息后,进行验证,状态转换,广播下一阶段消息等
        // 暂时只是一个空循环,等待消息处理线程工作
        sleep(10); // 避免CPU空转
    }

    // 等待监听线程结束 (实际不会结束,除非程序退出)
    pthread_join(listen_thread, NULL);
    close(listen_sock_fd);
}

/**
 * @brief 处理传入连接的线程函数 (每个传入连接会创建一个新线程来处理)
 * @param arg 监听套接字文件描述符
 */
void *handle_incoming_connection(void *arg) {
    int listen_sock_fd = (int)(long)arg;
    struct sockaddr_in remote_addr;
    socklen_t addr_len = sizeof(remote_addr);
    char remote_ip[INET_ADDRSTRLEN];

    while (1) {
        int client_sock_fd = accept(listen_sock_fd, (struct sockaddr*)&remote_addr, &addr_len);
        if (client_sock_fd == -1) {
            if (errno == EINTR) { // 被信号中断,继续
                continue;
            }
            perror("[Node] 接受连接失败");
            break;
        }

        inet_ntop(AF_INET, &(remote_addr.sin_addr), remote_ip, INET_ADDRSTRLEN);
        printf("[Node %u] 接收到来自 %s:%d 的连接。\n", g_node_state.node_id, remote_ip, ntohs(remote_addr.sin_port));

        // 识别连接的远程节点ID (通过端口号推断,非常简化)
        // 实际PBFT中,节点会通过握手协议交换ID
        uint8_t remote_node_id = (ntohs(remote_addr.sin_port) - BASE_NODE_PORT);
        if (remote_node_id >= NUM_NODES) { // 可能是客户端或其他未知连接
            printf("[Node %u] 警告: 无法识别远程节点ID %u,可能不是PBFT节点连接。\n", g_node_state.node_id, remote_node_id);
            // 对于客户端连接,我们可能需要单独处理
            // 这里为了简化,我们假设所有连接都是PBFT节点之间的连接
            // 客户端连接将在client.c中单独处理
            close(client_sock_fd);
            continue;
        }

        pthread_mutex_lock(&g_mutex);
        conn_sockets[remote_node_id] = client_sock_fd; // 存储连接套接字
        pthread_mutex_unlock(&g_mutex);

        printf("[Node %u] 已与 Node %u 建立连接 (作为服务器端)。\n", g_node_state.node_id, remote_node_id);

        // 为每个连接启动一个独立的线程来接收消息
        pthread_t recv_thread;
        ThreadArgs* args = (ThreadArgs*)malloc(sizeof(ThreadArgs));
        if (args == NULL) {
            perror("分配线程参数失败");
            close(client_sock_fd);
            continue;
        }
        args->client_sock_fd = client_sock_fd;
        args->remote_node_id = remote_node_id;

        if (pthread_create(&recv_thread, NULL, (void *(*)(void *))recv_message_thread, (void*)args) != 0) {
            perror("[Node] 创建接收消息线程失败");
            free(args);
            close(client_sock_
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值