小引子:

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): 对消息内容进行哈希运算得到的一个固定长度的字符串,用于验证消息的完整性。
三阶段提交流程:
-
预准备阶段 (Pre-prepare Phase):
-
发起者: 主节点。
-
消息:
<PRE-PREPARE, v, n, d, m>-
v:当前视图编号。 -
n:请求的序列号。 -
d:客户端请求m的消息摘要。 -
m:客户端请求内容。
-
-
流程:
-
主节点接收到客户端请求
m。 -
主节点为请求分配一个序列号
n,计算消息摘要d。 -
主节点构建
PRE-PREPARE消息,并广播给所有副本节点。
-
-
副本节点处理: 副本节点接收到
PRE-PREPARE消息后,会验证其合法性(如视图编号、序列号是否在有效范围内、消息摘要是否正确等)。如果验证通过,则进入准备阶段。
-
-
准备阶段 (Prepare Phase):
-
发起者: 所有副本节点(包括主节点自身,但主节点无需发送)。
-
消息:
<PREPARE, v, n, d, i>-
v:当前视图编号。 -
n:请求的序列号。 -
d:客户端请求m的消息摘要。 -
i:发送此消息的节点ID。
-
-
流程:
-
每个副本节点收到有效的
PRE-PREPARE消息后,会向所有其他节点广播PREPARE消息。 -
每个节点(包括主节点)会收集
PREPARE消息。当一个节点收集到2f个来自不同节点的PREPARE消息(包括它自己发送的,如果它是副本节点)和主节点的PRE-PREPARE消息,并且这些消息的v、n、d都一致时,就认为自己进入了“准备好”状态。
-
-
目的: 确保所有正常节点都同意请求的顺序(序列号)和内容。
-
-
提交阶段 (Commit Phase):
-
发起者: 所有节点(包括主节点)。
-
消息:
<COMMIT, v, n, d, i>-
v:当前视图编号。 -
n:请求的序列号。 -
d:客户端请求m的消息摘要。 -
i:发送此消息的节点ID。
-
-
流程:
-
当一个节点进入“准备好”状态后,它会向所有其他节点广播
COMMIT消息。 -
每个节点会收集
COMMIT消息。当一个节点收集到2f + 1个来自不同节点的COMMIT消息(包括它自己发送的)并且这些消息的v、n、d都一致时,就认为自己进入了“提交好”状态。
-
-
目的: 确保所有正常节点都同意执行该请求。一旦进入“提交好”状态,节点就可以安全地执行请求并将结果返回给客户端。
-
-
回复阶段 (Reply Phase):
-
发起者: 所有进入“提交好”状态的节点。
-
消息:
<REPLY, v, n, r, i>-
v:当前视图编号。 -
n:请求的序列号。 -
r:执行请求后的结果。 -
i:发送此消息的节点ID。
-
-
流程:
-
每个进入“提交好”状态的节点执行客户端请求。
-
将执行结果
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消息。
-
-
流程:
-
当一个副本节点认为主节点有问题时,它会启动一个计时器。
-
计时器超时后,该副本节点会向所有其他节点广播
VIEW-CHANGE消息。 -
当一个节点收集到
2f+1个来自不同节点的VIEW-CHANGE消息时,它就认为需要进行视图切换。 -
系统会根据预设的规则(例如,
新主节点 = (新视图编号) % N),选举一个新的主节点。 -
新的主节点会收集所有
VIEW-CHANGE消息,并发送NEW-VIEW消息给所有节点,其中包含新的视图编号和一些未完成请求的信息。 -
所有节点进入新的视图,并继续处理未完成的请求。
-
面试考点:
-
“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的协议定义,为每种消息类型创建对应的结构体。
-
客户端请求消息 (
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; -
-
预准备消息 (
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; -
-
准备消息 (
PrepareMsg):-
副本节点(包括主节点自身)相互发送。
// 准备消息 (所有节点相互发送) typedef struct { MessageHeader header; // 通用消息头 (类型为MSG_TYPE_PREPARE) uint8_t request_digest[DIGEST_SIZE]; // 原始客户端请求的摘要 } PrepareMsg; -
-
提交消息 (
CommitMsg):-
所有节点相互发送。
// 提交消息 (所有节点相互发送) typedef struct { MessageHeader header; // 通用消息头 (类型为MSG_TYPE_COMMIT) uint8_t request_digest[DIGEST_SIZE]; // 原始客户端请求的摘要 } CommitMsg; -
-
回复消息 (
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_

最低0.47元/天 解锁文章
619

被折叠的 条评论
为什么被折叠?



