小引子:
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_fd);
continue;
}
pthread_detach(recv_thread); // 将线程设置为分离状态,资源在结束时自动回收
}
return NULL;
}
/**
* @brief 连接到其他节点的函数 (作为客户端)
* @param my_id 当前节点的ID
*/
void connect_to_peers(uint8_t my_id) {
for (uint8_t i = 0; i < NUM_NODES; ++i) {
if (i == my_id) {
continue; // 不连接自己
}
// 避免重复连接:如果已经有连接,则跳过
pthread_mutex_lock(&g_mutex);
if (conn_sockets[i] != -1) {
pthread_mutex_unlock(&g_mutex);
printf("[Node %u] 已与 Node %u 连接,跳过。\n", my_id, i);
continue;
}
pthread_mutex_unlock(&g_mutex);
int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (sock_fd == -1) {
perror("[Node] 创建连接套接字失败");
continue;
}
struct sockaddr_in peer_addr;
memset(&peer_addr, 0, sizeof(peer_addr));
peer_addr.sin_family = AF_INET;
peer_addr.sin_port = htons(BASE_NODE_PORT + i);
// 连接到本地IP (127.0.0.1),因为所有节点都在同一台机器上模拟
if (inet_pton(AF_INET, "127.0.0.1", &peer_addr.sin_addr) <= 0) {
perror("[Node] 无效的对端IP地址");
close(sock_fd);
continue;
}
printf("[Node %u] 尝试连接到 Node %u (端口: %d)...\n", my_id, i, BASE_NODE_PORT + i);
// 尝试连接,如果失败(如对端未启动),等待并重试
int retry_count = 0;
const int MAX_CONNECT_RETRIES = 5;
while (connect(sock_fd, (struct sockaddr*)&peer_addr, sizeof(peer_addr)) == -1) {
if (errno == ECONNREFUSED && retry_count < MAX_CONNECT_RETRIES) {
printf("[Node %u] 连接 Node %u 失败 (连接被拒绝),重试中... (%d/%d)\n", my_id, i, retry_count + 1, MAX_CONNECT_RETRIES);
sleep(1); // 等待1秒后重试
retry_count++;
} else {
perror("[Node] 连接对端失败");
close(sock_fd);
sock_fd = -1; // 标记连接失败
break;
}
}
if (sock_fd != -1) {
pthread_mutex_lock(&g_mutex);
conn_sockets[i] = sock_fd; // 存储连接套接字
pthread_mutex_unlock(&g_mutex);
printf("[Node %u] 已与 Node %u 建立连接 (作为客户端)。\n", my_id, i);
// 为每个连接启动一个独立的线程来接收消息
pthread_t recv_thread;
ThreadArgs* args = (ThreadArgs*)malloc(sizeof(ThreadArgs));
if (args == NULL) {
perror("分配线程参数失败");
close(sock_fd);
continue;
}
args->client_sock_fd = sock_fd;
args->remote_node_id = i;
if (pthread_create(&recv_thread, NULL, (void *(*)(void *))recv_message_thread, (void*)args) != 0) {
perror("[Node] 创建接收消息线程失败");
free(args);
close(sock_fd);
continue;
}
pthread_detach(recv_thread); // 将线程设置为分离状态
}
}
}
/**
* @brief 接收消息的线程函数
* @param arg 线程参数 (包含套接字文件描述符和远程节点ID)
*/
void *recv_message_thread(void *arg) {
ThreadArgs* args = (ThreadArgs*)arg;
int sock_fd = args->client_sock_fd;
uint8_t remote_node_id = args->remote_node_id;
free(args); // 释放线程参数内存
PBFTMessage msg_buffer;
ssize_t bytes_received;
printf("[Node %u] 接收线程启动,监听来自 Node %u 的消息...\n", g_node_state.node_id, remote_node_id);
while (1) {
// 接收消息头部
bytes_received = recv(sock_fd, &msg_buffer.common_header, sizeof(MessageHeader), MSG_WAITALL);
if (bytes_received <= 0) {
if (bytes_received == 0) {
printf("[Node %u] Node %u 断开连接。\n", g_node_state.node_id, remote_node_id);
} else {
perror("[Node] 接收消息头部失败");
}
break; // 连接断开或出错
}
// 根据消息类型接收剩余的payload
size_t payload_size = 0;
switch (msg_buffer.common_header.type) {
case MSG_TYPE_CLIENT_REQUEST: payload_size = sizeof(ClientRequest) - sizeof(MessageHeader); break;
case MSG_TYPE_PRE_PREPARE: payload_size = sizeof(PrePrepareMsg) - sizeof(MessageHeader); break;
case MSG_TYPE_PREPARE: payload_size = sizeof(PrepareMsg) - sizeof(MessageHeader); break;
case MSG_TYPE_COMMIT: payload_size = sizeof(CommitMsg) - sizeof(MessageHeader); break;
case MSG_TYPE_REPLY: payload_size = sizeof(ReplyMsg) - sizeof(MessageHeader); break;
// 其他消息类型待添加
default:
fprintf(stderr, "[Node %u] 接收到未知消息类型 %u,跳过payload接收。\n", g_node_state.node_id, msg_buffer.common_header.type);
break;
}
if (payload_size > 0) {
bytes_received = recv(sock_fd, &msg_buffer.payload, payload_size, MSG_WAITALL);
if (bytes_received <= 0) {
if (bytes_received == 0) {
printf("[Node %u] Node %u 断开连接 (接收payload时)。\n", g_node_state.node_id, remote_node_id);
} else {
perror("[Node] 接收消息payload失败");
}
break;
}
if (bytes_received != payload_size) {
fprintf(stderr, "[Node %u] 接收payload长度不匹配!期望 %zu, 实际 %zd。\n", g_node_state.node_id, payload_size, bytes_received);
break;
}
}
// 成功接收一条完整的消息
printf("[Node %u] 收到来自 Node %u 的消息:\n", g_node_state.node_id, msg_buffer.common_header.sender_id);
print_message(&msg_buffer);
// --- 在这里添加PBFT消息处理逻辑 ---
// 例如:
// if (msg_buffer.common_header.type == MSG_TYPE_PRE_PREPARE) {
// // 验证消息,如果通过,广播PREPARE消息
// }
// else if (msg_buffer.common_header.type == MSG_TYPE_PREPARE) {
// // 收集PREPARE消息,检查是否达到2f个,如果达到,广播COMMIT消息
// }
}
// 连接断开,清理资源
pthread_mutex_lock(&g_mutex);
if (conn_sockets[remote_node_id] == sock_fd) { // 确保是当前连接
conn_sockets[remote_node_id] = -1;
}
pthread_mutex_unlock(&g_mutex);
close(sock_fd);
printf("[Node %u] 与 Node %u 的连接已关闭。\n", g_node_state.node_id, remote_node_id);
return NULL;
}
/**
* @brief 发送消息给指定节点
* @param target_node_id 目标节点ID
* @param msg 要发送的消息指针
*/
void send_message_to_node(uint8_t target_node_id, const PBFTMessage* msg) {
pthread_mutex_lock(&g_mutex);
int sock_fd = conn_sockets[target_node_id];
pthread_mutex_unlock(&g_mutex);
if (sock_fd == -1) {
fprintf(stderr, "[Node %u] 错误: 无法发送消息给 Node %u,连接未建立或已断开。\n", g_node_state.node_id, target_node_id);
return;
}
// 计算要发送的消息总长度
size_t total_msg_len = sizeof(MessageHeader);
switch (msg->common_header.type) {
case MSG_TYPE_CLIENT_REQUEST: total_msg_len += sizeof(ClientRequest) - sizeof(MessageHeader); break;
case MSG_TYPE_PRE_PREPARE: total_msg_len += sizeof(PrePrepareMsg) - sizeof(MessageHeader); break;
case MSG_TYPE_PREPARE: total_msg_len += sizeof(PrepareMsg) - sizeof(MessageHeader); break;
case MSG_TYPE_COMMIT: total_msg_len += sizeof(CommitMsg) - sizeof(MessageHeader); break;
case MSG_TYPE_REPLY: total_msg_len += sizeof(ReplyMsg) - sizeof(MessageHeader); break;
default:
fprintf(stderr, "[Node %u] 错误: 尝试发送未知消息类型 %u。\n", g_node_state.node_id, msg->common_header.type);
return;
}
// 发送消息
ssize_t bytes_sent = send(sock_fd, msg, total_msg_len, 0);
if (bytes_sent == -1) {
perror("[Node] 发送消息失败");
} else if (bytes_sent != total_msg_len) {
fprintf(stderr, "[Node %u] 警告: 发送消息长度不匹配!期望 %zu, 实际 %zd。\n", g_node_state.node_id, total_msg_len, bytes_sent);
} else {
printf("[Node %u] 成功发送消息 (类型: %u, 序列号: %u) 给 Node %u。\n",
g_node_state.node_id, msg->common_header.type, msg->common_header.sequence_num, target_node_id);
}
}
int main(int argc, char* argv[]) {
if (argc != 2) {
fprintf(stderr, "用法: %s <node_id>\n", argv[0]);
fprintf(stderr, "示例: %s 0 (启动Node 0)\n", argv[0]);
return 1;
}
uint8_t node_id = (uint8_t)atoi(argv[1]);
if (node_id >= NUM_NODES) {
fprintf(stderr, "错误: 节点ID必须小于 %d。\n", NUM_NODES);
return 1;
}
start_node(node_id);
return 0;
}
文件:client.c
(客户端代码)
#include "common.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <time.h> // For time()
// --- 客户端状态信息 (简化) ---
typedef struct {
uint8_t client_id;
// 实际客户端状态会包含已发送请求的记录,以及等待的回复等
} ClientState;
ClientState g_client_state;
// --- 辅助函数声明 ---
void send_client_request(const char* operation);
void *receive_reply_thread(void *arg); // 接收回复的线程函数
int main(int argc, char* argv[]) {
if (argc != 2) {
fprintf(stderr, "用法: %s <client_id>\n", argv[0]);
fprintf(stderr, "示例: %s 100 (启动Client 100)\n", argv[0]);
return 1;
}
g_client_state.client_id = (uint8_t)atoi(argv[1]);
printf("[Client %u] 启动中...\n", g_client_state.client_id);
// 1. 启动一个线程来接收来自节点的回复 (作为服务器端)
int listen_sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock_fd == -1) {
perror("[Client] 创建监听套接字失败");
exit(EXIT_FAILURE);
}
int optval = 1;
if (setsockopt(listen_sock_fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) == -1) {
perror("[Client] setsockopt SO_REUSEADDR 失败");
close(listen_sock_fd);
exit(EXIT_FAILURE);
}
struct sockaddr_in client_listen_addr;
memset(&client_listen_addr, 0, sizeof(client_listen_addr));
client_listen_addr.sin_family = AF_INET;
client_listen_addr.sin_port = htons(CLIENT_PORT + g_client_state.client_id); // 每个客户端监听不同端口
client_listen_addr.sin_addr.s_addr = INADDR_ANY;
if (bind(listen_sock_fd, (struct sockaddr*)&client_listen_addr, sizeof(client_listen_addr)) == -1) {
perror("[Client] 绑定监听地址失败");
close(listen_sock_fd);
exit(EXIT_FAILURE);
}
if (listen(listen_sock_fd, NUM_NODES) == -1) { // 监听节点数量
perror("[Client] 监听失败");
close(listen_sock_fd);
exit(EXIT_FAILURE);
}
printf("[Client %u] 正在监听回复 (端口: %d)...\n", g_client_state.client_id, CLIENT_PORT + g_client_state.client_id);
pthread_t reply_recv_thread;
if (pthread_create(&reply_recv_thread, NULL, receive_reply_thread, (void*)(long)listen_sock_fd) != 0) {
perror("[Client] 创建回复接收线程失败");
close(listen_sock_fd);
exit(EXIT_FAILURE);
}
pthread_detach(reply_recv_thread); // 分离线程
// 2. 发送客户端请求
char operation_buffer[MAX_MSG_CONTENT_SIZE];
while (1) {
printf("\n[Client %u] 请输入要发送的操作 (例如: 'transfer 100 to Bob', 输入 'quit' 退出):\n", g_client_state.client_id);
if (fgets(operation_buffer, sizeof(operation_buffer), stdin) == NULL) {
continue;
}
operation_buffer[strcspn(operation_buffer, "\n")] = 0; // 移除换行符
if (strcmp(operation_buffer, "quit") == 0) {
printf("[Client %u] 客户端退出。\n", g_client_state.client_id);
break;
}
send_client_request(operation_buffer);
sleep(2); // 等待回复
}
close(listen_sock_fd);
return 0;
}
/**
* @brief 发送客户端请求给主节点 (简化:直接发送给Node 0)
* @param operation 客户端操作内容
*/
void send_client_request(const char* operation) {
PBFTMessage client_msg;
memset(&client_msg, 0, sizeof(PBFTMessage));
// 填充消息头部
client_msg.common_header.type = MSG_TYPE_CLIENT_REQUEST;
client_msg.common_header.view_id = 0; // 客户端不知道当前视图,这里简化为0
client_msg.common_header.sequence_num = 0; // 客户端请求没有序列号,由主节点分配
client_msg.common_header.sender_id = g_client_state.client_id;
// 填充客户端请求内容
client_msg.payload.client_request.client_id = g_client_state.client_id;
client_msg.payload.client_request.timestamp = (uint32_t)time(NULL); // 使用当前时间戳
strncpy(client_msg.payload.client_request.operation, operation, MAX_MSG_CONTENT_SIZE - 1);
client_msg.payload.client_request.operation[MAX_MSG_CONTENT_SIZE - 1] = '\0';
calculate_digest(client_msg.payload.client_request.operation, strlen(client_msg.payload.client_request.operation), client_msg.payload.client_request.digest);
printf("[Client %u] 准备发送客户端请求:\n", g_client_state.client_id);
print_message(&client_msg);
// 连接到主节点 (简化:假设Node 0是主节点)
int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (sock_fd == -1) {
perror("[Client] 创建套接字失败");
return;
}
struct sockaddr_in primary_node_addr;
memset(&primary_node_addr, 0, sizeof(primary_node_addr));
primary_node_addr.sin_family = AF_INET;
primary_node_addr.sin_port = htons(BASE_NODE_PORT + 0); // 连接到Node 0
if (inet_pton(AF_INET, "127.0.0.1", &primary_node_addr.sin_addr) <= 0) {
perror("[Client] 无效的IP地址");
close(sock_fd);
return;
}
printf("[Client %u] 尝试连接到主节点 (Node 0 端口: %d)...\n", g_client_state.client_id, BASE_NODE_PORT + 0);
if (connect(sock_fd, (struct sockaddr*)&primary_node_addr, sizeof(primary_node_addr)) == -1) {
perror("[Client] 连接主节点失败");
close(sock_fd);
return;
}
printf("[Client %u] 成功连接到主节点 (Node 0)。\n", g_client_state.client_id);
// 发送消息
size_t total_msg_len = sizeof(ClientRequest);
ssize_t bytes_sent = send(sock_fd, &client_msg, total_msg_len, 0);
if (bytes_sent == -1) {
perror("[Client] 发送请求失败");
} else if (bytes_sent != total_msg_len) {
fprintf(stderr, "[Client %u] 警告: 发送请求长度不匹配!期望 %zu, 实际 %zd。\n", g_client_state.client_id, total_msg_len, bytes_sent);
} else {
printf("[Client %u] 成功发送请求给主节点。\n", g_client_state.client_id);
}
close(sock_fd); // 发送完请求即可关闭连接
}
/**
* @brief 接收回复的线程函数
* @param arg 监听套接字文件描述符
*/
void *receive_reply_thread(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 node_sock_fd = accept(listen_sock_fd, (struct sockaddr*)&remote_addr, &addr_len);
if (node_sock_fd == -1) {
perror("[Client] 接受节点回复连接失败");
break;
}
inet_ntop(AF_INET, &(remote_addr.sin_addr), remote_ip, INET_ADDRSTRLEN);
printf("[Client %u] 接收到来自 Node %d 的回复连接。\n", g_client_state.client_id, ntohs(remote_addr.sin_port) - BASE_NODE_PORT);
PBFTMessage reply_msg;
ssize_t bytes_received;
// 接收消息头部
bytes_received = recv(node_sock_fd, &reply_msg.common_header, sizeof(MessageHeader), MSG_WAITALL);
if (bytes_received <= 0) {
perror("[Client] 接收回复头部失败");
close(node_sock_fd);
continue;
}
if (reply_msg.common_header.type != MSG_TYPE_REPLY) {
fprintf(stderr, "[Client %u] 警告: 收到非REPLY消息类型 %u,忽略。\n", g_client_state.client_id, reply_msg.common_header.type);
close(node_sock_fd);
continue;
}
// 接收回复payload
size_t payload_size = sizeof(ReplyMsg) - sizeof(MessageHeader);
bytes_received = recv(node_sock_fd, &reply_msg.payload, payload_size, MSG_WAITALL);
if (bytes_received <= 0) {
perror("[Client] 接收回复payload失败");
close(node_sock_fd);
continue;
}
if (bytes_received != payload_size) {
fprintf(stderr, "[Client %u] 接收回复payload长度不匹配!期望 %zu, 实际 %zd。\n", g_client_state.client_id, payload_size, bytes_received);
close(node_sock_fd);
continue;
}
printf("[Client %u] 收到来自 Node %u 的回复:\n", g_client_state.client_id, reply_msg.common_header.sender_id);
print_message(&reply_msg);
// --- 在这里添加客户端回复验证逻辑 ---
// 客户端需要收集 f+1 个相同的回复才能确认请求完成
// 这里简化为收到一个就打印
close(node_sock_fd);
}
return NULL;
}
文件:Makefile
(自动化编译)
# 定义编译器
CC = gcc
# 定义编译选项
# -Wall: 开启所有警告
# -g: 生成调试信息,方便GDB调试
# -pthread: 链接POSIX线程库
# -std=c11: 使用C11标准
CFLAGS = -Wall -g -pthread -std=c11
# 定义目标文件
TARGETS = node client
# 定义源文件
NODE_SRCS = node.c utils.c
CLIENT_SRCS = client.c utils.c
# 定义目标文件 (中间的.o文件)
NODE_OBJS = $(NODE_SRCS:.c=.o)
CLIENT_OBJS = $(CLIENT_SRCS:.c=.o)
# 默认目标:编译所有可执行文件
all: $(TARGETS)
# 编译 node 可执行文件
node: $(NODE_OBJS)
$(CC) $(CFLAGS) $(NODE_OBJS) -o node
# 编译 client 可执行文件
client: $(CLIENT_OBJS)
$(CC) $(CFLAGS) $(CLIENT_OBJS) -o client
# 编译 .c 文件到 .o 文件 (通用规则)
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# 清理生成的文件
clean:
rm -f $(NODE_OBJS) $(CLIENT_OBJS) $(TARGETS)
# 伪目标声明,避免与同名文件冲突
.PHONY: all clean
代码分析与逻辑透析:
这三段C语言代码(common.h
, utils.c
, node.c
, client.c
)和Makefile
共同构建了一个最基础的PBFT算法仿真框架。它实现了多进程节点间的TCP通信和消息的序列化/反序列化,但尚未实现完整的PBFT共识逻辑。
-
common.h
:公共定义与消息结构-
宏定义:
NUM_NODES
(总节点数)、FAULTY_NODES
(可容忍的拜占庭节点数,根据3f+1
规则计算)、DIGEST_SIZE
、MAX_MSG_CONTENT_SIZE
、BASE_NODE_PORT
(节点监听端口基准)、CLIENT_PORT
(客户端监听端口基准)。这些是整个仿真系统的基本参数。 -
MessageType
枚举: 定义了PBFT协议中各种消息的类型,这是消息路由和解析的基础。 -
MessageHeader
结构体: 所有PBFT消息的公共头部。包含消息类型、视图ID、序列号、发送者ID。这是消息传输和校验的基本信息。 -
具体消息结构体:
ClientRequest
、PrePrepareMsg
、PrepareMsg
、CommitMsg
、ReplyMsg
。这些结构体严格按照PBFT协议中各阶段消息的定义来设计,包含各自特有的字段。-
注意: 为了简化仿真和调试,
PrePrepareMsg
中包含了完整的operation
,而不是仅仅request_digest
。在实际PBFT协议中,为了减少网络带宽,通常只传递摘要,接收方再根据摘要从本地缓存中查找原始请求。
-
-
PBFTMessage
共用体结构体: 将所有具体消息结构体封装在一个union
中。这样,在网络传输时,我们只需要发送和接收一个固定大小的PBFTMessage
结构体,然后根据common_header.type
来判断其具体内容,并访问payload
中的相应成员。这是一种常见的网络编程技巧,用于处理多种消息类型。 -
辅助函数声明:
calculate_digest
(简易哈希) 和print_message
(调试用)。
-
-
utils.c
:辅助函数实现-
calculate_digest()
: 一个非常简化的哈希函数。在实际PBFT中,这里必须使用安全的加密哈希算法,如SHA256,以保证消息摘要的不可伪造性。这里仅用于演示概念。 -
print_message()
: 这是一个非常重要的调试工具。它能够根据消息类型,详细打印出PBFTMessage
结构体中的内容,让你在仿真过程中清晰地看到每条消息的传输和解析情况。
-
-
node.c
:PBFT节点实现-
NodeState
结构体: 存储当前节点的ID、视图编号、最新序列号等基本状态信息。 -
g_node_state
: 全局变量,表示当前节点的运行时状态。 -
conn_sockets[NUM_NODES]
: 全局数组,用于存储当前节点与其他所有节点(包括客户端)建立的TCP连接套接字。conn_sockets[i]
表示与节点i
的连接。 -
g_mutex
: 互斥锁,用于保护conn_sockets
等全局共享资源,防止多线程并发访问导致数据不一致。 -
start_node(uint8_t node_id)
:-
初始化节点状态。
-
服务器端逻辑: 创建监听套接字,绑定端口(
BASE_NODE_PORT + node_id
),进入监听状态。然后启动一个独立的线程handle_incoming_connection
来处理所有传入的连接请求。 -
客户端逻辑: 调用
connect_to_peers
函数,主动尝试连接到其他所有节点。 -
主循环:目前只是一个空循环,未来将在这里实现PBFT的核心状态机逻辑。
-
-
handle_incoming_connection()
:-
在一个无限循环中,
accept()
新的客户端连接。 -
每接受一个连接,就创建一个新的线程
recv_message_thread
来专门处理该连接上的消息接收。 -
连接识别: 简单地通过远程端口号推断连接方是哪个节点。注意: 实际系统中,节点之间会通过更安全的握手协议来交换身份信息。
-
将新建立的连接套接字存储到
conn_sockets
数组中。
-
-
connect_to_peers()
:-
遍历所有其他节点ID。
-
为每个未连接的节点,创建一个TCP套接字,并尝试
connect()
到其监听端口。 -
实现了简单的连接重试机制,以应对对端节点尚未启动的情况。
-
连接成功后,同样启动一个独立的线程
recv_message_thread
来接收来自该对端节点的消息。
-
-
recv_message_thread()
:-
这是每个连接对应的消息接收线程。
-
在一个无限循环中,首先接收消息头部 (
MessageHeader
)。MSG_WAITALL
标志确保完整接收指定字节数。 -
根据消息头部中的
type
字段,判断消息类型,然后接收剩余的payload
部分。 -
接收到完整消息后,调用
print_message
打印消息内容(用于调试)。 -
核心: 未来PBFT算法的各个阶段消息处理逻辑将集成到这里。
-
连接断开或出错时,关闭套接字并清理资源。
-
-
send_message_to_node()
:-
通过
target_node_id
从conn_sockets
数组中获取对应的套接字。 -
计算要发送的
PBFTMessage
的总长度。 -
send()
:发送完整的消息结构体。 -
打印发送日志。
-
-
-
client.c
:客户端实现-
ClientState
: 存储客户端ID。 -
main()
:-
初始化客户端ID。
-
服务器端逻辑: 客户端也需要启动一个监听端口(
CLIENT_PORT + client_id
),用于接收来自PBFT节点的REPLY
消息。启动receive_reply_thread
来处理这些回复。 -
主循环: 客户端通过命令行输入操作,然后调用
send_client_request
将请求发送给主节点。
-
-
send_client_request()
:-
构建
ClientRequest
消息,包含客户端ID、时间戳、操作内容和摘要。 -
简化: 客户端直接连接到Node 0(假设它是主节点)。在实际PBFT中,客户端需要知道当前的主节点是谁,或者向所有节点发送请求。
-
发送完请求后,关闭连接。
-
-
receive_reply_thread()
:-
与节点端的接收线程类似,接受来自节点的连接。
-
接收并解析
REPLY
消息。 -
打印收到的回复。
-
核心: 客户端需要收集
f+1
个相同的REPLY
消息才能确认请求已完成。这部分逻辑将在后续添加。
-
-
-
Makefile
:自动化编译-
定义了编译器
CC
、编译选项CFLAGS
(包含-pthread
用于链接线程库)。 -
定义了
node
和client
两个可执行目标。 -
通过
$(SRCS:.c=.o)
自动生成.o
文件列表。 -
定义了编译
.c
到.o
的通用规则。 -
提供了
clean
目标用于清理生成的文件。
-
如何运行这个PBFT基础仿真框架:
-
保存文件: 将上述所有代码分别保存为
common.h
、utils.c
、node.c
、client.c
和Makefile
,放在同一个目录下。 -
打开WSL2终端 (或Linux终端): 进入你保存文件的目录。
-
编译: 在终端中执行
make
-
如果编译成功,你会看到生成了
node
和client
两个可执行文件。
-
-
启动节点: 打开四个独立的WSL2终端窗口。在每个窗口中分别启动一个PBFT节点:
-
终端1:
./node 0
(启动Node 0) -
终端2:
./node 1
(启动Node 1) -
终端3:
./node 2
(启动Node 2) -
终端4:
./node 3
(启动Node 3) -
观察每个节点的输出,它们会尝试相互连接。当所有节点都启动后,你会看到它们之间建立了连接。
-
-
启动客户端: 打开第五个WSL2终端窗口。
-
终端5:
./client 100
(启动Client 100) -
客户端会尝试连接到Node 0,并提示你输入操作。
-
-
发送请求: 在客户端终端输入一个操作,例如
transfer 100 to Bob
,然后按回车。-
客户端会打印发送请求的消息详情。
-
Node 0(主节点)会收到这个客户端请求,并打印消息详情。
-
目前,由于还没有实现PBFT的核心逻辑,节点收到消息后不会有进一步的动作。 客户端也只会收到Node 0的回复(如果Node 0被设置为回复)。
-
当前阶段的局限性:
-
未实现PBFT核心逻辑: 当前代码只是搭建了通信框架,PBFT的三阶段提交、视图切换等核心逻辑尚未实现。
-
简化的摘要函数:
calculate_digest
只是一个简单的哈希,不具备加密安全性。 -
简化的节点识别: 通过端口号推断节点ID,实际系统需要更健壮的身份验证。
-
无故障处理: 当前代码没有处理网络断开、节点崩溃等故障情况。
-
无并发控制: 消息处理线程只是打印消息,没有对共享状态进行修改,未来需要增加更多锁和同步机制。
小结与展望
老铁们,恭喜你!你已经成功迈出了PBFT算法C语言仿真的第一步!
在这一部分中,我们:
-
深入理解了分布式系统中的“一致性”挑战,特别是拜占庭将军问题和CAP定理,这是理解所有共识算法的基石。
-
详细剖析了PBFT算法的核心原理,包括其角色、三阶段提交流程、视图切换机制,以及**
3f+1
容错能力**的数学基础。 -
搭建了一个C语言PBFT仿真框架的骨架,包括:
-
设计了PBFT消息类型和结构体,特别是利用
union
实现统一消息格式。 -
实现了基于TCP套接字的多进程节点间通信,包括服务器端监听、客户端连接、以及多线程消息收发。
-
提供了基础的辅助函数(消息摘要、消息打印)和Makefile自动化编译脚本。
-
虽然我们还没有实现完整的PBFT共识逻辑,但你已经亲手构建了一个能够让多个“节点”和“客户端”在你的电脑上“说话”的网络环境!这正是PBFT算法仿真的第一步,也是最关键的一步!
接下来,我们将进入更具挑战性的中篇,在现有框架的基础上,逐步填充PBFT算法的三阶段提交核心逻辑,包括消息的验证、状态的转换、以及消息的广播。那将是真正让你的PBFT仿真器“活”起来的秘密武器!
请记住,技术学习就像“打怪升级”,每一步的积累都至关重要。如果你在搭建环境或运行代码时遇到任何问题,随时在评论区告诉我,咱们一起交流,一起成为区块链共识算法的“大神”!
敬请期待我的下一次更新!
------------------------------------------------------------------------------------------------------------更新于2025.6.26
PBFT算法仿真:从Win11到共识算法核心(中篇)
前言:让你的PBFT仿真器真正“动”起来!
老铁们,上篇我们已经把PBFT(Practical Byzantine Fault Tolerance,实用拜占庭容错)算法的理论基础和C语言仿真框架的“骨架”搭起来了。你是不是已经迫不及待地想看到这些节点在你的WSL2终端里“跑”起来,真正地达成共识了?
没错!今天,我们就要让这个“骨架”长出“血肉”,让你的PBFT仿真器真正“动”起来!
本篇作为“一文教你C语言彻底学会PBFT算法仿真”系列的中篇,我们将聚焦于PBFT算法最核心的部分——三阶段提交协议的C语言实现。这可不是简单的代码堆砌,我们将深入到每一个消息的验证、每一个状态的转换、每一个计数的更新,让你彻底理解PBFT是如何在拜占庭故障下保证一致性的!
我们将手把手带你:
-
设计并实现PBFT节点内部的“记忆”:消息日志!
-
理解并实现PBFT的“生命线”:状态机与水位线!
-
逐个攻克Pre-prepare、Prepare、Commit这三个核心阶段的C语言逻辑!
-
实现Replica节点对客户端请求的回复!
这部分内容会非常硬核,代码量也会让你“吃到饱”,而且每一行代码都带详细注释和逻辑分析,保证你不仅能看懂,还能亲手跑起来,彻底学透PBFT的每一个细节!
准备好了吗?咱们这就开始,让你的PBFT仿真器,从“静态模型”变为“动态网络”!
第四章:PBFT节点内部状态与消息管理——共识的“记忆”与“脉搏”
要实现PBFT算法,每个节点不仅仅是简单地收发消息,它还需要维护自己的状态,记录收到的消息,并根据这些信息来做出决策。这一章,我们来设计PBFT节点的“记忆”和“脉搏”。
4.1 节点状态机:PBFT的“生命周期”
PBFT算法是一个基于状态转换的协议。每个节点在处理请求时,会经历一系列的状态。理解这些状态及其转换条件,是理解PBFT算法的关键。
PBFT节点状态转换图(简化版):
graph LR
A[Idle/Initial] --> B{主节点接收请求}
B --> C[Pre-prepared]
C --收到有效PRE-PREPARE--> D[Prepared]
D --收到2f个PREPARE--> E[Committed]
E --收到2f+1个COMMIT--> F[Executed/Replied]
F --> A
B --超时或主节点作恶--> G[View-Change (待实现)]
D --超时或主节点作恶--> G
E --超时或主节点作恶--> G
状态解释:
-
Idle/Initial (空闲/初始): 节点处于等待请求或等待Pre-prepare消息的初始状态。
-
Pre-prepared (预准备完成): 节点(副本节点)收到并验证了主节点发来的
PRE-PREPARE
消息,并将其记录到日志中。此时,节点认为主节点已经为这个请求分配了序列号。 -
Prepared (准备完成): 节点(包括主节点和副本节点)收集到了
2f
个(包括自己发送的)有效的PREPARE
消息,这些消息都针对同一个请求(视图、序列号、摘要一致)。此时,节点认为大多数正常节点都同意了请求的顺序和内容。 -
Committed (提交完成): 节点(包括主节点和副本节点)收集到了
2f+1
个(包括自己发送的)有效的COMMIT
消息,这些消息都针对同一个请求。此时,节点认为大多数正常节点都同意执行这个请求。 -
Executed/Replied (已执行/已回复): 节点执行了请求(更新了状态机),并向客户端发送了
REPLY
消息。 -
View-Change (视图切换): 当主节点出现故障或作恶时,节点会进入视图切换状态,尝试选举新的主节点。这部分将在后续实现。
C语言中节点状态的扩展:
我们需要在NodeState
结构体中添加更多字段来维护这些状态信息。
// common.h 中 NodeState 的扩展
// 定义PBFT处理阶段
typedef enum {
PHASE_IDLE,
PHASE_PRE_PREPARE, // 收到PRE-PREPARE (副本节点) 或发送PRE-PREPARE (主节点)
PHASE_PREPARE, // 收到或发送PREPARE
PHASE_COMMIT, // 收到或发送COMMIT
PHASE_REPLY // 收到或发送REPLY
} PBFTPhase;
// --- 节点状态信息 ---
typedef struct {
uint8_t node_id; // 当前节点的ID
uint32_t current_view; // 当前视图编号
uint32_t last_executed_seq_num; // 上一个已执行的序列号 (低水位线)
uint32_t next_seq_num; // 下一个可用的序列号 (主节点分配)
bool is_primary; // 是否是主节点
PBFTPhase current_phase; // 当前节点所处的PBFT阶段
// 实际PBFT节点状态会复杂得多,包括消息日志、高水位、低水位、检查点等
// 这里先添加核心的用于三阶段提交的状态
} NodeState;
4.2 消息日志:PBFT的“记忆”
PBFT算法的安全性依赖于节点能够“记住”它们收到的消息。这些消息构成了消息日志。消息日志对于以下几个方面至关重要:
-
验证消息: 节点收到消息后,需要检查它是否与之前收到的消息一致。
-
防止重放攻击: 通过序列号和时间戳,防止恶意节点重放旧的消息。
-
视图切换恢复: 当发生视图切换时,新主节点需要从副本节点的消息日志中恢复未完成的请求状态。
消息日志的数据结构设计:
我们需要存储PRE-PREPARE
、PREPARE
和COMMIT
消息。由于消息数量可能很多,并且需要根据视图ID、序列号和摘要进行查找,我们可以考虑使用哈希表或链表的组合。
为了简化,我们先使用动态数组或链表来存储消息,并用一个结构体来表示每条日志条目。
ER图:消息日志结构(简化)
erDiagram
NODE ||--o{ MESSAGE_LOG : "stores"
MESSAGE_LOG {
uint32_t view_id
uint32_t sequence_num
uint8_t request_digest[32]
PBFTMessage message_content
bool is_prepared
bool is_committed
int prepare_count
int commit_count
// 其他状态,如是否已执行
}
C语言代码:消息日志定义与管理
我们将定义一个MessageLogEntry
结构体来存储每条消息及其状态,并使用一个简单的链表来管理这些日志条目。
// common.h 中添加消息日志相关定义
// PBFT消息日志条目
typedef struct MessageLogEntry {
uint32_t view_id;
uint32_t sequence_num;
uint8_t request_digest[DIGEST_SIZE];
PBFTMessage original_client_request; // 原始客户端请求的副本 (用于执行)
PBFTMessage pre_prepare_msg; // 存储收到的PRE-PREPARE消息
// 存储收到的PREPARE和COMMIT消息的集合 (这里简化为计数,实际应存储消息本身)
bool received_prepare[NUM_NODES]; // 标记是否收到某个节点的PREPARE消息
bool received_commit[NUM_NODES]; // 标记是否收到某个节点的COMMIT消息
int prepare_count; // 收集到的PREPARE消息数量
int commit_count; // 收集到的COMMIT消息数量
bool is_prepared; // 是否已达到Prepared状态
bool is_committed; // 是否已达到Committed状态
bool is_executed; // 是否已执行
struct MessageLogEntry *next; // 链表指针
} MessageLogEntry;
// common.h 中 NodeState 的扩展 (继续扩展)
typedef struct {
uint8_t node_id; // 当前节点的ID
uint32_t current_view; // 当前视图编号
uint32_t last_executed_seq_num; // 上一个已执行的序列号 (低水位线)
uint32_t next_seq_num; // 下一个可用的序列号 (主节点分配)
bool is_primary; // 是否是主节点
PBFTPhase current_phase; // 当前节点所处的PBFT阶段
MessageLogEntry *message_log_head; // 消息日志链表头
pthread_mutex_t log_mutex; // 保护消息日志的互斥锁
// 实际PBFT节点状态会复杂得多,包括高水位、检查点、视图切换相关消息等
} NodeState;
// common.h 中辅助函数声明
// 查找或创建消息日志条目
MessageLogEntry* get_or_create_log_entry(uint32_t view_id, uint32_t seq_num, const uint8_t* digest);
// 标记日志条目为Prepared状态
void mark_as_prepared(MessageLogEntry* entry);
// 标记日志条目为Committed状态
void mark_as_committed(MessageLogEntry* entry);
// 执行操作并标记为Executed状态
void execute_operation(MessageLogEntry* entry);
// 清理旧的日志条目 (基于水位线,简化)
void clean_old_log_entries();
```c
// utils.c 中实现新增的辅助函数
// 全局节点状态 (在node.c中定义,这里只是声明 extern)
extern NodeState g_node_state;
// 查找或创建消息日志条目
MessageLogEntry* get_or_create_log_entry(uint32_t view_id, uint32_t seq_num, const uint8_t* digest) {
pthread_mutex_lock(&g_node_state.log_mutex);
MessageLogEntry* current = g_node_state.message_log_head;
while (current != NULL) {
if (current->view_id == view_id &&
current->sequence_num == seq_num &&
memcmp(current->request_digest, digest, DIGEST_SIZE) == 0) {
pthread_mutex_unlock(&g_node_state.log_mutex);
return current; // 找到现有条目
}
current = current->next;
}
// 未找到,创建新条目
MessageLogEntry* new_entry = (MessageLogEntry*)malloc(sizeof(MessageLogEntry));
if (new_entry == NULL) {
fprintf(stderr, "[Node %u] 错误: 分配消息日志条目内存失败!\n", g_node_state.node_id);
pthread_mutex_unlock(&g_node_state.log_mutex);
exit(EXIT_FAILURE);
}
memset(new_entry, 0, sizeof(MessageLogEntry));
new_entry->view_id = view_id;
new_entry->sequence_num = seq_num;
memcpy(new_entry->request_digest, digest, DIGEST_SIZE);
new_entry->is_prepared = false;
new_entry->is_committed = false;
new_entry->is_executed = false;
new_entry->prepare_count = 0;
new_entry->commit_count = 0;
for (int i = 0; i < NUM_NODES; ++i) {
new_entry->received_prepare[i] = false;
new_entry->received_commit[i] = false;
}
// 将新条目添加到链表头部 (简化)
new_entry->next = g_node_state.message_log_head;
g_node_state.message_log_head = new_entry;
printf("[Node %u] 创建新的消息日志条目: 视图 %u, 序列号 %u\n", g_node_state.node_id, view_id, seq_num);
pthread_mutex_unlock(&g_node_state.log_mutex);
return new_entry;
}
// 标记日志条目为Prepared状态
void mark_as_prepared(MessageLogEntry* entry) {
pthread_mutex_lock(&g_node_state.log_mutex);
if (!entry->is_prepared) {
entry->is_prepared = true;
printf("[Node %u] 消息日志条目 (视图 %u, 序列号 %u) 已标记为 Prepared。\n",
g_node_state.node_id, entry->view_id, entry->sequence_num);
}
pthread_mutex_unlock(&g_node_state.log_mutex);
}
// 标记日志条目为Committed状态
void mark_as_committed(MessageLogEntry* entry) {
pthread_mutex_lock(&g_node_state.log_mutex);
if (!entry->is_committed) {
entry->is_committed = true;
printf("[Node %u] 消息日志条目 (视图 %u, 序列号 %u) 已标记为 Committed。\n",
g_node_state.node_id, entry->view_id, entry->sequence_num);
}
pthread_mutex_unlock(&g_node_state.log_mutex);
}
// 执行操作并标记为Executed状态 (简化:这里只是打印)
void execute_operation(MessageLogEntry* entry) {
pthread_mutex_lock(&g_node_state.log_mutex);
if (!entry->is_executed) {
entry->is_executed = true;
printf("[Node %u] 执行操作: '%s' (视图 %u, 序列号 %u)\n",
g_node_state.node_id, entry->original_client_request.payload.client_request.operation,
entry->view_id, entry->sequence_num);
// 实际应用中,这里会更新状态机,例如:
// apply_transaction(entry->original_client_request.payload.client_request.operation);
// 更新已执行序列号 (低水位线)
if (entry->sequence_num > g_node_state.last_executed_seq_num) {
g_node_state.last_executed_seq_num = entry->sequence_num;
printf("[Node %u] 更新 last_executed_seq_num 为 %u。\n", g_node_state.node_id, g_node_state.last_executed_seq_num);
}
}
pthread_mutex_unlock(&g_node_state.log_mutex);
}
// 清理旧的日志条目 (基于水位线,简化:清理所有小于last_executed_seq_num的条目)
void clean_old_log_entries() {
pthread_mutex_lock(&g_node_state.log_mutex);
MessageLogEntry* current = g_node_state.message_log_head;
MessageLogEntry* prev = NULL;
while (current != NULL) {
if (current->sequence_num <= g_node_state.last_executed_seq_num && current->is_executed) {
// 找到已执行且序列号小于等于低水位线的条目,可以清理
printf("[Node %u] 清理旧日志条目: 视图 %u, 序列号 %u\n", g_node_state.node_id, current->view_id, current->sequence_num);
if (prev == NULL) {
g_node_state.message_log_head = current->next;
free(current);
current = g_node_state.message_log_head;
} else {
prev->next = current->next;
free(current);
current = prev->next;
}
} else {
prev = current;
current = current->next;
}
}
pthread_mutex_unlock(&g_node_state.log_mutex);
}
4.3 水位线 (Watermarks):优化存储与垃圾回收
随着请求的不断处理,消息日志会越来越大。PBFT引入了水位线 (Watermarks) 和检查点 (Checkpoint) 机制来管理日志大小,进行垃圾回收。
-
低水位线 (h, Low Watermark): 表示所有节点都已提交并执行的最小序列号。所有序列号小于或等于
h
的请求及其相关消息都可以从日志中删除。在我们的仿真中,last_executed_seq_num
就扮演了低水位线的角色。 -
高水位线 (H, High Watermark): 表示当前主节点可以分配的最大序列号。通常是
h + K
,其中K
是一个常数(窗口大小)。这防止了主节点分配过大的序列号,从而限制了副本节点需要维护的日志大小。 -
检查点 (Checkpoint): 周期性地,系统会创建一个检查点,记录当前状态机的最新状态和对应的序列号。检查点一旦被
2f+1
个节点确认,就可以将所有小于检查点序列号的日志条目安全地删除。
在我们的仿真中,我们将简化水位线管理:
-
g_node_state.last_executed_seq_num
作为低水位线,每次执行完操作就更新。 -
clean_old_log_entries()
函数会清理所有序列号小于等于last_executed_seq_num
且已执行的日志条目。 -
高水位线和检查点机制将在后续更复杂的仿真中考虑。
第五章:PBFT三阶段提交协议的C语言实现——让共识真正发生!
现在,我们有了节点状态和消息日志,可以开始实现PBFT的三阶段提交协议了!我们将修改node.c
中的recv_message_thread
函数,根据收到的消息类型,调用不同的处理逻辑。
5.1 客户端请求处理 (主节点 Primary)
流程:
-
接收客户端请求: 主节点(Primary)通过其监听端口接收客户端发来的
CLIENT_REQUEST
消息。 -
验证请求: 检查请求的合法性(例如,客户端ID、时间戳、操作内容等)。
-
分配序列号: 为请求分配一个唯一的序列号
n
。这个序列号必须大于last_executed_seq_num
。 -
构建
PRE-PREPARE
消息: 包含视图IDv
、序列号n
、请求摘要d
和原始请求m
。 -
广播
PRE-PREPARE
消息: 将PRE-PREPARE
消息发送给所有副本节点。 -
存储到日志: 将
PRE-PREPARE
消息及其对应的客户端请求存储到自己的消息日志中。
C语言代码:node.c
中添加主节点处理逻辑
// node.c 中添加 PBFT 核心处理函数声明
void handle_client_request_msg(const ClientRequest* req_msg);
void handle_pre_prepare_msg(const PrePrepareMsg* pp_msg);
void handle_prepare_msg(const PrepareMsg* p_msg);
void handle_commit_msg(const CommitMsg* c_msg);
void send_reply_to_client(uint8_t client_id, uint32_t timestamp, const char* result);
// node.c 中修改 recv_message_thread 函数
void *recv_message_thread(void *arg) {
ThreadArgs* args = (ThreadArgs*)arg;
int sock_fd = args->client_sock_fd;
uint8_t remote_node_id = args->remote_node_id;
free(args); // 释放线程参数内存
PBFTMessage msg_buffer;
ssize_t bytes_received;
printf("[Node %u] 接收线程启动,监听来自 Node %u 的消息...\n", g_node_state.node_id, remote_node_id);
while (1) {
// 接收消息头部
bytes_received = recv(sock_fd, &msg_buffer.common_header, sizeof(MessageHeader), MSG_WAITALL);
if (bytes_received <= 0) {
if (bytes_received == 0) {
printf("[Node %u] Node %u 断开连接。\n", g_node_state.node_id, remote_node_id);
} else {
perror("[Node] 接收消息头部失败");
}
break; // 连接断开或出错
}
// 根据消息类型接收剩余的payload
size_t payload_size = 0;
switch (msg_buffer.common_header.type) {
case MSG_TYPE_CLIENT_REQUEST: payload_size = sizeof(ClientRequest) - sizeof(MessageHeader); break;
case MSG_TYPE_PRE_PREPARE: payload_size = sizeof(PrePrepareMsg) - sizeof(MessageHeader); break;
case MSG_TYPE_PREPARE: payload_size = sizeof(PrepareMsg) - sizeof(MessageHeader); break;
case MSG_TYPE_COMMIT: payload_size = sizeof(CommitMsg) - sizeof(MessageHeader); break;
case MSG_TYPE_REPLY: payload_size = sizeof(ReplyMsg) - sizeof(MessageHeader); break;
// 其他消息类型待添加
default:
fprintf(stderr, "[Node %u] 接收到未知消息类型 %u,跳过payload接收。\n", g_node_state.node_id, msg_buffer.common_header.type);
break;
}
if (payload_size > 0) {
bytes_received = recv(sock_fd, &msg_buffer.payload, payload_size, MSG_WAITALL);
if (bytes_received <= 0) {
if (bytes_received == 0) {
printf("[Node %u] Node %u 断开连接 (接收payload时)。\n", g_node_state.node_id, remote_node_id);
} else {
perror("[Node] 接收消息payload失败");
}
break;
}
if (bytes_received != payload_size) {
fprintf(stderr, "[Node %u] 接收payload长度不匹配!期望 %zu, 实际 %zd。\n", g_node_state.node_id, payload_size, bytes_received);
break;
}
}
// 成功接收一条完整的消息
printf("[Node %u] 收到来自 Node %u 的消息:\n", g_node_state.node_id, msg_buffer.common_header.sender_id);
print_message(&msg_buffer);
// --- PBFT消息处理逻辑 ---
pthread_mutex_lock(&g_mutex); // 保护g_node_state和其他共享资源
switch (msg_buffer.common_header.type) {
case MSG_TYPE_CLIENT_REQUEST:
// 只有主节点处理客户端请求
if (g_node_state.is_primary) {
handle_client_request_msg(&msg_buffer.payload.client_request);
} else {
printf("[Node %u] 警告: 副本节点收到客户端请求,忽略。\n", g_node_state.node_id);
}
break;
case MSG_TYPE_PRE_PREPARE:
// 副本节点处理PRE-PREPARE消息
if (!g_node_state.is_primary) {
handle_pre_prepare_msg(&msg_buffer.payload.pre_prepare);
} else {
printf("[Node %u] 警告: 主节点收到PRE-PREPARE消息,忽略。\n", g_node_state.node_id);
}
break;
case MSG_TYPE_PREPARE:
// 所有节点处理PREPARE消息
handle_prepare_msg(&msg_buffer.payload.prepare);
break;
case MSG_TYPE_COMMIT:
// 所有节点处理COMMIT消息
handle_commit_msg(&msg_buffer.payload.commit);
break;
case MSG_TYPE_REPLY:
// 节点通常不处理REPLY消息,REPLY是发给客户端的
printf("[Node %u] 收到REPLY消息,忽略 (REPLY是发给客户端的)。\n", g_node_state.node_id);
break;
default:
fprintf(stderr, "[Node %u] 未知消息类型 %u,无法处理。\n", g_node_state.node_id, msg_buffer.common_header.type);
break;
}
pthread_mutex_unlock(&g_mutex);
}
// 连接断开,清理资源
pthread_mutex_lock(&g_mutex);
if (conn_sockets[remote_node_id] == sock_fd) { // 确保是当前连接
conn_sockets[remote_node_id] = -1;
}
pthread_mutex_unlock(&g_mutex);
close(sock_fd);
printf("[Node %u] 与 Node %u 的连接已关闭。\n", g_node_state.node_id, remote_node_id);
return NULL;
}
/**
* @brief 处理客户端请求消息 (仅主节点调用)
* @param req_msg 客户端请求消息
*/
void handle_client_request_msg(const ClientRequest* req_msg) {
printf("[Node %u] (主节点) 收到客户端 %u 的请求: '%s'\n",
g_node_state.node_id, req_msg->client_id, req_msg->operation);
// 1. 验证请求 (简化:这里只检查时间戳,实际需要签名验证、防重放等)
// 假设时间戳必须大于0
if (req_msg->timestamp == 0) {
fprintf(stderr, "[Node %u] 错误: 无效的客户端请求 (时间戳为0)。\n", g_node_state.node_id);
// 实际PBFT会向客户端发送错误回复
return;
}
// 2. 分配序列号
// 序列号必须是递增的,且在水位线范围内
// 这里简化为直接递增,不考虑高水位线和检查点
g_node_state.next_seq_num++; // 分配下一个序列号
uint32_t current_seq_num = g_node_state.next_seq_num;
printf("[Node %u] (主节点) 为请求分配序列号: %u\n", g_node_state.node_id, current_seq_num);
// 3. 构建 PRE-PREPARE 消息
PBFTMessage pp_msg;
memset(&pp_msg, 0, sizeof(PBFTMessage));
pp_msg.common_header.type = MSG_TYPE_PRE_PREPARE;
pp_msg.common_header.view_id = g_node_state.current_view;
pp_msg.common_header.sequence_num = current_seq_num;
pp_msg.common_header.sender_id = g_node_state.node_id;
pp_msg.payload.pre_prepare.client_id = req_msg->client_id;
pp_msg.payload.pre_prepare.timestamp = req_msg->timestamp;
strncpy(pp_msg.payload.pre_prepare.operation, req_msg->operation, MAX_MSG_CONTENT_SIZE - 1);
memcpy(pp_msg.payload.pre_prepare.request_digest, req_msg->digest, DIGEST_SIZE);
// 4. 存储到消息日志 (主节点也需要存储)
MessageLogEntry* log_entry = get_or_create_log_entry(g_node_state.current_view, current_seq_num, req_msg->digest);
memcpy(&log_entry->original_client_request, req_msg, sizeof(ClientRequest)); // 存储原始请求
memcpy(&log_entry->pre_prepare_msg, &pp_msg.payload.pre_prepare, sizeof(PrePrepareMsg)); // 存储PRE-PREPARE消息
// 5. 广播 PRE-PREPARE 消息给所有副本节点
for (uint8_t i = 0; i < NUM_NODES; ++i) {
if (i == g_node_state.node_id) continue; // 不发给自己
send_message_to_node(i, &pp_msg);
}
// 主节点自己也进入Prepared状态 (因为自己发了PRE-PREPARE)
// 并且为自己增加一个PREPARE计数
log_entry->prepare_count++;
log_entry->received_prepare[g_node_state.node_id] = true;
printf("[Node %u] (主节点) 自身PREPARE计数: %d\n", g_node_state.node_id, log_entry->prepare_count);
// 检查是否达到Prepared状态 (主节点也需要收集PREPARE)
if (log_entry->prepare_count >= (2 * FAULTY_NODES)) { // 2f个PREPARE (不包括自己)
mark_as_prepared(log_entry);
// 如果达到Prepared状态,主节点也需要广播COMMIT消息
PBFTMessage commit_msg;
memset(&commit_msg, 0, sizeof(PBFTMessage));
commit_msg.common_header.type = MSG_TYPE_COMMIT;
commit_msg.common_header.view_id = g_node_state.current_view;
commit_msg.common_header.sequence_num = current_seq_num;
commit_msg.common_header.sender_id = g_node_state.node_id;
memcpy(commit_msg.payload.commit.request_digest, req_msg->digest, DIGEST_SIZE);
for (uint8_t i = 0; i < NUM_NODES; ++i) {
send_message_to_node(i, &commit_msg);
}
// 主节点自己也增加一个COMMIT计数
log_entry->commit_count++;
log_entry->received_commit[g_node_state.node_id] = true;
printf("[Node %u] (主节点) 自身COMMIT计数: %d\n", g_node_state.node_id, log_entry->commit_count);
}
}
5.2 预准备阶段 (副本节点 Backup)
流程:
-
接收
PRE-PREPARE
消息: 副本节点(Backup)收到主节点发来的PRE-PREPARE
消息。 -
验证
PRE-PREPARE
消息:-
视图ID
v
是否与当前视图匹配。 -
序列号
n
是否在有效范围内(h < n < H
)。 -
消息摘要
d
是否与原始请求m
的摘要匹配。 -
发送者是否是当前视图的主节点。
-
检查是否已经收到过相同
v, n
但不同d
的PRE-PREPARE
消息(防止主节点作恶)。
-
-
存储到日志: 如果验证通过,将
PRE-PREPARE
消息及其对应的客户端请求存储到自己的消息日志中。 -
构建
PREPARE
消息: 包含视图IDv
、序列号n
、请求摘要d
和自己的节点IDi
。 -
广播
PREPARE
消息: 将PREPARE
消息发送给所有其他节点(包括主节点)。
C语言代码:node.c
中添加副本节点处理逻辑
// node.c 中 handle_pre_prepare_msg 函数实现
/**
* @brief 处理PRE-PREPARE消息 (仅副本节点调用)
* @param pp_msg PRE-PREPARE消息
*/
void handle_pre_prepare_msg(const PrePrepareMsg* pp_msg) {
printf("[Node %u] (副本节点) 收到来自主节点 %u 的PRE-PREPARE消息 (视图 %u, 序列号 %u)\n",
g_node_state.node_id, pp_msg->common_header.sender_id, pp_msg->common_header.view_id, pp_msg->common_header.sequence_num);
// 1. 验证PRE-PREPARE消息
// 验证视图ID是否匹配
if (pp_msg->common_header.view_id != g_node_state.current_view) {
fprintf(stderr, "[Node %u] 错误: PRE-PREPARE视图ID不匹配 (期望 %u, 实际 %u)。\n",
g_node_state.node_id, g_node_state.current_view, pp_msg->common_header.view_id);
// 实际PBFT会触发视图切换
return;
}
// 验证发送者是否是当前视图的主节点
if (pp_msg->common_header.sender_id != (g_node_state.current_view % NUM_NODES)) {
fprintf(stderr, "[Node %u] 错误: PRE-PREPARE发送者不是当前主节点 (期望 %u, 实际 %u)。\n",
g_node_state.node_id, (g_node_state.current_view % NUM_NODES), pp_msg->common_header.sender_id);
// 实际PBFT会触发视图切换
return;
}
// 验证序列号是否在有效范围内 (简化:这里只检查大于已执行的序列号)
if (pp_msg->common_header.sequence_num <= g_node_state.last_executed_seq_num) {
fprintf(stderr, "[Node %u] 错误: PRE-PREPARE序列号过低 (序列号 %u <= 已执行 %u)。\n",
g_node_state.node_id, pp_msg->common_header.sequence_num, g_node_state.last_executed_seq_num);
return;
}
// 验证消息摘要是否正确 (这里依赖简化哈希)
uint8_t calculated_digest[DIGEST_SIZE];
calculate_digest(pp_msg->operation, strlen(pp_msg->operation), calculated_digest);
if (memcmp(calculated_digest, pp_msg->request_digest, DIGEST_SIZE) != 0) {
fprintf(stderr, "[Node %u] 错误: PRE-PREPARE摘要不匹配!\n", g_node_state.node_id);
return;
}
// 2. 存储到消息日志
MessageLogEntry* log_entry = get_or_create_log_entry(pp_msg->common_header.view_id,
pp_msg->common_header.sequence_num,
pp_msg->request_digest);
// 检查是否已经收到过相同 v, n 但不同 d 的 PRE-PREPARE 消息 (防止主节点作恶)
if (log_entry->pre_prepare_msg.common_header.type == MSG_TYPE_PRE_PREPARE &&
memcmp(log_entry->pre_prepare_msg.payload.pre_prepare.request_digest, pp_msg->request_digest, DIGEST_SIZE) != 0) {
fprintf(stderr, "[Node %u] 警告: 收到冲突的PRE-PREPARE消息 (视图 %u, 序列号 %u),触发视图切换!\n",
g_node_state.node_id, pp_msg->common_header.view_id, pp_msg->common_header.sequence_num);
// 实际PBFT会立刻触发视图切换
return;
}
memcpy(&log_entry->pre_prepare_msg, pp_msg, sizeof(PrePrepareMsg)); // 存储PRE-PREPARE消息
// 存储原始客户端请求 (从PRE-PREPARE中提取)
log_entry->original_client_request.common_header.type = MSG_TYPE_CLIENT_REQUEST;
log_entry->original_client_request.common_header.sender_id = pp_msg->client_id; // 客户端ID作为发送者
log_entry->original_client_request.payload.client_request.client_id = pp_msg->client_id;
log_entry->original_client_request.payload.client_request.timestamp = pp_msg->timestamp;
strncpy(log_entry->original_client_request.payload.client_request.operation, pp_msg->operation, MAX_MSG_CONTENT_SIZE - 1);
memcpy(log_entry->original_client_request.payload.client_request.digest, pp_msg->request_digest, DIGEST_SIZE);
// 3. 构建 PREPARE 消息
PBFTMessage p_msg;
memset(&p_msg, 0, sizeof(PBFTMessage));
p_msg.common_header.type = MSG_TYPE_PREPARE;
p_msg.common_header.view_id = g_node_state.current_view;
p_msg.common_header.sequence_num = pp_msg->common_header.sequence_num;
p_msg.common_header.sender_id = g_node_state.node_id;
memcpy(p_msg.payload.prepare.request_digest, pp_msg->request_digest, DIGEST_SIZE);
// 4. 广播 PREPARE 消息给所有节点
for (uint8_t i = 0; i < NUM_NODES; ++i) {
send_message_to_node(i, &p_msg);
}
// 副本节点自己也增加一个PREPARE计数
log_entry->prepare_count++;
log_entry->received_prepare[g_node_state.node_id] = true;
printf("[Node %u] 自身PREPARE计数: %d\n", g_node_state.node_id, log_entry->prepare_count);
}
5.3 准备阶段 (所有节点)
流程:
-
收集
PREPARE
消息: 所有节点(包括主节点和副本节点)都会接收来自其他节点的PREPARE
消息。 -
验证
PREPARE
消息:-
视图ID
v
、序列号n
、摘要d
是否与当前正在处理的请求一致。 -
发送者ID
i
是否合法。 -
检查是否重复收到某个节点的
PREPARE
消息。
-
-
更新消息日志: 记录收到
PREPARE
消息的节点ID,并增加prepare_count
。 -
判断是否达到
Prepared
状态: 当prepare_count
达到2f
个(包括自己发送的)有效的PREPARE
消息时,节点进入Prepared
状态。 -
构建
COMMIT
消息: 如果进入Prepared
状态,节点会构建COMMIT
消息,包含视图IDv
、序列号n
、请求摘要d
和自己的节点IDi
。 -
广播
COMMIT
消息: 将COMMIT
消息发送给所有其他节点。
C语言代码:node.c
中添加处理PREPARE
消息的逻辑
// node.c 中 handle_prepare_msg 函数实现
/**
* @brief 处理PREPARE消息 (所有节点调用)
* @param p_msg PREPARE消息
*/
void handle_prepare_msg(const PrepareMsg* p_msg) {
printf("[Node %u] 收到来自 Node %u 的PREPARE消息 (视图 %u, 序列号 %u)\n",
g_node_state.node_id, p_msg->common_header.sender_id, p_msg->common_header.view_id, p_msg->common_header.sequence_num);
// 1. 验证PREPARE消息
// 视图ID、序列号是否匹配当前处理的请求 (简化:这里只检查视图ID)
if (p_msg->common_header.view_id != g_node_state.current_view) {
fprintf(stderr, "[Node %u] 错误: PREPARE视图ID不匹配 (期望 %u, 实际 %u)。\n",
g_node_state.node_id, g_node_state.current_view, p_msg->common_header.view_id);
return;
}
// 检查发送者ID是否合法
if (p_msg->common_header.sender_id >= NUM_NODES) {
fprintf(stderr, "[Node %u] 错误: PREPARE发送者ID非法 %u。\n", g_node_state.node_id, p_msg->common_header.sender_id);
return;
}
// 2. 更新消息日志
MessageLogEntry* log_entry = get_or_create_log_entry(p_msg->common_header.view_id,
p_msg->common_header.sequence_num,
p_msg->request_digest);
// 检查是否已经收到过此节点的PREPARE消息
pthread_mutex_lock(&g_node_state.log_mutex);
if (log_entry->received_prepare[p_msg->common_header.sender_id]) {
printf("[Node %u] 警告: 收到重复的PREPARE消息来自 Node %u (视图 %u, 序列号 %u)。\n",
g_node_state.node_id, p_msg->common_header.sender_id, p_msg->common_header.view_id, p_msg->common_header.sequence_num);
pthread_mutex_unlock(&g_node_state.log_mutex);
return;
}
log_entry->received_prepare[p_msg->common_header.sender_id] = true;
log_entry->prepare_count++;
printf("[Node %u] 消息 (视图 %u, 序列号 %u) PREPARE计数: %d\n",
g_node_state.node_id, log_entry->view_id, log_entry->sequence_num, log_entry->prepare_count);
// 3. 判断是否达到Prepared状态
// PBFT要求收集 2f 个PREPARE消息 (包括自己发送的) 才能进入Prepared状态
if (!log_entry->is_prepared && log_entry->prepare_count >= (2 * FAULTY_NODES + 1)) { // 2f+1
mark_as_prepared(log_entry);
// 4. 构建 COMMIT 消息并广播
PBFTMessage commit_msg;
memset(&commit_msg, 0, sizeof(PBFTMessage));
commit_msg.common_header.type = MSG_TYPE_COMMIT;
commit_msg.common_header.view_id = g_node_state.current_view;
commit_msg.common_header.sequence_num = log_entry->sequence_num;
commit_msg.common_header.sender_id = g_node_state.node_id;
memcpy(commit_msg.payload.commit.request_digest, log_entry->request_digest, DIGEST_SIZE);
for (uint8_t i = 0; i < NUM_NODES; ++i) {
send_message_to_node(i, &commit_msg);
}
// 节点自己也增加一个COMMIT计数
log_entry->commit_count++;
log_entry->received_commit[g_node_state.node_id] = true;
printf("[Node %u] 自身COMMIT计数: %d\n", g_node_state.node_id, log_entry->commit_count);
}
pthread_mutex_unlock(&g_node_state.log_mutex);
}
5.4 提交阶段 (所有节点)
流程:
-
收集
COMMIT
消息: 所有节点都会接收来自其他节点的COMMIT
消息。 -
验证
COMMIT
消息:-
视图ID
v
、序列号n
、摘要d
是否与当前正在处理的请求一致。 -
发送者ID
i
是否合法。 -
检查是否重复收到某个节点的
COMMIT
消息。
-
-
更新消息日志: 记录收到
COMMIT
消息的节点ID,并增加commit_count
。 -
判断是否达到
Committed
状态: 当commit_count
达到2f+1
个(包括自己发送的)有效的COMMIT
消息时,节点进入Committed
状态。 -
执行操作: 如果进入
Committed
状态,节点就可以安全地执行请求(更新状态机)。 -
构建
REPLY
消息: 包含视图IDv
、序列号n
、执行结果r
和自己的节点IDi
。 -
发送
REPLY
消息: 将REPLY
消息发送给客户端。 -
清理日志: 如果执行的序列号是当前已执行的最高序列号,则更新低水位线,并清理旧的日志条目。
C语言代码:node.c
中添加处理COMMIT
消息的逻辑
// node.c 中 handle_commit_msg 函数实现
/**
* @brief 处理COMMIT消息 (所有节点调用)
* @param c_msg COMMIT消息
*/
void handle_commit_msg(const CommitMsg* c_msg) {
printf("[Node %u] 收到来自 Node %u 的COMMIT消息 (视图 %u, 序列号 %u)\n",
g_node_state.node_id, c_msg->common_header.sender_id, c_msg->common_header.view_id, c_msg->common_header.sequence_num);
// 1. 验证COMMIT消息
// 视图ID、序列号是否匹配当前处理的请求 (简化:只检查视图ID)
if (c_msg->common_header.view_id != g_node_state.current_view) {
fprintf(stderr, "[Node %u] 错误: COMMIT视图ID不匹配 (期望 %u, 实际 %u)。\n",
g_node_state.node_id, g_node_state.current_view, c_msg->common_header.view_id);
return;
}
// 检查发送者ID是否合法
if (c_msg->common_header.sender_id >= NUM_NODES) {
fprintf(stderr, "[Node %u] 错误: COMMIT发送者ID非法 %u。\n", g_node_state.node_id, c_msg->common_header.sender_id);
return;
}
// 2. 更新消息日志
MessageLogEntry* log_entry = get_or_create_log_entry(c_msg->common_header.view_id,
c_msg->common_header.sequence_num,
c_msg->request_digest);
// 检查是否已经收到过此节点的COMMIT消息
pthread_mutex_lock(&g_node_state.log_mutex);
if (log_entry->received_commit[c_msg->common_header.sender_id]) {
printf("[Node %u] 警告: 收到重复的COMMIT消息来自 Node %u (视图 %u, 序列号 %u)。\n",
g_node_state.node_id, c_msg->common_header.sender_id, c_msg->common_header.view_id, c_msg->common_header.sequence_num);
pthread_mutex_unlock(&g_node_state.log_mutex);
return;
}
log_entry->received_commit[c_msg->common_header.sender_id] = true;
log_entry->commit_count++;
printf("[Node %u] 消息 (视图 %u, 序列号 %u) COMMIT计数: %d\n",
g_node_state.node_id, log_entry->view_id, log_entry->sequence_num, log_entry->commit_count);
// 3. 判断是否达到Committed状态
// PBFT要求收集 2f+1 个COMMIT消息 (包括自己发送的) 才能进入Committed状态
if (!log_entry->is_committed && log_entry->commit_count >= (2 * FAULTY_NODES + 1)) {
mark_as_committed(log_entry);
// 4. 执行操作 (如果尚未执行)
if (!log_entry->is_executed) {
execute_operation(log_entry); // 执行操作并更新last_executed_seq_num
clean_old_log_entries(); // 清理旧日志
}
// 5. 构建 REPLY 消息并发送给客户端
// 只有当PBFT节点执行完操作后,才会向客户端发送REPLY
PBFTMessage reply_msg;
memset(&reply_msg, 0, sizeof(PBFTMessage));
reply_msg.common_header.type = MSG_TYPE_REPLY;
reply_msg.common_header.view_id = g_node_state.current_view;
reply_msg.common_header.sequence_num = log_entry->sequence_num;
reply_msg.common_header.sender_id = g_node_state.node_id;
reply_msg.payload.reply.client_id = log_entry->original_client_request.payload.client_request.client_id;
reply_msg.payload.reply.timestamp = log_entry->original_client_request.payload.client_request.timestamp;
// 简化:执行结果为“OK”
snprintf(reply_msg.payload.reply.result, MAX_MSG_CONTENT_SIZE, "OK for op '%s'", log_entry->original_client_request.payload.client_request.operation);
// 客户端连接是临时的,由客户端主动连接,这里需要重新建立连接或使用预存的客户端连接
// 为了简化,我们假设客户端会监听一个固定的端口,节点主动连接过去发送回复
int client_sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (client_sock_fd == -1) {
perror("[Node] 创建客户端回复套接字失败");
pthread_mutex_unlock(&g_node_state.log_mutex);
return;
}
struct sockaddr_in client_addr;
memset(&client_addr, 0, sizeof(client_addr));
client_addr.sin_family = AF_INET;
client_addr.sin_port = htons(CLIENT_PORT + reply_msg.payload.reply.client_id); // 连接到客户端的监听端口
if (inet_pton(AF_INET, "127.0.0.1", &client_addr.sin_addr) <= 0) {
perror("[Node] 无效的客户端IP地址");
close(client_sock_fd);
pthread_mutex_unlock(&g_node_state.log_mutex);
return;
}
printf("[Node %u] 尝试连接客户端 %u (端口: %d) 发送回复...\n", g_node_state.node_id, reply_msg.payload.reply.client_id, CLIENT_PORT + reply_msg.payload.reply.client_id);
if (connect(client_sock_fd, (struct sockaddr*)&client_addr, sizeof(client_addr)) == -1) {
perror("[Node] 连接客户端失败,无法发送回复");
close(client_sock_fd);
pthread_mutex_unlock(&g_node_state.log_mutex);
return;
}
printf("[Node %u] 成功连接客户端 %u,发送回复。\n", g_node_state.node_id, reply_msg.payload.reply.client_id);
size_t total_msg_len = sizeof(ReplyMsg);
ssize_t bytes_sent = send(client_sock_fd, &reply_msg, total_msg_len, 0);
if (bytes_sent == -1) {
perror("[Node] 发送回复失败");
} else if (bytes_sent != total_msg_len) {
fprintf(stderr, "[Node %u] 警告: 发送回复长度不匹配!期望 %zu, 实际 %zd。\n", g_node_state.node_id, total_msg_len, bytes_sent);
} else {
printf("[Node %u] 成功发送回复给客户端 %u。\n", g_node_state.node_id, reply_msg.payload.reply.client_id);
}
close(client_sock_fd);
}
pthread_mutex_unlock(&g_node_state.log_mutex);
}
5.5 回复阶段 (客户端)
流程:
-
收集
REPLY
消息: 客户端会接收来自PBFT节点的REPLY
消息。 -
验证
REPLY
消息: 检查视图ID、序列号、客户端ID、时间戳是否与自己发出的请求匹配。 -
判断是否达到
f+1
个相同回复: 客户端会等待收集到f+1
个来自不同节点的相同REPLY
消息(结果一致)。 -
确认请求完成: 一旦收集到足够的相同回复,客户端就确认其请求已在分布式系统中达成共识并执行。
C语言代码:client.c
中添加回复计数和验证逻辑
// client.c 中添加客户端状态扩展
// 客户端状态信息
typedef struct {
uint8_t client_id;
// 存储已发送请求的记录,以及等待的回复
// 简化:这里只记录最近一次请求的信息
uint32_t last_sent_timestamp;
uint8_t last_sent_digest[DIGEST_SIZE];
char last_sent_operation[MAX_MSG_CONTENT_SIZE];
// 收集到的回复
// 实际需要一个更复杂的数据结构来存储不同请求的回复,并进行去重和计数
// 这里简化为只处理一个请求的回复
int reply_count;
bool received_reply[NUM_NODES]; // 标记是否收到某个节点的回复
// 存储收到的第一个有效回复的结果,用于比较
char first_valid_reply_result[MAX_MSG_CONTENT_SIZE];
pthread_mutex_t reply_mutex; // 保护回复计数的互斥锁
} ClientState;
ClientState g_client_state; // 全局客户端状态
// client.c 中 main 函数初始化互斥锁
int main(int argc, char* argv[]) {
// ... (省略其他初始化代码)
pthread_mutex_init(&g_client_state.reply_mutex, NULL); // 初始化互斥锁
// ... (省略其他代码)
pthread_mutex_destroy(&g_client_state.reply_mutex); // 销毁互斥锁
return 0;
}
// client.c 中 send_client_request 函数修改
void send_client_request(const char* operation) {
// ... (省略构建消息代码)
// 存储最近一次发送请求的信息
pthread_mutex_lock(&g_client_state.reply_mutex);
g_client_state.last_sent_timestamp = client_msg.payload.client_request.timestamp;
memcpy(g_client_state.last_sent_digest, client_msg.payload.client_request.digest, DIGEST_SIZE);
strncpy(g_client_state.last_sent_operation, client_msg.payload.client_request.operation, MAX_MSG_CONTENT_SIZE - 1);
g_client_state.last_sent_operation[MAX_MSG_CONTENT_SIZE - 1] = '\0';
g_client_state.reply_count = 0; // 重置回复计数
for (int i = 0; i < NUM_NODES; ++i) {
g_client_state.received_reply[i] = false;
}
memset(g_client_state.first_valid_reply_result, 0, MAX_MSG_CONTENT_SIZE); // 清空结果
pthread_mutex_unlock(&g_client_state.reply_mutex);
// ... (省略发送消息代码)
}
// client.c 中 receive_reply_thread 函数修改
void *receive_reply_thread(void *arg) {
// ... (省略接收消息头部和payload的代码)
printf("[Client %u] 收到来自 Node %u 的回复:\n", g_client_state.client_id, reply_msg.common_header.sender_id);
print_message(&reply_msg);
// --- 客户端回复验证逻辑 ---
pthread_mutex_lock(&g_client_state.reply_mutex);
// 1. 验证回复是否与最近发送的请求匹配
if (reply_msg.payload.reply.client_id != g_client_state.client_id ||
reply_msg.payload.reply.timestamp != g_client_state.last_sent_timestamp) {
fprintf(stderr, "[Client %u] 警告: 收到不匹配的回复 (客户端ID或时间戳不符)。\n", g_client_state.client_id);
pthread_mutex_unlock(&g_client_state.reply_mutex);
close(node_sock_fd);
return NULL;
}
// 2. 检查是否重复收到此节点的回复
if (g_client_state.received_reply[reply_msg.common_header.sender_id]) {
printf("[Client %u] 警告: 收到重复的回复来自 Node %u。\n", g_client_state.client_id, reply_msg.common_header.sender_id);
pthread_mutex_unlock(&g_client_state.reply_mutex);
close(node_sock_fd);
return NULL;
}
// 3. 首次收到有效回复,记录结果
if (g_client_state.reply_count == 0) {
strncpy(g_client_state.first_valid_reply_result, reply_msg.payload.reply.result, MAX_MSG_CONTENT_SIZE - 1);
g_client_state.first_valid_reply_result[MAX_MSG_CONTENT_SIZE - 1] = '\0';
} else {
// 后续回复,检查结果是否一致
if (strcmp(g_client_state.first_valid_reply_result, reply_msg.payload.reply.result) != 0) {
fprintf(stderr, "[Client %u] 警告: 收到不一致的回复结果!\n", g_client_state.client_id);
// 实际PBFT中,客户端会等待 f+1 个相同结果的回复
// 如果收到不一致的回复,可能表明有拜占庭节点,需要继续等待或触发视图切换
pthread_mutex_unlock(&g_client_state.reply_mutex);
close(node_sock_fd);
return NULL;
}
}
// 4. 增加回复计数
g_client_state.received_reply[reply_msg.common_header.sender_id] = true;
g_client_state.reply_count++;
printf("[Client %u] 当前收集到 %d 个有效回复。\n", g_client_state.client_id, g_client_state.reply_count);
// 5. 判断是否达到 f+1 个相同回复
if (g_client_state.reply_count >= (FAULTY_NODES + 1)) { // f+1 个回复
printf("\n[Client %u] 请求 '%s' 已达成共识并执行!最终结果: '%s'\n",
g_client_state.client_id, g_client_state.last_sent_operation, g_client_state.first_valid_reply_result);
// 可以在这里重置客户端状态,准备发送下一个请求
g_client_state.reply_count = 0; // 重置计数,以便处理下一个请求
}
pthread_mutex_unlock(&g_client_state.reply_mutex);
close(node_sock_fd);
return NULL;
}
更新后的 common.h
(完整版):
#ifndef COMMON_H
#define COMMON_H
#include <stdint.h> // For uint32_t, uint8_t
#include <stdbool.h> // For bool
#include <pthread.h> // For pthread_mutex_t
// 定义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;
// 定义PBFT处理阶段
typedef enum {
PHASE_IDLE,
PHASE_PRE_PREPARE, // 收到PRE-PREPARE (副本节点) 或发送PRE-PREPARE (主节点)
PHASE_PREPARE, // 收到或发送PREPARE
PHASE_COMMIT, // 收到或发送COMMIT
PHASE_REPLY // 收到或发送REPLY
} PBFTPhase;
// PBFT消息日志条目
typedef struct MessageLogEntry {
uint32_t view_id;
uint32_t sequence_num;
uint8_t request_digest[DIGEST_SIZE];
PBFTMessage original_client_request; // 原始客户端请求的副本 (用于执行)
PBFTMessage pre_prepare_msg; // 存储收到的PRE-PREPARE消息
// 存储收到的PREPARE和COMMIT消息的集合 (这里简化为计数,实际应存储消息本身)
bool received_prepare[NUM_NODES]; // 标记是否收到某个节点的PREPARE消息
bool received_commit[NUM_NODES]; // 标记是否收到某个节点的COMMIT消息
int prepare_count; // 收集到的PREPARE消息数量
int commit_count; // 收集到的COMMIT消息数量
bool is_prepared; // 是否已达到Prepared状态
bool is_committed; // 是否已达到Committed状态
bool is_executed; // 是否已执行
struct MessageLogEntry *next; // 链表指针
} MessageLogEntry;
// --- 节点状态信息 ---
typedef struct {
uint8_t node_id; // 当前节点的ID
uint32_t current_view; // 当前视图编号
uint32_t last_executed_seq_num; // 上一个已执行的序列号 (低水位线)
uint32_t next_seq_num; // 下一个可用的序列号 (主节点分配)
bool is_primary; // 是否是主节点
PBFTPhase current_phase; // 当前节点所处的PBFT阶段
MessageLogEntry *message_log_head; // 消息日志链表头
pthread_mutex_t log_mutex; // 保护消息日志的互斥锁
// 实际PBFT节点状态会复杂得多,包括高水位、检查点、视图切换相关消息等
} NodeState;
// --- 辅助函数声明 ---
// 计算消息摘要 (这里简化为简单的哈希,实际应使用SHA256等)
void calculate_digest(const char* data, size_t len, uint8_t* digest_out);
// 打印消息内容 (用于调试)
void print_message(const PBFTMessage* msg);
// 查找或创建消息日志条目
MessageLogEntry* get_or_create_log_entry(uint32_t view_id, uint32_t seq_num, const uint8_t* digest);
// 标记日志条目为Prepared状态
void mark_as_prepared(MessageLogEntry* entry);
// 标记日志条目为Committed状态
void mark_as_committed(MessageLogEntry* entry);
// 执行操作并标记为Executed状态
void execute_operation(MessageLogEntry* entry);
// 清理旧的日志条目 (基于水位线,简化)
void clean_old_log_entries();
#endif // COMMON_H
更新后的 node.c
(完整版):
#include "common.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h> // For close(), read(), write(), sleep()
#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(), pthread_mutex_t
#include <errno.h> // For errno
// --- 全局节点状态 ---
NodeState g_node_state;
// --- 线程参数结构体 ---
typedef struct {
int client_sock_fd; // 客户端连接的套接字
uint8_t remote_node_id; // 远程节点的ID
} ThreadArgs;
// --- 全局套接字数组,用于存储与其他节点的连接 ---
// conn_sockets[i] 存储与节点i的连接
// 如果 conn_sockets[i] > 0,表示已连接
int conn_sockets[NUM_NODES];
// --- 互斥锁,保护全局套接字数组 ---
pthread_mutex_t g_conn_sockets_mutex = PTHREAD_MUTEX_INITIALIZER;
// --- 辅助函数声明 (已在common.h中声明,这里是实现) ---
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); // 发送消息给指定节点
void *recv_message_thread(void *arg); // 接收消息的线程函数
// --- PBFT核心处理函数声明 ---
void handle_client_request_msg(const ClientRequest* req_msg);
void handle_pre_prepare_msg(const PrePrepareMsg* pp_msg);
void handle_prepare_msg(const PrepareMsg* p_msg);
void handle_commit_msg(const CommitMsg* c_msg);
// send_reply_to_client 函数在 handle_commit_msg 中直接实现,不再单独声明
/**
* @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_executed_seq_num = 0; // 初始已执行序列号为0
g_node_state.next_seq_num = 0; // 主节点从1开始分配序列号
g_node_state.is_primary = (node_id == (g_node_state.current_view % NUM_NODES)); // 初始主节点是Node 0
g_node_state.current_phase = PHASE_IDLE; // 初始阶段为空闲
// 初始化消息日志链表头和互斥锁
g_node_state.message_log_head = NULL;
pthread_mutex_init(&g_node_state.log_mutex, NULL);
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逻辑主要在recv_message_thread中被动触发
sleep(5); // 避免CPU空转,每5秒检查一次
clean_old_log_entries(); // 周期性清理日志
}
// 等待监听线程结束 (实际不会结束,除非程序退出)
pthread_join(listen_thread, NULL);
close(listen_sock_fd);
pthread_mutex_destroy(&g_node_state.log_mutex); // 销毁互斥锁
pthread_mutex_destroy(&g_conn_sockets_mutex);
}
/**
* @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_conn_sockets_mutex);
conn_sockets[remote_node_id] = client_sock_fd; // 存储连接套接字
pthread_mutex_unlock(&g_conn_sockets_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, recv_message_thread, (void*)args) != 0) {
perror("[Node] 创建接收消息线程失败");
free(args);
close(client_sock_fd);
continue;
}
pthread_detach(recv_thread); // 将线程设置为分离状态,资源在结束时自动回收
}
return NULL;
}
/**
* @brief 连接到其他节点的函数 (作为客户端)
* @param my_id 当前节点的ID
*/
void connect_to_peers(uint8_t my_id) {
for (uint8_t i = 0; i < NUM_NODES; ++i) {
if (i == my_id) {
continue; // 不连接自己
}
// 避免重复连接:如果已经有连接,则跳过
pthread_mutex_lock(&g_conn_sockets_mutex);
if (conn_sockets[i] != -1) {
pthread_mutex_unlock(&g_conn_sockets_mutex);
printf("[Node %u] 已与 Node %u 连接,跳过。\n", my_id, i);
continue;
}
pthread_mutex_unlock(&g_conn_sockets_mutex);
int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (sock_fd == -1) {
perror("[Node] 创建连接套接字失败");
continue;
}
struct sockaddr_in peer_addr;
memset(&peer_addr, 0, sizeof(peer_addr));
peer_addr.sin_family = AF_INET;
peer_addr.sin_port = htons(BASE_NODE_PORT + i);
// 连接到本地IP (127.0.0.1),因为所有节点都在同一台机器上模拟
if (inet_pton(AF_INET, "127.0.0.1", &peer_addr.sin_addr) <= 0) {
perror("[Node] 无效的对端IP地址");
close(sock_fd);
continue;
}
printf("[Node %u] 尝试连接到 Node %u (端口: %d)...\n", my_id, i, BASE_NODE_PORT + i);
// 尝试连接,如果失败(如对端未启动),等待并重试
int retry_count = 0;
const int MAX_CONNECT_RETRIES = 5;
while (connect(sock_fd, (struct sockaddr*)&peer_addr, sizeof(peer_addr)) == -1) {
if (errno == ECONNREFUSED && retry_count < MAX_CONNECT_RETRIES) {
printf("[Node %u] 连接 Node %u 失败 (连接被拒绝),重试中... (%d/%d)\n", my_id, i, retry_count + 1, MAX_CONNECT_RETRIES);
sleep(1); // 等待1秒后重试
retry_count++;
} else {
perror("[Node] 连接对端失败");
close(sock_fd);
sock_fd = -1; // 标记连接失败
break;
}
}
if (sock_fd != -1) {
pthread_mutex_lock(&g_conn_sockets_mutex);
conn_sockets[i] = sock_fd; // 存储连接套接字
pthread_mutex_unlock(&g_conn_sockets_mutex);
printf("[Node %u] 已与 Node %u 建立连接 (作为客户端)。\n", my_id, i);
// 为每个连接启动一个独立的线程来接收消息
pthread_t recv_thread;
ThreadArgs* args = (ThreadArgs*)malloc(sizeof(ThreadArgs));
if (args == NULL) {
perror("分配线程参数失败");
close(sock_fd);
continue;
}
args->client_sock_fd = sock_fd;
args->remote_node_id = i;
if (pthread_create(&recv_thread, NULL, recv_message_thread, (void*)args) != 0) {
perror("[Node] 创建接收消息线程失败");
free(args);
close(sock_fd);
continue;
}
pthread_detach(recv_thread); // 将线程设置为分离状态
}
}
}
/**
* @brief 接收消息的线程函数
* @param arg 线程参数 (包含套接字文件描述符和远程节点ID)
*/
void *recv_message_thread(void *arg) {
ThreadArgs* args = (ThreadArgs*)arg;
int sock_fd = args->client_sock_fd;
uint8_t remote_node_id = args->remote_node_id;
free(args); // 释放线程参数内存
PBFTMessage msg_buffer;
ssize_t bytes_received;
printf("[Node %u] 接收线程启动,监听来自 Node %u 的消息...\n", g_node_state.node_id, remote_node_id);
while (1) {
// 接收消息头部
bytes_received = recv(sock_fd, &msg_buffer.common_header, sizeof(MessageHeader), MSG_WAITALL);
if (bytes_received <= 0) {
if (bytes_received == 0) {
printf("[Node %u] Node %u 断开连接。\n", g_node_state.node_id, remote_node_id);
} else {
perror("[Node] 接收消息头部失败");
}
break; // 连接断开或出错
}
// 根据消息类型接收剩余的payload
size_t payload_size = 0;
switch (msg_buffer.common_header.type) {
case MSG_TYPE_CLIENT_REQUEST: payload_size = sizeof(ClientRequest) - sizeof(MessageHeader); break;
case MSG_TYPE_PRE_PREPARE: payload_size = sizeof(PrePrepareMsg) - sizeof(MessageHeader); break;
case MSG_TYPE_PREPARE: payload_size = sizeof(PrepareMsg) - sizeof(MessageHeader); break;
case MSG_TYPE_COMMIT: payload_size = sizeof(CommitMsg) - sizeof(MessageHeader); break;
case MSG_TYPE_REPLY: payload_size = sizeof(ReplyMsg) - sizeof(MessageHeader); break;
// 其他消息类型待添加
default:
fprintf(stderr, "[Node %u] 接收到未知消息类型 %u,跳过payload接收。\n", g_node_state.node_id, msg_buffer.common_header.type);
break;
}
if (payload_size > 0) {
bytes_received = recv(sock_fd, &msg_buffer.payload, payload_size, MSG_WAITALL);
if (bytes_received <= 0) {
if (bytes_received == 0) {
printf("[Node %u] Node %u 断开连接 (接收payload时)。\n", g_node_state.node_id, remote_node_id);
} else {
perror("[Node] 接收消息payload失败");
}
break;
}
if (bytes_received != payload_size) {
fprintf(stderr, "[Node %u] 接收payload长度不匹配!期望 %zu, 实际 %zd。\n", g_node_state.node_id, payload_size, bytes_received);
break;
}
}
// 成功接收一条完整的消息
printf("[Node %u] 收到来自 Node %u 的消息:\n", g_node_state.node_id, msg_buffer.common_header.sender_id);
print_message(&msg_buffer);
// --- PBFT消息处理逻辑 ---
pthread_mutex_lock(&g_node_state.log_mutex); // 保护g_node_state和其他共享资源
switch (msg_buffer.common_header.type) {
case MSG_TYPE_CLIENT_REQUEST:
// 只有主节点处理客户端请求
if (g_node_state.is_primary) {
handle_client_request_msg(&msg_buffer.payload.client_request);
} else {
printf("[Node %u] 警告: 副本节点收到客户端请求,忽略。\n", g_node_state.node_id);
}
break;
case MSG_TYPE_PRE_PREPARE:
// 副本节点处理PRE-PREPARE消息
if (!g_node_state.is_primary) {
handle_pre_prepare_msg(&msg_buffer.payload.pre_prepare);
} else {
printf("[Node %u] 警告: 主节点收到PRE-PREPARE消息,忽略。\n", g_node_state.node_id);
}
break;
case MSG_TYPE_PREPARE:
// 所有节点处理PREPARE消息
handle_prepare_msg(&msg_buffer.payload.prepare);
break;
case MSG_TYPE_COMMIT:
// 所有节点处理COMMIT消息
handle_commit_msg(&msg_buffer.payload.commit);
break;
case MSG_TYPE_REPLY:
// 节点通常不处理REPLY消息,REPLY是发给客户端的
printf("[Node %u] 收到REPLY消息,忽略 (REPLY是发给客户端的)。\n", g_node_state.node_id);
break;
default:
fprintf(stderr, "[Node %u] 未知消息类型 %u,无法处理。\n", g_node_state.node_id, msg_buffer.common_header.type);
break;
}
pthread_mutex_unlock(&g_node_state.log_mutex);
}
// 连接断开,清理资源
pthread_mutex_lock(&g_conn_sockets_mutex);
if (conn_sockets[remote_node_id] == sock_fd) { // 确保是当前连接
conn_sockets[remote_node_id] = -1;
}
pthread_mutex_unlock(&g_conn_sockets_mutex);
close(sock_fd);
printf("[Node %u] 与 Node %u 的连接已关闭。\n", g_node_state.node_id, remote_node_id);
return NULL;
}
/**
* @brief 发送消息给指定节点
* @param target_node_id 目标节点ID
* @param msg 要发送的消息指针
*/
void send_message_to_node(uint8_t target_node_id, const PBFTMessage* msg) {
pthread_mutex_lock(&g_conn_sockets_mutex);
int sock_fd = conn_sockets[target_node_id];
pthread_mutex_unlock(&g_conn_sockets_mutex);
if (sock_fd == -1) {
fprintf(stderr, "[Node %u] 错误: 无法发送消息给 Node %u,连接未建立或已断开。\n", g_node_state.node_id, target_node_id);
return;
}
// 计算要发送的消息总长度
size_t total_msg_len = sizeof(MessageHeader);
switch (msg->common_header.type) {
case MSG_TYPE_CLIENT_REQUEST: total_msg_len += sizeof(ClientRequest) - sizeof(MessageHeader); break;
case MSG_TYPE_PRE_PREPARE: total_msg_len += sizeof(PrePrepareMsg) - sizeof(MessageHeader); break;
case MSG_TYPE_PREPARE: total_msg_len += sizeof(PrepareMsg) - sizeof(MessageHeader); break;
case MSG_TYPE_COMMIT: total_msg_len += sizeof(CommitMsg) - sizeof(MessageHeader); break;
case MSG_TYPE_REPLY: total_msg_len += sizeof(ReplyMsg) - sizeof(MessageHeader); break;
default:
fprintf(stderr, "[Node %u] 错误: 尝试发送未知消息类型 %u。\n", g_node_state.node_id, msg->common_header.type);
return;
}
// 发送消息
ssize_t bytes_sent = send(sock_fd, msg, total_msg_len, 0);
if (bytes_sent == -1) {
perror("[Node] 发送消息失败");
} else if (bytes_sent != total_msg_len) {
fprintf(stderr, "[Node %u] 警告: 发送消息长度不匹配!期望 %zu, 实际 %zd。\n", g_node_state.node_id, total_msg_len, bytes_sent);
} else {
printf("[Node %u] 成功发送消息 (类型: %u, 序列号: %u) 给 Node %u。\n",
g_node_state.node_id, msg->common_header.type, msg->common_header.sequence_num, target_node_id);
}
}
/**
* @brief 处理客户端请求消息 (仅主节点调用)
* @param req_msg 客户端请求消息
*/
void handle_client_request_msg(const ClientRequest* req_msg) {
printf("[Node %u] (主节点) 收到客户端 %u 的请求: '%s'\n",
g_node_state.node_id, req_msg->client_id, req_msg->operation);
// 1. 验证请求 (简化:这里只检查时间戳,实际需要签名验证、防重放等)
// 假设时间戳必须大于0
if (req_msg->timestamp == 0) {
fprintf(stderr, "[Node %u] 错误: 无效的客户端请求 (时间戳为0)。\n", g_node_state.node_id);
// 实际PBFT会向客户端发送错误回复
return;
}
// 2. 分配序列号
// 序列号必须是递增的,且在水位线范围内
// 这里简化为直接递增,不考虑高水位线和检查点
g_node_state.next_seq_num++; // 分配下一个序列号
uint32_t current_seq_num = g_node_state.next_seq_num;
printf("[Node %u] (主节点) 为请求分配序列号: %u\n", g_node_state.node_id, current_seq_num);
// 3. 构建 PRE-PREPARE 消息
PBFTMessage pp_msg;
memset(&pp_msg, 0, sizeof(PBFTMessage));
pp_msg.common_header.type = MSG_TYPE_PRE_PREPARE;
pp_msg.common_header.view_id = g_node_state.current_view;
pp_msg.common_header.sequence_num = current_seq_num;
pp_msg.common_header.sender_id = g_node_state.node_id;
pp_msg.payload.pre_prepare.client_id = req_msg->client_id;
pp_msg.payload.pre_prepare.timestamp = req_msg->timestamp;
strncpy(pp_msg.payload.pre_prepare.operation, req_msg->operation, MAX_MSG_CONTENT_SIZE - 1);
memcpy(pp_msg.payload.pre_prepare.request_digest, req_msg->digest, DIGEST_SIZE);
// 4. 存储到消息日志 (主节点也需要存储)
MessageLogEntry* log_entry = get_or_create_log_entry(g_node_state.current_view, current_seq_num, req_msg->digest);
memcpy(&log_entry->original_client_request, req_msg, sizeof(ClientRequest)); // 存储原始请求
memcpy(&log_entry->pre_prepare_msg, &pp_msg.payload.pre_prepare, sizeof(PrePrepareMsg)); // 存储PRE-PREPARE消息
// 5. 广播 PRE-PREPARE 消息给所有副本节点
for (uint8_t i = 0; i < NUM_NODES; ++i) {
if (i == g_node_state.node_id) continue; // 不发给自己
send_message_to_node(i, &pp_msg);
}
// 主节点自己也进入Prepared状态 (因为自己发了PRE-PREPARE)
// 并且为自己增加一个PREPARE计数
log_entry->prepare_count++;
log_entry->received_prepare[g_node_state.node_id] = true;
printf("[Node %u] (主节点) 自身PREPARE计数: %d\n", g_node_state.node_id, log_entry->prepare_count);
// 检查是否达到Prepared状态 (主节点也需要收集PREPARE)
// 注意:主节点在发送PRE-PREPARE后,自身就认为进入了Prepared状态,并立即发送COMMIT
// PBFT协议中,主节点在发送PRE-PREPARE后,会等待2f个PREPARE消息,然后进入Prepared状态
// 这里简化为:主节点发送PRE-PREPARE后,自身PREPARE计数+1,然后等待其他PREPARE消息
// 当自身PREPARE计数达到2f+1时,才进入Prepared状态并发送COMMIT
// 这个逻辑与副本节点一致,简化了实现
if (log_entry->prepare_count >= (2 * FAULTY_NODES + 1)) { // 2f+1个PREPARE
mark_as_prepared(log_entry);
// 如果达到Prepared状态,主节点也需要广播COMMIT消息
PBFTMessage commit_msg;
memset(&commit_msg, 0, sizeof(PBFTMessage));
commit_msg.common_header.type = MSG_TYPE_COMMIT;
commit_msg.common_header.view_id = g_node_state.current_view;
commit_msg.common_header.sequence_num = current_seq_num;
commit_msg.common_header.sender_id = g_node_state.node_id;
memcpy(commit_msg.payload.commit.request_digest, req_msg->digest, DIGEST_SIZE);
for (uint8_t i = 0; i < NUM_NODES; ++i) {
send_message_to_node(i, &commit_msg);
}
// 主节点自己也增加一个COMMIT计数
log_entry->commit_count++;
log_entry->received_commit[g_node_state.node_id] = true;
printf("[Node %u] (主节点) 自身COMMIT计数: %d\n", g_node_state.node_id, log_entry->commit_count);
}
}
/**
* @brief 处理PRE-PREPARE消息 (仅副本节点调用)
* @param pp_msg PRE-PREPARE消息
*/
void handle_pre_prepare_msg(const PrePrepareMsg* pp_msg) {
printf("[Node %u] (副本节点) 收到来自主节点 %u 的PRE-PREPARE消息 (视图 %u, 序列号 %u)\n",
g_node_state.node_id, pp_msg->common_header.sender_id, pp_msg->common_header.view_id, pp_msg->common_header.sequence_num);
// 1. 验证PRE-PREPARE消息
// 验证视图ID是否匹配
if (pp_msg->common_header.view_id != g_node_state.current_view) {
fprintf(stderr, "[Node %u] 错误: PRE-PREPARE视图ID不匹配 (期望 %u, 实际 %u)。\n",
g_node_state.node_id, g_node_state.current_view, pp_msg->common_header.view_id);
// 实际PBFT会触发视图切换
return;
}
// 验证发送者是否是当前视图的主节点
if (pp_msg->common_header.sender_id != (g_node_state.current_view % NUM_NODES)) {
fprintf(stderr, "[Node %u] 错误: PRE-PREPARE发送者不是当前主节点 (期望 %u, 实际 %u)。\n",
g_node_state.node_id, (g_node_state.current_view % NUM_NODES), pp_msg->common_header.sender_id);
// 实际PBFT会触发视图切换
return;
}
// 验证序列号是否在有效范围内 (简化:这里只检查大于已执行的序列号)
if (pp_msg->common_header.sequence_num <= g_node_state.last_executed_seq_num) {
fprintf(stderr, "[Node %u] 错误: PRE-PREPARE序列号过低 (序列号 %u <= 已执行 %u)。\n",
g_node_state.node_id, pp_msg->common_header.sequence_num, g_node_state.last_executed_seq_num);
return;
}
// 验证消息摘要是否正确 (这里依赖简化哈希)
uint8_t calculated_digest[DIGEST_SIZE];
calculate_digest(pp_msg->operation, strlen(pp_msg->operation), calculated_digest);
if (memcmp(calculated_digest, pp_msg->request_digest, DIGEST_SIZE) != 0) {
fprintf(stderr, "[Node %u] 错误: PRE-PREPARE摘要不匹配!\n", g_node_state.node_id);
return;
}
// 2. 存储到消息日志
MessageLogEntry* log_entry = get_or_create_log_entry(pp_msg->common_header.view_id,
pp_msg->common_header.sequence_num,
pp_msg->request_digest);
// 检查是否已经收到过相同 v, n 但不同 d 的 PRE-PREPARE 消息 (防止主节点作恶)
if (log_entry->pre_prepare_msg.common_header.type == MSG_TYPE_PRE_PREPARE &&
memcmp(log_entry->pre_prepare_msg.payload.pre_prepare.request_digest, pp_msg->request_digest, DIGEST_SIZE) != 0) {
fprintf(stderr, "[Node %u] 警告: 收到冲突的PRE-PREPARE消息 (视图 %u, 序列号 %u),触发视图切换!\n",
g_node_state.node_id, pp_msg->common_header.view_id, pp_msg->common_header.sequence_num);
// 实际PBFT会立刻触发视图切换
return;
}
memcpy(&log_entry->pre_prepare_msg, pp_msg, sizeof(PrePrepareMsg)); // 存储PRE-PREPARE消息
// 存储原始客户端请求 (从PRE-PREPARE中提取)
log_entry->original_client_request.common_header.type = MSG_TYPE_CLIENT_REQUEST;
log_entry->original_client_request.common_header.sender_id = pp_msg->client_id; // 客户端ID作为发送者
log_entry->original_client_request.payload.client_request.client_id = pp_msg->client_id;
log_entry->original_client_request.payload.client_request.timestamp = pp_msg->timestamp;
strncpy(log_entry->original_client_request.payload.client_request.operation, pp_msg->operation, MAX_MSG_CONTENT_SIZE - 1);
memcpy(log_entry->original_client_request.payload.client_request.digest, pp_msg->request_digest, DIGEST_SIZE);
// 3. 构建 PREPARE 消息
PBFTMessage p_msg;
memset(&p_msg, 0, sizeof(PBFTMessage));
p_msg.common_header.type = MSG_TYPE_PREPARE;
p_msg.common_header.view_id = g_node_state.current_view;
p_msg.common_header.sequence_num = pp_msg->common_header.sequence_num;
p_msg.common_header.sender_id = g_node_state.node_id;
memcpy(p_msg.payload.prepare.request_digest, pp_msg->request_digest, DIGEST_SIZE);
// 4. 广播 PREPARE 消息给所有节点
for (uint8_t i = 0; i < NUM_NODES; ++i) {
send_message_to_node(i, &p_msg);
}
// 副本节点自己也增加一个PREPARE计数
log_entry->prepare_count++;
log_entry->received_prepare[g_node_state.node_id] = true;
printf("[Node %u] 自身PREPARE计数: %d\n", g_node_state.node_id, log_entry->prepare_count);
}
/**
* @brief 处理PREPARE消息 (所有节点调用)
* @param p_msg PREPARE消息
*/
void handle_prepare_msg(const PrepareMsg* p_msg) {
printf("[Node %u] 收到来自 Node %u 的PREPARE消息 (视图 %u, 序列号 %u)\n",
g_node_state.node_id, p_msg->common_header.sender_id, p_msg->common_header.view_id, p_msg->common_header.sequence_num);
// 1. 验证PREPARE消息
// 视图ID、序列号是否匹配当前处理的请求 (简化:这里只检查视图ID)
if (p_msg->common_header.view_id != g_node_state.current_view) {
fprintf(stderr, "[Node %u] 错误: PREPARE视图ID不匹配 (期望 %u, 实际 %u)。\n",
g_node_state.node_id, g_node_state.current_view, p_msg->common_header.view_id);
return;
}
// 检查发送者ID是否合法
if (p_msg->common_header.sender_id >= NUM_NODES) {
fprintf(stderr, "[Node %u] 错误: PREPARE发送者ID非法 %u。\n", g_node_state.node_id, p_msg->common_header.sender_id);
return;
}
// 2. 更新消息日志
MessageLogEntry* log_entry = get_or_create_log_entry(p_msg->common_header.view_id,
p_msg->common_header.sequence_num,
p_msg->request_digest);
// 检查是否已经收到过此节点的PREPARE消息
pthread_mutex_lock(&g_node_state.log_mutex); // 保护日志条目
if (log_entry->received_prepare[p_msg->common_header.sender_id]) {
printf("[Node %u] 警告: 收到重复的PREPARE消息来自 Node %u (视图 %u, 序列号 %u)。\n",
g_node_state.node_id, p_msg->common_header.sender_id, p_msg->common_header.view_id, p_msg->common_header.sequence_num);
pthread_mutex_unlock(&g_node_state.log_mutex);
return;
}
log_entry->received_prepare[p_msg->common_header.sender_id] = true;
log_entry->prepare_count++;
printf("[Node %u] 消息 (视图 %u, 序列号 %u) PREPARE计数: %d\n",
g_node_state.node_id, log_entry->view_id, log_entry->sequence_num, log_entry->prepare_count);
// 3. 判断是否达到Prepared状态
// PBFT要求收集 2f+1 个PREPARE消息 (包括自己发送的) 才能进入Prepared状态
if (!log_entry->is_prepared && log_entry->prepare_count >= (2 * FAULTY_NODES + 1)) {
mark_as_prepared(log_entry);
// 4. 构建 COMMIT 消息并广播
PBFTMessage commit_msg;
memset(&commit_msg, 0, sizeof(PBFTMessage));
commit_msg.common_header.type = MSG_TYPE_COMMIT;
commit_msg.common_header.view_id = g_node_state.current_view;
commit_msg.common_header.sequence_num = log_entry->sequence_num;
commit_msg.common_header.sender_id = g_node_state.node_id;
memcpy(commit_msg.payload.commit.request_digest, log_entry->request_digest, DIGEST_SIZE);
for (uint8_t i = 0; i < NUM_NODES; ++i) {
send_message_to_node(i, &commit_msg);
}
// 节点自己也增加一个COMMIT计数
log_entry->commit_count++;
log_entry->received_commit[g_node_state.node_id] = true;
printf("[Node %u] 自身COMMIT计数: %d\n", g_node_state.node_id, log_entry->commit_count);
}
pthread_mutex_unlock(&g_node_state.log_mutex);
}
/**
* @brief 处理COMMIT消息 (所有节点调用)
* @param c_msg COMMIT消息
*/
void handle_commit_msg(const CommitMsg* c_msg) {
printf("[Node %u] 收到来自 Node %u 的COMMIT消息 (视图 %u, 序列号 %u)\n",
g_node_state.node_id, c_msg->common_header.sender_id, c_msg->common_header.view_id, c_msg->common_header.sequence_num);
// 1. 验证COMMIT消息
// 视图ID、序列号是否匹配当前处理的请求 (简化:只检查视图ID)
if (c_msg->common_header.view_id != g_node_state.current_view) {
fprintf(stderr, "[Node %u] 错误: COMMIT视图ID不匹配 (期望 %u, 实际 %u)。\n",
g_node_state.node_id, g_node_state.current_view, c_msg->common_header.view_id);
return;
}
// 检查发送者ID是否合法
if (c_msg->common_header.sender_id >= NUM_NODES) {
fprintf(stderr, "[Node %u] 错误: COMMIT发送者ID非法 %u。\n", g_node_state.node_id, c_msg->common_header.sender_id);
return;
}
// 2. 更新消息日志
MessageLogEntry* log_entry = get_or_create_log_entry(c_msg->common_header.view_id,
c_msg->common_header.sequence_num,
c_msg->request_digest);
// 检查是否已经收到过此节点的COMMIT消息
pthread_mutex_lock(&g_node_state.log_mutex); // 保护日志条目
if (log_entry->received_commit[c_msg->common_header.sender_id]) {
printf("[Node %u] 警告: 收到重复的COMMIT消息来自 Node %u (视图 %u, 序列号 %u)。\n",
g_node_state.node_id, c_msg->common_header.sender_id, c_msg->common_header.view_id, c_msg->common_header.sequence_num);
pthread_mutex_unlock(&g_node_state.log_mutex);
return;
}
log_entry->received_commit[c_msg->common_header.sender_id] = true;
log_entry->commit_count++;
printf("[Node %u] 消息 (视图 %u, 序列号 %u) COMMIT计数: %d\n",
g_node_state.node_id, log_entry->view_id, log_entry->sequence_num, log_entry->commit_count);
// 3. 判断是否达到Committed状态
// PBFT要求收集 2f+1 个COMMIT消息 (包括自己发送的) 才能进入Committed状态
if (!log_entry->is_committed && log_entry->commit_count >= (2 * FAULTY_NODES + 1)) {
mark_as_committed(log_entry);
// 4. 执行操作 (如果尚未执行)
if (!log_entry->is_executed) {
execute_operation(log_entry); // 执行操作并更新last_executed_seq_num
clean_old_log_entries(); // 清理旧日志
}
// 5. 构建 REPLY 消息并发送给客户端
// 只有当PBFT节点执行完操作后,才会向客户端发送REPLY
PBFTMessage reply_msg;
memset(&reply_msg, 0, sizeof(PBFTMessage));
reply_msg.common_header.type = MSG_TYPE_REPLY;
reply_msg.common_header.view_id = g_node_state.current_view;
reply_msg.common_header.sequence_num = log_entry->sequence_num;
reply_msg.common_header.sender_id = g_node_state.node_id;
reply_msg.payload.reply.client_id = log_entry->original_client_request.payload.client_request.client_id;
reply_msg.payload.reply.timestamp = log_entry->original_client_request.payload.client_request.timestamp;
// 简化:执行结果为“OK”
snprintf(reply_msg.payload.reply.result, MAX_MSG_CONTENT_SIZE, "OK for op '%s'", log_entry->original_client_request.payload.client_request.operation);
// 客户端连接是临时的,由客户端主动连接,这里需要重新建立连接或使用预存的客户端连接
// 为了简化,我们假设客户端会监听一个固定的端口,节点主动连接过去发送回复
int client_sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (client_sock_fd == -1) {
perror("[Node] 创建客户端回复套接字失败");
pthread_mutex_unlock(&g_node_state.log_mutex);
return;
}
struct sockaddr_in client_addr;
memset(&client_addr, 0, sizeof(client_addr));
client_addr.sin_family = AF_INET;
client_addr.sin_port = htons(CLIENT_PORT + reply_msg.payload.reply.client_id); // 连接到客户端的监听端口
if (inet_pton(AF_INET, "127.0.0.1", &client_addr.sin_addr) <= 0) {
perror("[Node] 无效的客户端IP地址");
close(client_sock_fd);
pthread_mutex_unlock(&g_node_state.log_mutex);
return;
}
printf("[Node %u] 尝试连接客户端 %u (端口: %d) 发送回复...\n", g_node_state.node_id, reply_msg.payload.reply.client_id, CLIENT_PORT + reply_msg.payload.reply.client_id);
if (connect(client_sock_fd, (struct sockaddr*)&client_addr, sizeof(client_addr)) == -1) {
perror("[Node] 连接客户端失败,无法发送回复");
close(client_sock_fd);
pthread_mutex_unlock(&g_node_state.log_mutex);
return;
}
printf("[Node %u] 成功连接客户端 %u,发送回复。\n", g_node_state.node_id, reply_msg.payload.reply.client_id);
size_t total_msg_len = sizeof(ReplyMsg);
ssize_t bytes_sent = send(client_sock_fd, &reply_msg, total_msg_len, 0);
if (bytes_sent == -1) {
perror("[Node] 发送回复失败");
} else if (bytes_sent != total_msg_len) {
fprintf(stderr, "[Node %u] 警告: 发送回复长度不匹配!期望 %zu, 实际 %zd。\n", g_node_state.node_id, total_msg_len, bytes_sent);
} else {
printf("[Node %u] 成功发送回复给客户端 %u。\n", g_node_state.node_id, reply_msg.payload.reply.client_id);
}
close(client_sock_fd);
}
pthread_mutex_unlock(&g_node_state.log_mutex);
}
int main(int argc, char* argv[]) {
if (argc != 2) {
fprintf(stderr, "用法: %s <node_id>\n", argv[0]);
fprintf(stderr, "示例: %s 0 (启动Node 0)\n", argv[0]);
return 1;
}
uint8_t node_id = (uint8_t)atoi(argv[1]);
if (node_id >= NUM_NODES) {
fprintf(stderr, "错误: 节点ID必须小于 %d。\n", NUM_NODES);
return 1;
}
start_node(node_id);
return 0;
}
更新后的 client.c
(完整版):
#include "common.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <time.h> // For time()
#include <pthread.h> // For pthread_mutex_t
// --- 客户端状态信息 ---
typedef struct {
uint8_t client_id;
// 存储已发送请求的记录,以及等待的回复
// 简化:这里只记录最近一次请求的信息
uint32_t last_sent_timestamp;
uint8_t last_sent_digest[DIGEST_SIZE];
char last_sent_operation[MAX_MSG_CONTENT_SIZE];
// 收集到的回复
// 实际需要一个更复杂的数据结构来存储不同请求的回复,并进行去重和计数
// 这里简化为只处理一个请求的回复
int reply_count;
bool received_reply[NUM_NODES]; // 标记是否收到某个节点的回复
// 存储收到的第一个有效回复的结果,用于比较
char first_valid_reply_result[MAX_MSG_CONTENT_SIZE];
pthread_mutex_t reply_mutex; // 保护回复计数的互斥锁
} ClientState;
ClientState g_client_state; // 全局客户端状态
// --- 辅助函数声明 ---
void send_client_request(const char* operation);
void *receive_reply_thread(void *arg); // 接收回复的线程函数
int main(int argc, char* argv[]) {
if (argc != 2) {
fprintf(stderr, "用法: %s <client_id>\n", argv[0]);
fprintf(stderr, "示例: %s 100 (启动Client 100)\n", argv[0]);
return 1;
}
g_client_state.client_id = (uint8_t)atoi(argv[1]);
printf("[Client %u] 启动中...\n", g_client_state.client_id);
pthread_mutex_init(&g_client_state.reply_mutex, NULL); // 初始化互斥锁
// 1. 启动一个线程来接收来自节点的回复 (作为服务器端)
int listen_sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock_fd == -1) {
perror("[Client] 创建监听套接字失败");
exit(EXIT_FAILURE);
}
int optval = 1;
if (setsockopt(listen_sock_fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) == -1) {
perror("[Client] setsockopt SO_REUSEADDR 失败");
close(listen_sock_fd);
exit(EXIT_FAILURE);
}
struct sockaddr_in client_listen_addr;
memset(&client_listen_addr, 0, sizeof(client_listen_addr));
client_listen_addr.sin_family = AF_INET;
client_listen_addr.sin_port = htons(CLIENT_PORT + g_client_state.client_id); // 每个客户端监听不同端口
client_listen_addr.sin_addr.s_addr = INADDR_ANY;
if (bind(listen_sock_fd, (struct sockaddr*)&client_listen_addr, sizeof(client_listen_addr)) == -1) {
perror("[Client] 绑定监听地址失败");
close(listen_sock_fd);
exit(EXIT_FAILURE);
}
if (listen(listen_sock_fd, NUM_NODES) == -1) { // 监听节点数量
perror("[Client] 监听失败");
close(listen_sock_fd);
exit(EXIT_FAILURE);
}
printf("[Client %u] 正在监听回复 (端口: %d)...\n", g_client_state.client_id, CLIENT_PORT + g_client_state.client_id);
pthread_t reply_recv_thread;
if (pthread_create(&reply_recv_thread, NULL, receive_reply_thread, (void*)(long)listen_sock_fd) != 0) {
perror("[Client] 创建回复接收线程失败");
close(listen_sock_fd);
exit(EXIT_FAILURE);
}
pthread_detach(reply_recv_thread); // 分离线程
// 2. 发送客户端请求
char operation_buffer[MAX_MSG_CONTENT_SIZE];
while (1) {
printf("\n[Client %u] 请输入要发送的操作 (例如: 'transfer 100 to Bob', 输入 'quit' 退出):\n", g_client_state.client_id);
if (fgets(operation_buffer, sizeof(operation_buffer), stdin) == NULL) {
continue;
}
operation_buffer[strcspn(operation_buffer, "\n")] = 0; // 移除换行符
if (strcmp(operation_buffer, "quit") == 0) {
printf("[Client %u] 客户端退出。\n", g_client_state.client_id);
break;
}
send_client_request(operation_buffer);
sleep(5); // 等待回复,给节点处理时间
}
close(listen_sock_fd);
pthread_mutex_destroy(&g_client_state.reply_mutex); // 销毁互斥锁
return 0;
}
/**
* @brief 发送客户端请求给主节点 (简化:直接发送给Node 0)
* @param operation 客户端操作内容
*/
void send_client_request(const char* operation) {
PBFTMessage client_msg;
memset(&client_msg, 0, sizeof(PBFTMessage));
// 填充消息头部
client_msg.common_header.type = MSG_TYPE_CLIENT_REQUEST;
client_msg.common_header.view_id = 0; // 客户端不知道当前视图,这里简化为0
client_msg.common_header.sequence_num = 0; // 客户端请求没有序列号,由主节点分配
client_msg.common_header.sender_id = g_client_state.client_id;
// 填充客户端请求内容
client_msg.payload.client_request.client_id = g_client_state.client_id;
client_msg.payload.client_request.timestamp = (uint32_t)time(NULL); // 使用当前时间戳
strncpy(client_msg.payload.client_request.operation, operation, MAX_MSG_CONTENT_SIZE - 1);
client_msg.payload.client_request.operation[MAX_MSG_CONTENT_SIZE - 1] = '\0';
calculate_digest(client_msg.payload.client_request.operation, strlen(client_msg.payload.client_request.operation), client_msg.payload.client_request.digest);
printf("[Client %u] 准备发送客户端请求:\n", g_client_state.client_id);
print_message(&client_msg);
// 存储最近一次发送请求的信息
pthread_mutex_lock(&g_client_state.reply_mutex);
g_client_state.last_sent_timestamp = client_msg.payload.client_request.timestamp;
memcpy(g_client_state.last_sent_digest, client_msg.payload.client_request.digest, DIGEST_SIZE);
strncpy(g_client_state.last_sent_operation, client_msg.payload.client_request.operation, MAX_MSG_CONTENT_SIZE - 1);
g_client_state.last_sent_operation[MAX_MSG_CONTENT_SIZE - 1] = '\0';
g_client_state.reply_count = 0; // 重置回复计数
for (int i = 0; i < NUM_NODES; ++i) {
g_client_state.received_reply[i] = false;
}
memset(g_client_state.first_valid_reply_result, 0, MAX_MSG_CONTENT_SIZE); // 清空结果
pthread_mutex_unlock(&g_client_state.reply_mutex);
// 连接到主节点 (简化:假设Node 0是主节点)
int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (sock_fd == -1) {
perror("[Client] 创建套接字失败");
return;
}
struct sockaddr_in primary_node_addr;
memset(&primary_node_addr, 0, sizeof(primary_node_addr));
primary_node_addr.sin_family = AF_INET;
primary_node_addr.sin_port = htons(BASE_NODE_PORT + 0); // 连接到Node 0
if (inet_pton(AF_INET, "127.0.0.1", &primary_node_addr.sin_addr) <= 0) {
perror("[Client] 无效的IP地址");
close(sock_fd);
return;
}
printf("[Client %u] 尝试连接到主节点 (Node 0 端口: %d)...\n", g_client_state.client_id, BASE_NODE_PORT + 0);
if (connect(sock_fd, (struct sockaddr*)&primary_node_addr, sizeof(primary_node_addr)) == -1) {
perror("[Client] 连接主节点失败");
close(sock_fd);
return;
}
printf("[Client %u] 成功连接到主节点 (Node 0)。\n", g_client_state.client_id);
// 发送消息
size_t total_msg_len = sizeof(ClientRequest);
ssize_t bytes_sent = send(sock_fd, &client_msg, total_msg_len, 0);
if (bytes_sent == -1) {
perror("[Client] 发送请求失败");
} else if (bytes_sent != total_msg_len) {
fprintf(stderr, "[Client %u] 警告: 发送请求长度不匹配!期望 %zu, 实际 %zd。\n", g_client_state.client_id, total_msg_len, bytes_sent);
} else {
printf("[Client %u] 成功发送请求给主节点。\n", g_client_state.client_id);
}
close(sock_fd); // 发送完请求即可关闭连接
}
/**
* @brief 接收回复的线程函数
* @param arg 监听套接字文件描述符
*/
void *receive_reply_thread(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 node_sock_fd = accept(listen_sock_fd, (struct sockaddr*)&remote_addr, &addr_len);
if (node_sock_fd == -1) {
perror("[Client] 接受节点回复连接失败");
break;
}
inet_ntop(AF_INET, &(remote_addr.sin_addr), remote_ip, INET_ADDRSTRLEN);
printf("[Client %u] 接收到来自 Node %d 的回复连接。\n", g_client_state.client_id, ntohs(remote_addr.sin_port) - BASE_NODE_PORT);
PBFTMessage reply_msg;
ssize_t bytes_received;
// 接收消息头部
bytes_received = recv(node_sock_fd, &reply_msg.common_header, sizeof(MessageHeader), MSG_WAITALL);
if (bytes_received <= 0) {
perror("[Client] 接收回复头部失败");
close(node_sock_fd);
continue;
}
if (reply_msg.common_header.type != MSG_TYPE_REPLY) {
fprintf(stderr, "[Client %u] 警告: 收到非REPLY消息类型 %u,忽略。\n", g_client_state.client_id, reply_msg.common_header.type);
close(node_sock_fd);
continue;
}
// 接收回复payload
size_t payload_size = sizeof(ReplyMsg) - sizeof(MessageHeader);
bytes_received = recv(node_sock_fd, &reply_msg.payload, payload_size, MSG_WAITALL);
if (bytes_received <= 0) {
perror("[Client] 接收回复payload失败");
close(node_sock_fd);
continue;
}
if (bytes_received != payload_size) {
fprintf(stderr, "[Client %u] 接收回复payload长度不匹配!期望 %zu, 实际 %zd。\n", g_client_state.client_id, payload_size, bytes_received);
close(node_sock_fd);
continue;
}
printf("[Client %u] 收到来自 Node %u 的回复:\n", g_client_state.client_id, reply_msg.common_header.sender_id);
print_message(&reply_msg);
// --- 客户端回复验证逻辑 ---
pthread_mutex_lock(&g_client_state.reply_mutex);
// 1. 验证回复是否与最近发送的请求匹配
// PBFT客户端会根据请求的摘要和时间戳来匹配回复
// 这里简化为只匹配时间戳
if (reply_msg.payload.reply.client_id != g_client_state.client_id ||
reply_msg.payload.reply.timestamp != g_client_state.last_sent_timestamp) {
fprintf(stderr, "[Client %u] 警告: 收到不匹配的回复 (客户端ID或时间戳不符)。\n", g_client_state.client_id);
pthread_mutex_unlock(&g_client_state.reply_mutex);
close(node_sock_fd);
return NULL;
}
// 2. 检查是否重复收到此节点的回复
if (g_client_state.received_reply[reply_msg.common_header.sender_id]) {
printf("[Client %u] 警告: 收到重复的回复来自 Node %u。\n", g_client_state.client_id, reply_msg.common_header.sender_id);
pthread_mutex_unlock(&g_client_state.reply_mutex);
close(node_sock_fd);
return NULL;
}
// 3. 首次收到有效回复,记录结果
if (g_client_state.reply_count == 0) {
strncpy(g_client_state.first_valid_reply_result, reply_msg.payload.reply.result, MAX_MSG_CONTENT_SIZE - 1);
g_client_state.first_valid_reply_result[MAX_MSG_CONTENT_SIZE - 1] = '\0';
} else {
// 后续回复,检查结果是否一致
if (strcmp(g_client_state.first_valid_reply_result, reply_msg.payload.reply.result) != 0) {
fprintf(stderr, "[Client %u] 警告: 收到不一致的回复结果!\n", g_client_state.client_id);
// 实际PBFT中,客户端会等待 f+1 个相同结果的回复
// 如果收到不一致的回复,可能表明有拜占庭节点,需要继续等待或触发视图切换
pthread_mutex_unlock(&g_client_state.reply_mutex);
close(node_sock_fd);
return NULL;
}
}
// 4. 增加回复计数
g_client_state.received_reply[reply_msg.common_header.sender_id] = true;
g_client_state.reply_count++;
printf("[Client %u] 当前收集到 %d 个有效回复。\n", g_client_state.client_id, g_client_state.reply_count);
// 5. 判断是否达到 f+1 个相同回复
if (g_client_state.reply_count >= (FAULTY_NODES + 1)) { // f+1 个回复
printf("\n[Client %u] 请求 '%s' 已达成共识并执行!最终结果: '%s'\n",
g_client_state.client_id, g_client_state.last_sent_operation, g_client_state.first_valid_reply_result);
// 可以在这里重置客户端状态,准备发送下一个请求
g_client_state.reply_count = 0; // 重置计数,以便处理下一个请求
// 实际客户端这里会通知应用层请求完成
}
pthread_mutex_unlock(&g_client_state.reply_mutex);
close(node_sock_fd);
}
return NULL;
}
运行这个PBFT仿真器:
-
更新文件: 将上述所有更新后的代码分别替换掉你之前的
common.h
、utils.c
、node.c
、client.c
。 -
打开WSL2终端 (或Linux终端): 进入你保存文件的目录。
-
重新编译: 在终端中执行
make clean
然后make
-
确保编译成功,没有错误。
-
-
启动节点: 打开四个独立的WSL2终端窗口。在每个窗口中分别启动一个PBFT节点:
-
终端1:
./node 0
(Node 0 是主节点) -
终端2:
./node 1
-
终端3:
./node 2
-
终端4:
./node 3
-
观察每个节点的输出,它们会尝试相互连接。当所有节点都启动后,你会看到它们之间建立了连接。Node 0 会显示它是主节点。
-
-
启动客户端: 打开第五个WSL2终端窗口。
-
终端5:
./client 100
(启动Client 100) -
客户端会尝试连接到Node 0,并提示你输入操作。
-
-
发送请求: 在客户端终端输入一个操作,例如
transfer 100 to Bob
,然后按回车。-
观察所有终端的输出! 你会看到:
-
客户端发送
CLIENT_REQUEST
给Node 0。 -
Node 0 收到
CLIENT_REQUEST
,分配序列号,然后广播PRE-PREPARE
给Node 1, 2, 3。 -
Node 1, 2, 3 收到
PRE-PREPARE
,验证后广播PREPARE
给所有节点(包括Node 0)。 -
所有节点(包括Node 0)收到
PREPARE
消息,计数。当某个节点收集到2f+1
个PREPARE
后,它会广播COMMIT
消息。 -
所有节点收到
COMMIT
消息,计数。当某个节点收集到2f+1
个COMMIT
后,它会执行操作(打印“执行操作”),然后向客户端发送REPLY
。 -
客户端收到来自不同节点的
REPLY
消息,当收集到f+1
个相同的REPLY
后,客户端会确认请求已达成共识并执行。
-
-
恭喜你!你现在已经拥有了一个能够模拟PBFT三阶段提交协议的C语言仿真器了!这可不是简单的“Hello World”,这是分布式共识算法的真实模拟!
第六章:当前仿真的局限性与未来展望
现在你已经亲手实现了PBFT的三阶段提交协议,是不是感觉对共识算法的理解又上了一个台阶?但作为一名硬核的C语言程序员,我们也要清醒地认识到,当前的仿真器还存在一些局限性。
6.1 当前仿真的局限性回顾
-
简化的消息摘要: 目前的
calculate_digest
函数只是一个简单的哈希,不具备加密安全性。在实际PBFT中,消息的完整性和不可伪造性依赖于安全的加密哈希(如SHA256)和数字签名。 -
无数字签名: PBFT协议要求所有消息都进行数字签名,以验证发送者的身份和消息的完整性。我们的仿真中没有实现签名和验签机制。
-
简化的消息日志管理: 消息日志目前只是一个简单的链表,没有考虑高效的查找(例如哈希表)、持久化存储、以及更高级的水位线管理(高水位线、检查点)。
-
无视图切换机制: 这是PBFT的关键容错机制,当主节点故障或作恶时,系统无法自动恢复。
-
无故障注入: 无法模拟节点宕机、网络延迟、消息丢失或恶意节点发送错误消息等故障情况,因此无法真正验证PBFT的拜占庭容错能力。
-
简化的客户端回复: 客户端只简单地等待
f+1
个回复,没有处理超时、重复请求等复杂逻辑。 -
单线程消息处理: 虽然每个连接都有一个接收线程,但PBFT核心逻辑(
handle_xxx_msg
)是在主线程的互斥锁保护下顺序执行的,这限制了并发处理能力。真实的PBFT节点会使用事件驱动或线程池来并发处理消息。 -
无状态机复制: 节点只是“打印”执行操作,没有真正维护一个共享的状态机(例如一个共享的账本)。
6.2 下一步:视图切换机制的实现预告
在接下来的篇章中,我们将着重解决上述部分局限性,特别是PBFT的另一个核心机制——视图切换 (View Change)!
视图切换是PBFT算法保证活性的关键。当主节点出现故障(如宕机,或者它不发送PRE-PREPARE
消息,或者发送了无效消息)时,副本节点需要能够检测到并触发视图切换,选举一个新的主节点,并从旧视图的状态中恢复,继续推进共识。
实现视图切换将涉及:
-
计时器机制: 副本节点需要启动计时器,如果长时间未收到主节点的消息,则触发视图切换。
-
VIEW-CHANGE
消息: 副本节点发送给其他节点的视图切换请求。 -
NEW-VIEW
消息: 新主节点在收集到足够多的VIEW-CHANGE
消息后,发送给所有节点的确认消息。 -
状态恢复: 新主节点需要从
VIEW-CHANGE
消息中包含的信息,恢复未完成的请求状态。
这将是PBFT仿真器从“正常运行”到“容错运行”的关键一步!
6.3 性能优化与安全性增强(概念性提及)
在更高级的PBFT仿真中,我们还会考虑:
-
消息压缩与序列化: 优化消息传输效率。
-
非对称加密(数字签名): 引入OpenSSL等库实现消息的签名和验签,确保安全性。
-
更高效的消息日志: 使用哈希表或数据库来管理消息日志,提高查找效率。
-
多核并行处理: 利用线程池或事件驱动模型,提高节点的消息处理吞吐量。
-
网络模拟: 引入网络延迟、丢包等模拟,更真实地测试算法性能。
这些高级特性,将让你的PBFT仿真器更接近真实世界的分布式系统!
小结与展望
老铁们,恭喜你!你已经成功闯过了PBFT算法C语言仿真的第二关:三阶段提交协议的实现!
在这一部分中,我们:
-
设计并实现了PBFT节点的核心状态管理,包括状态机阶段和消息日志(链表实现)。
-
逐个实现了PBFT三阶段提交协议的C语言逻辑:
-
主节点处理
CLIENT_REQUEST
并广播PRE-PREPARE
。 -
副本节点处理
PRE-PREPARE
并广播PREPARE
。 -
所有节点收集
PREPARE
并广播COMMIT
。 -
所有节点收集
COMMIT
,执行操作并向客户端发送REPLY
。
-
-
实现了客户端收集
f+1
个回复并确认请求的逻辑。
现在,你的PBFT仿真器已经能够在一个无故障的场景下,完整地走完从客户端请求到共识达成并回复的全过程了!这绝对是值得你炫耀的硬核技能!
请记住,共识算法的魅力在于它解决了分布式系统中“信任”和“一致性”的难题。通过C语言亲手实现它,你将对这些抽象概念有更深刻的理解。这对于你未来在嵌入式、区块链、分布式系统等领域的职业发展,将是巨大的财富!
敬请期待我的下一次更新——我们将深入PBFT的视图切换机制,让你的仿真器具备真正的拜占庭容错能力!
如果你在学习过程中有任何疑问,或者对代码有任何改进的想法,随时在评论区告诉我,咱们一起交流,!!!