前言:PBFT的“超能力”——视图切换的奥秘
老铁们,经过上两篇的洗礼,你已经用C语言亲手搭建了一个能跑通PBFT三阶段提交协议的仿真器。是不是感觉自己对“共识”这个词有了全新的理解?
但是,PBFT之所以能成为区块链和分布式系统中的“明星”,并不仅仅因为它能在正常情况下达成共识。它真正的“超能力”,是在面对“叛徒”(拜占庭节点)或者“掉线”(主节点故障)时,依然能够保证系统一致性和活性!
而实现这个“超能力”的核心,就是我们今天要深入剖析的——视图切换(View Change)机制!
想象一下,如果当前的主节点突然宕机了,或者它开始作恶,不按规矩发送消息,甚至发送了冲突的消息,整个系统是不是就停摆了?PBFT可不允许这种情况发生!它有一套完善的“自我修复”机制,能够在检测到主节点异常时,迅速“换届选举”,选出新的主节点,并让系统恢复正常运行。
今天,我们就来彻底揭开PBFT“容错心脏”的秘密!这部分内容会更加深入,代码量也会让你“吃到撑”,但每一行代码都将带着你理解PBFT在面对故障时的“力挽狂澜”!
我们将手把手带你:
-
理解视图切换的“为什么”和“怎么触发”!
-
深入剖析
VIEW-CHANGE
和NEW-VIEW
这两种核心消息的“内涵”! -
用C语言实现视图切换的每一个逻辑节点,让你的仿真器真正具备“拜占庭容错”的能力!
-
甚至,我们还会加入一个简单的“故障注入”机制,让你亲手模拟主节点“宕机”,然后看系统如何“自我修复”!
准备好了吗?系好安全带,咱们这就深入PBFT的“容错心脏”,让你的C语言PBFT仿真器,从“正常运行”模式切换到“故障自愈”模式!
第七章:视图切换机制——PBFT的“容错心脏”
视图切换是PBFT算法保证活性的关键。它确保了即使主节点出现故障(崩溃或作恶),系统也能继续处理客户端请求。
7.1 为什么需要视图切换?
在PBFT的三阶段提交过程中,主节点扮演着核心的协调者角色。它负责接收客户端请求、分配序列号、并广播 PRE-PREPARE
消息。如果主节点出现以下任何一种异常,都可能导致共识过程停滞:
-
主节点崩溃: 主节点直接宕机,不再发送任何消息。
-
主节点作恶(恶意行为):
-
主节点故意不发送
PRE-PREPARE
消息。 -
主节点发送了无效的
PRE-PREPARE
消息(例如,视图ID错误、序列号不合法、摘要不匹配)。 -
主节点为同一个序列号发送了不同请求的
PRE-PREPARE
消息(双花攻击)。 -
主节点不响应客户端的请求。
-
在这些情况下,如果系统没有一种机制来替换掉这个有问题的主节点,那么整个系统就会“卡死”,无法继续处理新的请求。视图切换就是为了解决这个问题而设计的。
7.2 视图切换的触发条件:计时器与消息异常
副本节点通过以下方式检测主节点是否异常,并触发视图切换:
-
超时机制 (Timeout):
-
每个副本节点都会为每个客户端请求或每个
PRE-PREPARE
消息启动一个计时器。 -
如果在一个预设的时间内(例如,收到客户端请求后,没有收到主节点的
PRE-PREPARE
消息;或者在PRE-PREPARE
阶段后,没有收到足够的PREPARE
消息),计时器超时,副本节点就认为主节点可能存在问题。 -
这是最常见的触发视图切换的方式。
-
-
消息异常:
-
副本节点收到来自主节点的无效消息(例如,签名验证失败、视图ID不匹配、序列号不合法、摘要错误)。
-
副本节点收到来自主节点的冲突消息(例如,为同一个序列号发送了两个不同内容的
PRE-PREPARE
消息)。
-
一旦满足触发条件,副本节点就会启动视图切换流程。
7.3 VIEW-CHANGE
消息详解——副本节点的“弹劾书”
当一个副本节点决定触发视图切换时,它会构建并广播一个 VIEW-CHANGE
消息。这个消息是副本节点向其他节点发出的“弹劾书”,表明它认为当前主节点有问题,需要更换。
VIEW-CHANGE
消息结构:
// common.h 中添加 VIEW_CHANGE 消息类型
// 视图切换消息 (由副本节点发送给所有节点)
typedef struct {
MessageHeader header; // 通用消息头 (type = MSG_TYPE_VIEW_CHANGE)
uint32_t new_view_id; // 提议的新视图编号
uint32_t last_stable_checkpoint_seq_num; // 节点已确认的最新检查点序列号 (低水位线)
// P 集合: 包含所有在旧视图中已Prepared但未Committed的请求的PRE-PREPARE消息和PREPARE消息集合
// Q 集合: 包含所有在旧视图中已Prepared的请求的PRE-PREPARE消息和PREPARE消息集合
// 为了简化仿真,这里不直接传输P和Q集合的完整消息,而是传输其摘要和相关信息
// 实际PBFT中,P和Q集合是视图切换的关键,用于新主节点恢复状态
// 我们将简化为只传输 Prepared 状态的请求摘要和序列号
// 实际PBFT中,P和Q集合是视图切换的关键,用于新主节点恢复状态
// 这里用一个数组来模拟P集合,存储Prepared状态的请求的摘要和序列号
// 简化:只存储最近的几个Prepared请求,实际应存储所有未完成的Prepared请求
uint32_t prepared_requests_count;
struct {
uint32_t seq_num;
uint8_t digest[DIGEST_SIZE];
// 实际P集合还包含对应的pre-prepare和prepare消息集合的证明
} prepared_requests[10]; // 简化:最多记录10个Prepared请求
} ViewChangeMsg;
VIEW-CHANGE
消息中的关键信息:
-
new_view_id
: 提议的新视图编号。通常是当前视图编号加1。 -
last_stable_checkpoint_seq_num
: 发送此VIEW-CHANGE
消息的节点所知道的最新稳定检查点的序列号。检查点是PBFT用于垃圾回收和状态恢复的关键。它表示系统在该序列号之前的状态已经稳定,并且所有正常节点都已执行。 -
P
集合: (Prepared messages) 一个集合,包含所有发送者节点在旧视图中已经达到 Prepared 状态但尚未 Committed 的请求的证明。每个证明包括:-
对应的
PRE-PREPARE
消息。 -
2f
个匹配的PREPARE
消息。 -
P
集合是新主节点用来恢复未完成请求的关键。
-
-
Q
集合: (Quorum messages) 一个集合,包含所有发送者节点在旧视图中已经达到 Prepared 状态的所有请求的证明(包括已Committed和未Committed的)。Q
集合用于确保新主节点不会跳过任何已达成Prepared状态的请求。
ER图:VIEW-CHANGE消息结构(简化)
erDiagram
VIEW_CHANGE_MSG {
MessageType header_type
uint32_t header_view_id
uint8_t header_sender_id
uint32_t new_view_id
uint32_t last_stable_checkpoint_seq_num
uint32_t prepared_requests_count
}
VIEW_CHANGE_MSG ||--o{ PREPARED_REQUEST_ENTRY : "contains"
PREPARED_REQUEST_ENTRY {
uint32_t seq_num
uint8_t digest[32]
}
7.4 NEW-VIEW
消息详解——新主节点的“就职宣言”
当新的主节点被选举出来后,它会收集足够多的 VIEW-CHANGE
消息,并构建一个 NEW-VIEW
消息广播给所有节点。这个消息是新主节点的“就职宣言”,通知所有节点进入新的视图,并告知它们需要处理哪些未完成的请求。
NEW-VIEW
消息结构:
// common.h 中添加 NEW_VIEW 消息类型
// 新视图消息 (由新主节点发送给所有节点)
typedef struct {
MessageHeader header; // 通用消息头 (type = MSG_TYPE_NEW_VIEW)
uint32_t new_view_id; // 新视图编号
// V 集合: 收集到的 2f+1 个 VIEW-CHANGE 消息的集合
// 为了简化,这里不直接传输V集合的完整消息
// O 集合: 新主节点重新PRE-PREPARE的请求的PRE-PREPARE消息集合
// 实际O集合是PRE-PREPARE消息的集合
// 这里用一个数组来模拟O集合,存储PRE-PREPARE消息的摘要和序列号
uint32_t pre_prepared_requests_count;
PrePrepareMsg pre_prepared_requests[10]; // 简化:最多记录10个重新PRE-PREPARE的请求
} NewViewMsg;
NEW-VIEW
消息中的关键信息:
-
new_view_id
: 新的视图编号。 -
V
集合: 新主节点收集到的2f+1
个有效的VIEW-CHANGE
消息的集合。这个集合是新主节点合法性的证明。 -
O
集合: 新主节点为所有在视图切换过程中被确定为未完成的请求,重新生成的PRE-PREPARE
消息的集合。这些请求是新主节点从收集到的VIEW-CHANGE
消息的P
集合中恢复出来的。
7.5 视图切换核心流程——PBFT的“自我修复”
思维导图:PBFT视图切换流程
graph TD
A[副本节点计时器超时 或 收到无效消息] --> B[副本节点: 广播 VIEW-CHANGE]
B --(VIEW-CHANGE, new_view_id, P, Q)--> C{所有节点收集VIEW-CHANGE}
C --新主节点收集到2f+1个VIEW-CHANGE--> D[新主节点: 确定新视图状态]
D --> D1[确定新低水位线h']
D --> D2[从P集合恢复未完成请求]
D --> D3[为未完成请求重新PRE-PREPARE (生成O集合)]
D --> E[新主节点: 广播 NEW-VIEW]
E --(NEW-VIEW, new_view_id, V, O)--> F{所有节点接收NEW-VIEW}
F --> F1[验证NEW-VIEW合法性]
F --> F2[更新本地视图ID]
F --> F3[处理O集合中的PRE-PREPARE消息]
F --> G[系统在新视图中恢复正常共识]
详细流程:
-
触发视图切换:
-
每个副本节点维护一个计时器,用于检测主节点是否在预定时间内响应。
-
如果计时器超时,或者副本节点检测到主节点发送了无效或冲突的消息,该副本节点就会启动视图切换。
-
它会递增自己的视图编号(例如,从
v
到v+1
),并构建一个VIEW-CHANGE
消息。 -
VIEW-CHANGE
消息包含:提议的新视图编号v+1
,以及该节点在当前视图v
中所有已 Prepared 但尚未 Committed 的请求的证明(P
集合),以及所有已 Prepared 的请求的证明(Q
集合)。 -
该副本节点将
VIEW-CHANGE
消息广播给所有其他节点(包括它自己)。
-
-
收集
VIEW-CHANGE
消息:-
所有节点都会接收来自其他节点的
VIEW-CHANGE
消息。 -
每个节点会验证收到的
VIEW-CHANGE
消息的合法性(例如,签名、视图编号、P
和Q
集合的有效性)。 -
当一个节点(无论是旧主节点、副本节点,还是未来的新主节点)收集到
2f+1
个来自不同节点的有效VIEW-CHANGE
消息时,它就认为视图切换可以开始了。
-
-
新主节点确定新视图状态:
-
在新的视图
v+1
中,新的主节点是(v+1) % N
。 -
新的主节点在收集到
2f+1
个有效的VIEW-CHANGE
消息后,会执行以下操作:-
确定新的低水位线
h'
: 从所有收集到的VIEW-CHANGE
消息中,找到所有节点报告的last_stable_checkpoint_seq_num
中的最大值。这个值将成为新视图的低水位线。 -
从
P
集合中恢复未完成请求: 新主节点会检查所有收到的VIEW-CHANGE
消息中的P
集合。它会找出所有在旧视图中已经达到 Prepared 状态但尚未 Committed 的请求。 -
为未完成请求重新
PRE-PREPARE
(生成O
集合): 对于这些未完成的请求,新主节点会为它们重新生成PRE-PREPARE
消息,使用新的视图编号v+1
和它们原有的序列号。这些重新生成的PRE-PREPARE
消息构成了O
集合。
-
-
-
新主节点广播
NEW-VIEW
消息:-
新主节点构建一个
NEW-VIEW
消息,其中包含:新的视图编号v+1
,它收集到的2f+1
个VIEW-CHANGE
消息的集合(V
集合),以及它重新PRE-PREPARE
的请求集合(O
集合)。 -
新主节点将
NEW-VIEW
消息广播给所有其他节点。
-
-
所有节点处理
NEW-VIEW
消息:-
所有节点收到
NEW-VIEW
消息后,会验证其合法性(例如,发送者是否是新主节点、V
集合是否包含2f+1
个有效VIEW-CHANGE
消息,并且这些VIEW-CHANGE
消息是否与NEW-VIEW
中的O
集合一致)。 -
如果验证通过,节点会更新自己的
current_view
为new_view_id
。 -
节点会处理
O
集合中的所有PRE-PREPARE
消息,就像它们是正常阶段收到的PRE-PREPARE
消息一样,从而继续推进这些请求的共识过程。 -
对于那些在旧视图中已经达到 Prepared 状态但在新视图中没有收到
PRE-PREPARE
消息的请求,节点会将其标记为“中止”,并等待新的请求。
-
-
系统在新视图中恢复正常共识:
-
一旦所有正常节点都处理了
NEW-VIEW
消息并更新了视图,系统就在新的视图中恢复了正常运行。 -
新的客户端请求将发送给新的主节点,并开始新的三阶段提交过程。
-
面试考点:
-
“详细描述PBFT的视图切换机制。它解决了什么问题?包括哪些消息?” 这是PBFT的难点,也是区分高手的关键。
-
“
VIEW-CHANGE
消息中的P
和Q
集合有什么作用?” 它们是新主节点恢复状态的关键。 -
“为什么新主节点需要收集
2f+1
个VIEW-CHANGE
消息?” 确保至少有一个正常节点在其中,并且能够从正常节点那里获取到正确的状态信息。
7.6 C语言实现:视图切换的“代码之魂”
现在,我们将把上述复杂的视图切换逻辑,用C语言一步步实现。这会涉及到对 common.h
、node.c
和 client.c
的大量修改和新增代码。
首先,更新 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
// 定义视图切换超时时间 (毫秒)
#define VIEW_CHANGE_TIMEOUT_MS 5000 // 5秒
// 定义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]; // 原始客户端请求的摘要
} 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;
// --- 视图切换相关消息 ---
// Prepared请求的简要信息 (用于VIEW-CHANGE的P和Q集合)
typedef struct {
uint32_t seq_num;
uint8_t digest[DIGEST_SIZE];
// 实际PBFT中,这里还包含对应的PRE-PREPARE和PREPARE消息的证明
// 为了简化,我们只传输序列号和摘要
} PreparedRequestInfo;
// 视图切换消息 (由副本节点发送给所有节点)
typedef struct {
MessageHeader header; // 通用消息头 (type = MSG_TYPE_VIEW_CHANGE)
uint32_t new_view_id; // 提议的新视图编号
uint32_t last_stable_checkpoint_seq_num; // 节点已确认的最新检查点序列号 (低水位线)
// P 集合: 包含所有在旧视图中已Prepared但未Committed的请求的简要信息
uint32_t p_set_count;
PreparedRequestInfo p_set[NUM_NODES * 2]; // 简化:假设最多有2N个未完成请求
// 实际PBFT中,P集合是PRE-PREPARE消息和2f个PREPARE消息的集合
// Q 集合: 包含所有在旧视图中已Prepared的请求的简要信息
// 为了简化,我们不显式区分P和Q,只传输P集合,并在新主节点处根据P集合恢复
// 实际Q集合是所有Prepared请求的集合,用于确保新主节点不会跳过任何已Prepared的请求
} ViewChangeMsg;
// 新视图消息 (由新主节点发送给所有节点)
typedef struct {
MessageHeader header; // 通用消息头 (type = MSG_TYPE_NEW_VIEW)
uint32_t new_view_id; // 新视图编号
// V 集合: 收集到的 2f+1 个 VIEW-CHANGE 消息的集合
// 为了简化,这里不直接传输V集合的完整消息,而是传递其发送者ID和提议的新视图ID
uint32_t view_change_proof_count;
uint8_t view_change_senders[NUM_NODES]; // 存储发送VIEW-CHANGE的节点ID
// O 集合: 新主节点重新PRE-PREPARE的请求的PRE-PREPARE消息集合
uint32_t pre_prepared_requests_count;
PrePrepareMsg pre_prepared_requests[NUM_NODES * 2]; // 简化:最多记录2N个重新PRE-PREPARE的请求
} NewViewMsg;
// 统一消息结构体 (使用union节省内存,一次只存储一种消息)
// 方便网络传输和处理
typedef struct {
MessageHeader common_header; // 所有消息共有的头部信息
union {
ClientRequest client_request;
PrePrepareMsg pre_prepare;
PrepareMsg prepare;
CommitMsg commit;
ReplyMsg reply;
ViewChangeMsg view_change; // 新增视图切换消息
NewViewMsg new_view; // 新增新视图消息
// 其他消息类型待添加
} payload; // 消息的具体内容
} PBFTMessage;
// 定义PBFT处理阶段
typedef enum {
PHASE_IDLE,
PHASE_PRE_PREPARE, // 收到PRE-PREPARE (副本节点) 或发送PRE-PREPARE (主节点)
PHASE_PREPARE, // 收到或发送PREPARE
PHASE_COMMIT, // 收到或发送COMMIT
PHASE_REPLY, // 收到或发送REPLY
PHASE_VIEW_CHANGE, // 正在进行视图切换
PHASE_NEW_VIEW // 正在处理新视图
} 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; // 保护消息日志的互斥锁
// 视图切换相关状态
uint32_t view_change_timer_start_time_ms; // 视图切换计时器开始时间
bool view_change_triggered; // 是否已触发视图切换
uint32_t proposed_new_view_id; // 提议的新视图ID
int view_change_count[NUM_NODES]; // 记录每个节点发送的VIEW-CHANGE次数 (简化)
bool received_view_change[NUM_NODES]; // 标记是否收到某个节点的VIEW-CHANGE消息
int total_view_change_count; // 收集到的VIEW-CHANGE消息总数
// 存储收集到的VIEW-CHANGE消息 (实际应存储完整消息,这里简化为计数)
// 存储收集到的NEW-VIEW消息 (实际应存储完整消息,这里简化为计数)
bool received_new_view[NUM_NODES]; // 标记是否收到某个节点的NEW-VIEW消息
int total_new_view_count; // 收集到的NEW-VIEW消息总数
// 故障注入标志 (用于模拟主节点不响应)
bool simulate_primary_failure; // 如果为true,主节点不发送PRE-PREPARE
int failure_node_id; // 要模拟故障的节点ID
} 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();
// 获取当前时间 (毫秒)
uint32_t get_current_time_ms();
#endif // COMMON_H
然后,更新 utils.c
,添加新的辅助函数:
#include "common.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h> // For exit
#include <time.h> // For clock_gettime, CLOCK_MONOTONIC
// 全局节点状态 (在node.c中定义,这里只是声明 extern)
extern NodeState g_node_state;
// 简化版摘要计算 (实际应使用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;
case MSG_TYPE_VIEW_CHANGE:
printf(" 提议新视图ID: %u\n", msg->payload.view_change.new_view_id);
printf(" 最新检查点序列号: %u\n", msg->payload.view_change.last_stable_checkpoint_seq_num);
printf(" Prepared请求数量 (P集合简化): %u\n", msg->payload.view_change.p_set_count);
for (uint32_t i = 0; i < msg->payload.view_change.p_set_count; ++i) {
printf(" SeqNum: %u, Digest: ", msg->payload.view_change.p_set[i].seq_num);
for (int j = 0; j < DIGEST_SIZE; ++j) {
printf("%02x", msg->payload.view_change.p_set[i].digest[j]);
}
printf("\n");
}
break;
case MSG_TYPE_NEW_VIEW:
printf(" 新视图ID: %u\n", msg->payload.new_view.new_view_id);
printf(" VIEW-CHANGE证明数量: %u\n", msg->payload.new_view.view_change_proof_count);
printf(" 重新PRE-PREPARE请求数量 (O集合简化): %u\n", msg->payload.new_view.pre_prepared_requests_count);
for (uint32_t i = 0; i < msg->payload.new_view.pre_prepared_requests_count; ++i) {
printf(" SeqNum: %u, Digest: ", msg->payload.new_view.pre_prepared_requests[i].common_header.sequence_num);
for (int j = 0; j < DIGEST_SIZE; ++j) {
printf("%02x", msg->payload.new_view.pre_prepared_requests[i].request_digest[j]);
}
printf("\n");
}
break;
default:
// 其他消息类型暂不详细打印
break;
}
printf("------------------\n");
}
// 查找或创建消息日志条目
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->is_executed && current->sequence_num <= g_node_state.last_executed_seq_num) {
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);
}
// 获取当前时间 (毫秒)
uint32_t get_current_time_ms() {
struct timespec ts;
// CLOCK_MONOTONIC 是一个单调递增的时钟,不受系统时间调整的影响,适合测量时间间隔
if (clock_gettime(CLOCK_MONOTONIC, &ts) == -1) {
perror("clock_gettime");
return 0;
}
return (uint32_t)(ts.tv_sec * 1000 + ts.tv_nsec / 1000000);
}
最后,重头戏来了!修改 node.c
和 client.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
#include <time.h> // For time() in get_current_time_ms (if not using clock_gettime)
// --- 全局节点状态 ---
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);
void handle_view_change_msg(const ViewChangeMsg* vc_msg); // 新增
void handle_new_view_msg(const NewViewMsg* nv_msg); // 新增
// --- 视图切换相关函数声明 ---
void trigger_view_change(); // 触发视图切换
void start_view_change_timer(); // 启动视图切换计时器
void reset_view_change_state(); // 重置视图切换相关状态
/**
* @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);
// 初始化视图切换相关状态
g_node_state.view_change_triggered = false;
g_node_state.proposed_new_view_id = 0;
g_node_state.total_view_change_count = 0;
g_node_state.total_new_view_count = 0;
for (int i = 0; i < NUM_NODES; ++i) {
g_node_state.view_change_count[i] = 0;
g_node_state.received_view_change[i] = false;
g_node_state.received_new_view[i] = false;
}
// 故障注入默认关闭
g_node_state.simulate_primary_failure = false;
g_node_state.failure_node_id = -1;
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) {
// 副本节点需要启动视图切换计时器
if (!g_node_state.is_primary && !g_node_state.view_change_triggered) {
// 如果当前没有正在处理的请求,或者长时间没有收到主节点的PRE-PREPARE,启动计时器
// 这里简化为:只要不是主节点,就周期性检查是否需要启动计时器
// 实际PBFT中,计时器通常在收到客户端请求后,等待PRE-PREPARE时启动
// 或者在PRE-PREPARE后,等待PREPARE/COMMIT时启动
if (g_node_state.view_change_timer_start_time_ms == 0) {
start_view_change_timer();
} else if (get_current_time_ms() - g_node_state.view_change_timer_start_time_ms > VIEW_CHANGE_TIMEOUT_MS) {
printf("[Node %u] 视图切换计时器超时!触发视图切换。\n", g_node_state.node_id);
trigger_view_change();
}
}
sleep(1); // 每秒检查一次,避免CPU空转
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) { // 可能是客户端或其他未知连接 (客户端端口在9000以上)
// 对于客户端连接,我们不存储到conn_sockets,因为是临时的
printf("[Node %u] 警告: 无法识别远程节点ID %u,可能是客户端连接,不存储到conn_sockets。\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; // 这里的remote_node_id不再是PBFT节点ID,而是客户端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); // 将线程设置为分离状态
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_id = args->remote_node_id; // 可能是节点ID,也可能是客户端ID
free(args); // 释放线程参数内存
PBFTMessage msg_buffer;
ssize_t bytes_received;
printf("[Node %u] 接收线程启动,监听来自 ID %u 的消息...\n", g_node_state.node_id, remote_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] ID %u 断开连接。\n", g_node_state.node_id, remote_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;
case MSG_TYPE_VIEW_CHANGE: payload_size = sizeof(ViewChangeMsg) - sizeof(MessageHeader); break; // 新增
case MSG_TYPE_NEW_VIEW: payload_size = sizeof(NewViewMsg) - 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] ID %u 断开连接 (接收payload时)。\n", g_node_state.node_id, remote_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] 收到来自 ID %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;
case MSG_TYPE_VIEW_CHANGE: // 新增视图切换消息处理
handle_view_change_msg(&msg_buffer.payload.view_change);
break;
case MSG_TYPE_NEW_VIEW: // 新增新视图消息处理
handle_new_view_msg(&msg_buffer.payload.new_view);
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);
}
// 连接断开,清理资源
// 注意:这里需要区分是PBFT节点连接还是客户端连接
// 对于PBFT节点连接,才从conn_sockets中移除
if (remote_id < NUM_NODES) { // 假设PBFT节点ID小于NUM_NODES
pthread_mutex_lock(&g_conn_sockets_mutex);
if (conn_sockets[remote_id] == sock_fd) { // 确保是当前连接
conn_sockets[remote_id] = -1;
}
pthread_mutex_unlock(&g_conn_sockets_mutex);
printf("[Node %u] 与 Node %u 的连接已关闭。\n", g_node_state.node_id, remote_id);
} else {
printf("[Node %u] 与客户端 %u 的连接已关闭。\n", g_node_state.node_id, remote_id);
}
close(sock_fd);
return NULL;
}
/**
* @brief 发送消息给指定节点
* @param target_node_id 目标节点ID
* @param msg 要发送的消息指针
*/
void send_message_to_node(uint8_t target_node_id, const PBFTMessage* msg) {
// 故障注入点:如果当前节点是故障节点,并且要模拟主节点故障,则不发送PRE-PREPARE
if (g_node_state.simulate_primary_failure &&
g_node_state.node_id == g_node_state.failure_node_id &&
msg->common_header.type == MSG_TYPE_PRE_PREPARE) {
printf("[Node %u] (故障模拟) 模拟主节点故障,不发送 PRE-PREPARE 消息。\n", g_node_state.node_id);
return;
}
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;
case MSG_TYPE_VIEW_CHANGE: total_msg_len += sizeof(ViewChangeMsg) - sizeof(MessageHeader); break;
case MSG_TYPE_NEW_VIEW: total_msg_len += sizeof(NewViewMsg) - 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);
}
// 收到客户端请求后,重置视图切换计时器(因为主节点响应了)
start_view_change_timer();
}
/**
* @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);
// 收到主节点的PRE-PREPARE消息,重置视图切换计时器
start_view_change_timer();
// 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会立刻触发视图切换
trigger_view_change();
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);
}
/**
* @brief 触发视图切换
*/
void trigger_view_change() {
pthread_mutex_lock(&g_node_state.log_mutex);
if (g_node_state.view_change_triggered) {
printf("[Node %u] 视图切换已触发,无需重复触发。\n", g_node_state.node_id);
pthread_mutex_unlock(&g_node_state.log_mutex);
return;
}
g_node_state.view_change_triggered = true;
g_node_state.proposed_new_view_id = g_node_state.current_view + 1;
g_node_state.current_phase = PHASE_VIEW_CHANGE;
printf("[Node %u] 触发视图切换!提议新视图ID: %u\n", g_node_state.node_id, g_node_state.proposed_new_view_id);
// 1. 构建 VIEW-CHANGE 消息
PBFTMessage vc_msg;
memset(&vc_msg, 0, sizeof(PBFTMessage));
vc_msg.common_header.type = MSG_TYPE_VIEW_CHANGE;
vc_msg.common_header.view_id = g_node_state.current_view; // 旧视图ID
vc_msg.common_header.sender_id = g_node_state.node_id;
vc_msg.payload.view_change.new_view_id = g_node_state.proposed_new_view_id;
vc_msg.payload.view_change.last_stable_checkpoint_seq_num = g_node_state.last_executed_seq_num;
// 填充 P 集合 (简化:收集所有已Prepared但未Executed的请求)
MessageLogEntry* current_log_entry = g_node_state.message_log_head;
uint32_t p_count = 0;
while (current_log_entry != NULL && p_count < (NUM_NODES * 2)) { // 限制P集合大小
if (current_log_entry->is_prepared && !current_log_entry->is_executed) {
vc_msg.payload.view_change.p_set[p_count].seq_num = current_log_entry->sequence_num;
memcpy(vc_msg.payload.view_change.p_set[p_count].digest, current_log_entry->request_digest, DIGEST_SIZE);
p_count++;
}
current_log_entry = current_log_entry->next;
}
vc_msg.payload.view_change.p_set_count = p_count;
// 2. 广播 VIEW-CHANGE 消息给所有节点
for (uint8_t i = 0; i < NUM_NODES; ++i) {
send_message_to_node(i, &vc_msg);
}
// 自身也增加VIEW-CHANGE计数
g_node_state.received_view_change[g_node_state.node_id] = true;
g_node_state.total_view_change_count++;
printf("[Node %u] 自身VIEW-CHANGE计数: %d\n", g_node_state.node_id, g_node_state.total_view_change_count);
pthread_mutex_unlock(&g_node_state.log_mutex);
}
/**
* @brief 处理VIEW-CHANGE消息
* @param vc_msg VIEW-CHANGE消息
*/
void handle_view_change_msg(const ViewChangeMsg* vc_msg) {
printf("[Node %u] 收到来自 Node %u 的VIEW-CHANGE消息 (提议新视图 %u)\n",
g_node_state.node_id, vc_msg->common_header.sender_id, vc_msg->payload.view_change.new_view_id);
// 1. 验证VIEW-CHANGE消息
// 检查发送者ID是否合法
if (vc_msg->common_header.sender_id >= NUM_NODES) {
fprintf(stderr, "[Node %u] 错误: VIEW-CHANGE发送者ID非法 %u。\n", g_node_state.node_id, vc_msg->common_header.sender_id);
return;
}
// 检查是否重复收到此节点的VIEW-CHANGE
pthread_mutex_lock(&g_node_state.log_mutex);
if (g_node_state.received_view_change[vc_msg->common_header.sender_id]) {
printf("[Node %u] 警告: 收到重复的VIEW-CHANGE消息来自 Node %u。\n", g_node_state.node_id, vc_msg->common_header.sender_id);
pthread_mutex_unlock(&g_node_state.log_mutex);
return;
}
// 检查提议的新视图ID是否合法 (必须大于当前视图)
if (vc_msg->payload.view_change.new_view_id <= g_node_state.current_view) {
fprintf(stderr, "[Node %u] 错误: VIEW-CHANGE提议新视图ID %u 不合法 (必须大于当前视图 %u)。\n",
g_node_state.node_id, vc_msg->payload.view_change.new_view_id, g_node_state.current_view);
pthread_mutex_unlock(&g_node_state.log_mutex);
return;
}
// 2. 更新VIEW-CHANGE计数
g_node_state.received_view_change[vc_msg->common_header.sender_id] = true;
g_node_state.total_view_change_count++;
printf("[Node %u] VIEW-CHANGE计数: %d\n", g_node_state.node_id, g_node_state.total_view_change_count);
// 3. 如果当前节点是新主节点,并且收集到足够的VIEW-CHANGE消息
uint8_t new_primary_id = vc_msg->payload.view_change.new_view_id % NUM_NODES;
if (g_node_state.node_id == new_primary_id && g_node_state.total_view_change_count >= (2 * FAULTY_NODES + 1)) {
printf("[Node %u] (新主节点) 收集到足够的VIEW-CHANGE消息 (%d/%d)!准备发送NEW-VIEW。\n",
g_node_state.node_id, g_node_state.total_view_change_count, (2 * FAULTY_NODES + 1));
// 4. 新主节点构建 NEW-VIEW 消息
PBFTMessage nv_msg;
memset(&nv_msg, 0, sizeof(PBFTMessage));
nv_msg.common_header.type = MSG_TYPE_NEW_VIEW;
nv_msg.common_header.view_id = vc_msg->payload.view_change.new_view_id; // 新视图ID
nv_msg.common_header.sender_id = g_node_state.node_id;
nv_msg.payload.new_view.new_view_id = vc_msg->payload.view_change.new_view_id;
// 填充 V 集合 (简化:只记录发送者ID)
nv_msg.payload.new_view.view_change_proof_count = 0;
for (int i = 0; i < NUM_NODES; ++i) {
if (g_node_state.received_view_change[i]) {
nv_msg.payload.new_view.view_change_senders[nv_msg.payload.new_view.view_change_proof_count++] = i;
}
}
// 填充 O 集合 (简化:从日志中找到所有Prepared但未Executed的请求,重新PRE-PREPARE)
// 实际PBFT中,新主节点会从所有VIEW-CHANGE消息的P集合中,找到最高序列号的Prepared请求,
// 并为所有未完成的请求重新PRE-PREPARE
MessageLogEntry* current_log_entry = g_node_state.message_log_head;
uint32_t o_count = 0;
while (current_log_entry != NULL && o_count < (NUM_NODES * 2)) {
if (current_log_entry->is_prepared && !current_log_entry->is_executed) {
// 为这个请求重新构建PRE-PREPARE消息
PrePrepareMsg re_pp_msg;
memset(&re_pp_msg, 0, sizeof(PrePrepareMsg));
re_pp_msg.common_header.type = MSG_TYPE_PRE_PREPARE;
re_pp_msg.common_header.view_id = nv_msg.payload.new_view.new_view_id; // 新视图ID
re_pp_msg.common_header.sequence_num = current_log_entry->sequence_num;
re_pp_msg.common_header.sender_id = g_node_state.node_id; // 新主节点
re_pp_msg.client_id = current_log_entry->original_client_request.payload.client_request.client_id;
re_pp_msg.timestamp = current_log_entry->original_client_request.payload.client_request.timestamp;
strncpy(re_pp_msg.operation, current_log_entry->original_client_request.payload.client_request.operation, MAX_MSG_CONTENT_SIZE - 1);
memcpy(re_pp_msg.request_digest, current_log_entry->request_digest, DIGEST_SIZE);
memcpy(&nv_msg.payload.new_view.pre_prepared_requests[o_count], &re_pp_msg, sizeof(PrePrepareMsg));
o_count++;
}
current_log_entry = current_log_entry->next;
}
nv_msg.payload.new_view.pre_prepared_requests_count = o_count;
// 5. 新主节点广播 NEW-VIEW 消息
for (uint8_t i = 0; i < NUM_NODES; ++i) {
send_message_to_node(i, &nv_msg);
}
// 新主节点自己也更新视图状态
handle_new_view_msg(&nv_msg); // 自己处理自己的NEW-VIEW消息
}
pthread_mutex_unlock(&g_node_state.log_mutex);
}
/**
* @brief 处理NEW-VIEW消息
* @param nv_msg NEW-VIEW消息
*/
void handle_new_view_msg(const NewViewMsg* nv_msg) {
printf("[Node %u] 收到来自新主节点 %u 的NEW-VIEW消息 (新视图 %u)\n",
g_node_state.node_id, nv_msg->common_header.sender_id, nv_msg->payload.new_view.new_view_id);
// 1. 验证NEW-VIEW消息
// 检查发送者是否是新主节点
uint8_t expected_primary_id = nv_msg->payload.new_view.new_view_id % NUM_NODES;
if (nv_msg->common_header.sender_id != expected_primary_id) {
fprintf(stderr, "[Node %u] 错误: NEW-VIEW发送者 %u 不是新视图 %u 的主节点 (期望 %u)。\n",
g_node_state.node_id, nv_msg->common_header.sender_id, nv_msg->payload.new_view.new_view_id, expected_primary_id);
return;
}
// 检查新视图ID是否合法 (必须大于当前视图)
if (nv_msg->payload.new_view.new_view_id <= g_node_state.current_view) {
fprintf(stderr, "[Node %u] 错误: NEW-VIEW新视图ID %u 不合法 (必须大于当前视图 %u)。\n",
g_node_state.node_id, nv_msg->payload.new_view.new_view_id, g_node_state.current_view);
return;
}
// 检查是否重复收到此节点的NEW-VIEW
pthread_mutex_lock(&g_node_state.log_mutex);
if (g_node_state.received_new_view[nv_msg->common_header.sender_id]) {
printf("[Node %u] 警告: 收到重复的NEW-VIEW消息来自 Node %u。\n", g_node_state.node_id, nv_msg->common_header.sender_id);
pthread_mutex_unlock(&g_node_state.log_mutex);
return;
}
// 2. 更新NEW-VIEW计数
g_node_state.received_new_view[nv_msg->common_header.sender_id] = true;
g_node_state.total_new_view_count++;
printf("[Node %u] NEW-VIEW计数: %d\n", g_node_state.node_id, g_node_state.total_new_view_count);
// 3. 更新本地视图状态
g_node_state.current_view = nv_msg->payload.new_view.new_view_id;
g_node_state.is_primary = (g_node_state.node_id == (g_node_state.current_view % NUM_NODES));
printf("[Node %u] 视图已更新为 %u。当前角色: %s\n",
g_node_state.node_id, g_node_state.current_view, g_node_state.is_primary ? "主节点" : "副本节点");
// 重置视图切换相关状态
reset_view_change_state();
// 4. 处理 O 集合中的 PRE-PREPARE 消息
// 新主节点会重新PRE-PREPARE所有未完成的请求
for (uint32_t i = 0; i < nv_msg->payload.new_view.pre_prepared_requests_count; ++i) {
PrePrepareMsg* re_pp_msg = &nv_msg->payload.new_view.pre_prepared_requests[i];
printf("[Node %u] 处理NEW-VIEW中的重新PRE-PREPARE消息 (序列号 %u)\n",
g_node_state.node_id, re_pp_msg->common_header.sequence_num);
// 模拟收到PRE-PREPARE消息,触发后续的Prepare/Commit流程
handle_pre_prepare_msg(re_pp_msg);
}
pthread_mutex_unlock(&g_node_state.log_mutex);
}
/**
* @brief 启动视图切换计时器
*/
void start_view_change_timer() {
pthread_mutex_lock(&g_node_state.log_mutex);
g_node_state.view_change_timer_start_time_ms = get_current_time_ms();
printf("[Node %u] 视图切换计时器启动/重置。\n", g_node_state.node_id);
pthread_mutex_unlock(&g_node_state.log_mutex);
}
/**
* @brief 重置视图切换相关状态
*/
void reset_view_change_state() {
g_node_state.view_change_triggered = false;
g_node_state.view_change_timer_start_time_ms = 0; // 停止计时器
g_node_state.proposed_new_view_id = 0;
g_node_state.total_view_change_count = 0;
g_node_state.total_new_view_count = 0;
for (int i = 0; i < NUM_NODES; ++i) {
g_node_state.view_change_count[i] = 0;
g_node_state.received_view_change[i] = false;
g_node_state.received_new_view[i] = false;
}
g_node_state.current_phase = PHASE_IDLE; // 恢复到空闲阶段
printf("[Node %u] 视图切换状态已重置。\n", g_node_state.node_id);
}
int main(int argc, char* argv[]) {
if (argc < 2 || argc > 4) {
fprintf(stderr, "用法: %s <node_id> [simulate_primary_failure] [failure_node_id]\n", argv[0]);
fprintf(stderr, "示例: %s 0\n", argv[0]);
fprintf(stderr, "示例: %s 1 simulate_primary_failure 0 (Node 1 启动时模拟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;
}
// 处理故障注入参数
if (argc >= 3 && strcmp(argv[2], "simulate_primary_failure") == 0) {
g_node_state.simulate_primary_failure = true;
if (argc == 4) {
g_node_state.failure_node_id = (uint8_t)atoi(argv[3]);
printf("[Node %u] 将模拟 Node %u 作为故障主节点。\n", node_id, g_node_state.failure_node_id);
} else {
fprintf(stderr, "错误: 模拟主节点故障时需要指定故障节点ID。\n");
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; // 保护回复计数的互斥锁
uint32_t request_sent_time_ms; // 请求发送时间
} 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);
// 客户端等待回复,如果超时,可以重新发送请求
uint32_t start_wait_time = get_current_time_ms();
while (g_client_state.reply_count < (FAULTY_NODES + 1) &&
(get_current_time_ms() - start_wait_time < VIEW_CHANGE_TIMEOUT_MS * 2)) { // 客户端等待时间可以更长
sleep(1);
}
if (g_client_state.reply_count < (FAULTY_NODES + 1)) {
printf("[Client %u] 警告: 未在规定时间内收到足够回复,可能主节点故障或网络延迟。请尝试重新发送请求。\n", g_client_state.client_id);
}
}
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); // 清空结果
g_client_state.request_sent_time_ms = get_current_time_ms(); // 记录请求发送时间
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;
// 客户端需要知道当前的主节点是谁。这里简化为总是连接到Node 0。
// 实际PBFT中,客户端会通过某种方式(例如,从DNS或配置中获取所有节点列表,然后尝试连接)
// 或者在收到VIEW-CHANGE消息后,更新其主节点信息。
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;
}
// 获取当前时间 (毫秒)
uint32_t get_current_time_ms() {
struct timespec ts;
// CLOCK_MONOTONIC 是一个单调递增的时钟,不受系统时间调整的影响,适合测量时间间隔
if (clock_gettime(CLOCK_MONOTONIC, &ts) == -1) {
perror("clock_gettime");
return 0;
}
return (uint32_t)(ts.tv_sec * 1000 + ts.tv_nsec / 1000000);
}
代码分析与逻辑透析:
本次更新是PBFT仿真器的核心升级,它引入了视图切换机制,让系统具备了拜占庭容错能力。同时,我们还加入了简单的故障注入功能,让你能亲手模拟主节点故障,并观察系统如何进行“自我修复”。
-
common.h
的更新:-
VIEW_CHANGE_TIMEOUT_MS
: 定义了视图切换的超时时间,这是副本节点判断主节点是否异常的依据。 -
ViewChangeMsg
结构体: 定义了VIEW-CHANGE
消息的结构。-
new_view_id
:提议的新视图ID。 -
last_stable_checkpoint_seq_num
:发送者已执行的最新序列号,用于新主节点确定低水位线。 -
p_set_count
和prepared_requests
:简化表示P
集合,存储了副本节点已 Prepared 但未 Executed 的请求的序列号和摘要。实际PBFT中,这里会包含更完整的证明(PRE-PREPARE
和PREPARE
消息)。
-
-
NewViewMsg
结构体: 定义了NEW-VIEW
消息的结构。-
new_view_id
:新视图ID。 -
view_change_proof_count
和view_change_senders
:简化表示V
集合,记录了新主节点收集到的VIEW-CHANGE
消息的数量和发送者ID。实际PBFT中,这里会包含完整的VIEW-CHANGE
消息。 -
pre_prepared_requests_count
和pre_prepared_requests
:简化表示O
集合,包含了新主节点为未完成请求重新生成的PRE-PREPARE
消息。
-
-
PBFTPhase
枚举: 增加了PHASE_VIEW_CHANGE
和PHASE_NEW_VIEW
,表示节点在视图切换过程中的状态。 -
NodeState
结构体扩展:-
view_change_timer_start_time_ms
:记录视图切换计时器启动的时间。 -
view_change_triggered
:布尔标志,表示是否已触发视图切换。 -
proposed_new_view_id
:当前节点提议的新视图ID。 -
received_view_change
和total_view_change_count
:用于收集VIEW-CHANGE
消息。 -
received_new_view
和total_new_view_count
:用于收集NEW-VIEW
消息。 -
simulate_primary_failure
和failure_node_id
: 这是我们新增的故障注入标志和要模拟故障的节点ID。
-
-
-
utils.c
的更新:-
print_message()
: 增加了对VIEW-CHANGE
和NEW-VIEW
消息的详细打印,方便调试。 -
get_current_time_ms()
: 新增辅助函数,用于获取高精度时间戳(毫秒),用于计时器。这里使用了clock_gettime(CLOCK_MONOTONIC)
,它提供了一个单调递增的时钟,非常适合测量时间间隔,因为它不受系统时间调整的影响。
-
-
node.c
的更新:-
start_node()
:-
初始化视图切换相关的状态变量。
-
故障注入参数解析:
main
函数现在可以接受额外的命令行参数来模拟主节点故障。例如,./node 1 simulate_primary_failure 0
表示Node 1在启动时,会模拟Node 0作为故障主节点(即Node 0不会发送PRE-PREPARE
消息)。 -
主循环中,副本节点会周期性地检查视图切换计时器是否超时,如果超时则调用
trigger_view_change()
。
-
-
send_message_to_node()
: 故障注入点! 在这里加入了判断逻辑:如果当前节点是故障节点(由命令行参数指定),并且它要发送的是PRE-PREPARE
消息,那么就直接返回,不发送该消息,从而模拟主节点不响应。 -
recv_message_thread()
:-
客户端连接处理优化: 区分了PBFT节点之间的连接和客户端连接。对于客户端连接,不再将其套接字存储到
conn_sockets
数组中,因为它是一次性的。 -
switch
语句中增加了对MSG_TYPE_VIEW_CHANGE
和MSG_TYPE_NEW_VIEW
的处理,分别调用handle_view_change_msg()
和handle_new_view_msg()
。
-
-
handle_client_request_msg()
和handle_pre_prepare_msg()
:-
在主节点收到客户端请求后,以及副本节点收到
PRE-PREPARE
消息后,都会调用start_view_change_timer()
来重置计时器。这表示主节点正在正常工作,无需视图切换。 -
在
handle_pre_prepare_msg()
中,如果副本节点收到冲突的PRE-PREPARE
消息(同一个序列号但摘要不同),会立即调用trigger_view_change()
,模拟主节点作恶。
-
-
trigger_view_change()
:-
设置
view_change_triggered
标志,防止重复触发。 -
递增
current_view
,计算proposed_new_view_id
。 -
构建
VIEW-CHANGE
消息,填充new_view_id
、last_stable_checkpoint_seq_num
。 -
填充
P
集合: 遍历消息日志,收集所有is_prepared
但!is_executed
的日志条目,将其序列号和摘要添加到VIEW-CHANGE
消息的p_set
中。 -
广播
VIEW-CHANGE
消息给所有节点。
-
-
handle_view_change_msg()
:-
验证收到的
VIEW-CHANGE
消息的合法性(发送者ID、新视图ID)。 -
更新
total_view_change_count
和received_view_change
数组。 -
新主节点逻辑: 如果当前节点是新视图的主节点(
node_id == (new_view_id % NUM_NODES)
),并且收集到了2f+1
个VIEW-CHANGE
消息,则开始构建并广播NEW-VIEW
消息。-
填充
V
集合: 记录所有发送VIEW-CHANGE
消息的节点ID。 -
填充
O
集合: 新主节点会遍历自己的消息日志,找到所有is_prepared
但!is_executed
的请求,为它们重新构建PRE-PREPARE
消息(使用新的视图ID),并添加到NEW-VIEW
消息的pre_prepared_requests
中。 -
广播
NEW-VIEW
消息。 -
新主节点自己也会调用
handle_new_view_msg()
来处理自己的NEW-VIEW
消息。
-
-
-
handle_new_view_msg()
:-
验证收到的
NEW-VIEW
消息的合法性(发送者是否是新主节点、新视图ID)。 -
更新
total_new_view_count
和received_new_view
数组。 -
更新本地视图状态: 将
g_node_state.current_view
更新为new_view_id
,并重新判断is_primary
角色。 -
重置视图切换状态: 调用
reset_view_change_state()
,清除所有视图切换相关的临时状态。 -
处理
O
集合: 遍历NEW-VIEW
消息中的pre_prepared_requests
(O
集合),为每个请求调用handle_pre_prepare_msg()
。这将触发这些未完成请求在新视图中的三阶段提交过程。
-
-
start_view_change_timer()
和reset_view_change_state()
: 辅助函数,用于管理视图切换计时器和重置相关状态。
-
-
client.c
的更新:-
main
函数中,客户端在发送请求后,会等待一段时间(VIEW_CHANGE_TIMEOUT_MS * 2
),如果未收到足够回复,会打印警告,提示用户可能需要重新发送请求。这模拟了客户端在实际分布式系统中检测到故障时的行为。 -
get_current_time_ms()
函数也添加到客户端,用于计时。
-
运行这个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
-
-
观察输出,确认节点相互连接。
-
-
启动客户端:
-
打开第五个WSL2终端窗口。
-
终端5:
./client 100
(启动Client 100) -
在客户端输入
test op 1
。你会看到所有节点正常走完三阶段提交,客户端收到回复。
-
-
模拟主节点故障(视图切换演示):
-
关闭Node 0 的终端! 这模拟了主节点宕机。
-
重新启动Node 1,并注入故障模拟参数:
-
终端2:
./node 1 simulate_primary_failure 0
(让Node 1知道Node 0是故障节点) -
注意: 这里的
simulate_primary_failure 0
是为了在Node 0 恢复后,如果它再次成为主节点,可以模拟它继续故障。但对于当前演示,我们直接关闭Node 0,所以这个参数主要影响Node 0的行为。
-
-
在客户端再次发送请求: 输入
test op 2
。-
观察 Node 1, 2, 3 的终端!
-
它们会因为长时间没有收到主节点(Node 0)的
PRE-PREPARE
消息而计时器超时。 -
Node 1, 2, 3 会广播
VIEW-CHANGE
消息。 -
它们会收集到
2f+1
个VIEW-CHANGE
消息。 -
根据
(old_view + 1) % N
的规则,Node 1 将成为新的主节点(因为(0 + 1) % 4 = 1
)。 -
Node 1 会广播
NEW-VIEW
消息。 -
所有节点收到
NEW-VIEW
后,会更新视图ID为1,Node 1 成为主节点,Node 0, 2, 3 成为副本节点。 -
客户端会再次发送请求,这次请求应该会发送给新的主节点 Node 1,然后继续三阶段提交,最终达成共识。
-
-
通过这个实验,你将亲眼看到PBFT在主节点故障时,如何通过视图切换机制,实现系统的“自我修复”和共识的持续进行!这才是PBFT真正的魅力所在!
小结与展望
老铁们,恭喜你!你已经成功闯过了PBFT算法C语言仿真的第三关:视图切换机制的实现!
在这一部分中,我们:
-
深入理解了视图切换的必要性,以及它如何保证PBFT的活性。
-
详细剖析了
VIEW-CHANGE
和NEW-VIEW
两种核心消息的结构和作用。 -
用C语言实现了视图切换的核心逻辑:
-
副本节点通过计时器检测主节点故障。
-
触发视图切换时,广播
VIEW-CHANGE
消息,并携带已 Prepared 的请求信息。 -
新主节点收集
VIEW-CHANGE
消息,构建并广播NEW-VIEW
消息,其中包含重新PRE-PREPARE
的请求。 -
所有节点处理
NEW-VIEW
消息,更新视图,并继续处理请求。
-
-
引入了简单的“故障注入”机制,让你能够亲手模拟主节点宕机,并观察系统如何通过视图切换进行“自我修复”。
现在,你的PBFT仿真器已经具备了基本的拜占庭容错能力!你不仅能让它在正常情况下达成共识,还能让它在主节点故障时“力挽狂澜”,这绝对是值得你骄傲的硬核技能!
当然,我们当前的仿真器为了简化,还有很多可以改进的地方,例如:
-
更完善的故障注入: 模拟网络延迟、丢包、任意拜占庭行为(发送错误消息、不响应特定消息)。
-
数字签名与验签: 引入真实的加密库(如OpenSSL)来增强消息的安全性。
-
检查点机制: 实现更完善的日志清理和状态恢复。
-
更高效的消息日志: 将链表替换为哈希表等数据结构,提高查找效率。
-
并发处理优化: 优化消息处理流程,提高吞吐量。
这些将在后续的篇章中逐步展开。接下来,我们将进入更广阔的共识算法世界,探索PBFT的优化版本(如Credit-PBFT、Tree-PBFT),以及其他主流共识算法(如Raft、DPOS)的原理和仿真思路!
敬请期待我的下一次更新!如果你在学习过程中有任何疑问,或者对代码有任何改进的想法,随时在评论区告诉我,咱们一起交流,一起成为区块链共识算法的“大神”!
------------------------------------------------------------------------------------------------更新于2025.6.21 下午5:26
【硬核揭秘】一文教你C语言彻底学会PBFT算法仿真:从Win11到共识算法全景(终章)
前言:从PBFT仿真到分布式共识的“全景图”
老铁们,一路走来,我们已经用C语言亲手搭建了一个具备基本拜占庭容错能力的PBFT仿真器。从最初的TCP通信框架,到三阶段提交协议,再到复杂的视图切换机制,你已经掌握了PBFT算法的核心精髓。这无疑是你分布式系统学习旅程中的一个重要里程碑!
现在,是时候将我们的视角从具体的PBFT实现,扩展到更广阔的分布式共识领域了。本篇作为“一文教你C语言彻底学会PBFT算法仿真”系列的终章,我们将不再深入新的PBFT代码细节,而是聚焦于:
-
回顾当前PBFT仿真器的局限性,并探讨实际PBFT系统所面临的挑战。
-
介绍PBFT的常见优化方向,让你了解如何构建更高效、更健壮的PBFT系统。
-
展望其他主流分布式共识算法,为你打开通往分布式系统更深层次学习的大门。
通过本章,你将对PBFT的实际应用、性能瓶颈以及其他共识算法的适用场景有更全面的认识,从而建立起一个更完善的分布式共识知识体系。
准备好了吗?咱们这就开启分布式共识算法的“全景图”之旅!
第八章:PBFT的实际挑战与优化方向——从理论到实践的桥梁
我们之前实现的PBFT仿真器,虽然涵盖了核心逻辑,但在实际生产环境中,PBFT面临着诸多挑战。理解这些挑战以及对应的优化方案,是构建高性能、高可用分布式系统的关键。
8.1 当前PBFT仿真器的局限性回顾
在深入优化之前,我们先快速回顾一下当前仿真器为了简化而牺牲的一些特性:
-
简化的消息摘要与无数字签名:
-
局限: 我们的摘要函数只是一个简单的哈希,不具备加密安全性。更重要的是,所有消息都没有进行数字签名和验签。在实际PBFT中,数字签名是防止消息伪造、篡改和验证发送者身份的基石。
-
影响: 无法抵御恶意节点伪造消息、冒充其他节点发送消息等拜占庭行为。
-
-
简化的消息日志管理:
-
局限: 消息日志使用简单的链表,查找效率不高。没有实现持久化存储,节点重启后状态会丢失。缺乏完善的高水位线和检查点机制来高效地清理旧日志。
-
影响: 随着处理请求的增多,内存占用会持续增长;节点故障恢复复杂;日志查找效率低。
-
-
单线程消息处理(部分):
-
局限: 虽然每个连接都有独立的接收线程,但PBFT核心逻辑(
handle_xxx_msg
)在互斥锁保护下顺序执行。 -
影响: 限制了并发处理能力,成为系统吞吐量的瓶颈。
-
-
无网络模拟:
-
局限: 仿真器假设网络是理想的,无延迟、无丢包、无乱序。
-
影响: 无法真实反映分布式系统在复杂网络环境下的性能和鲁棒性。
-
-
简化的客户端交互:
-
局限: 客户端仅简单地等待
f+1
个回复,未处理超时重发、主节点切换后的请求重定向等复杂逻辑。 -
影响: 实际客户端需要更智能的逻辑来应对网络和节点故障。
-
-
无状态机复制细节:
-
局限: 节点执行操作时只是打印一行日志,没有真正维护和更新一个共享的状态机(例如一个分布式账本、数据库状态)。
-
影响: 无法模拟真实应用场景下的数据一致性。
-
8.2 PBFT的常见优化方向
为了解决上述挑战,实际的PBFT系统通常会引入各种优化和扩展。
8.2.1 安全性增强:数字签名与加密哈希
-
核心: 使用强大的加密哈希算法(如SHA256、SHA3)计算消息摘要,并使用非对称加密(如RSA、ECDSA)对消息进行数字签名。
-
实现:
-
每个节点生成一对公钥和私钥。私钥用于签名,公钥分发给所有其他节点。
-
发送消息时,节点使用私钥对消息内容(或其摘要)进行签名,并将签名附加到消息中。
-
接收消息时,节点使用发送者的公钥验证签名。
-
-
工具: 在C语言中,通常会集成OpenSSL等加密库来实现这些功能。
8.2.2 性能优化:吞吐量与延迟
-
消息聚合 / 批量处理 (Batching):
-
原理: 主节点不立即处理每个客户端请求,而是将多个请求聚合到一个批次中,然后为整个批次生成一个
PRE-PREPARE
消息。副本节点也对批次进行处理。 -
优点: 显著减少网络中传输的消息数量和签名/验签的开销,从而提高系统吞吐量。
-
挑战: 引入了额外的延迟(需要等待批次积累),并且批次大小的选择影响性能。
-
-
流水线处理 (Pipelining):
-
原理: 允许在处理前一个请求的某个阶段时,就开始处理下一个请求的早期阶段。例如,在请求
n
处于 Prepare 阶段时,请求n+1
就可以开始 Pre-prepare 阶段。 -
优点: 提高并发度,降低平均延迟。
-
挑战: 增加了实现的复杂性,需要更精细的序列号管理和消息日志同步。
-
-
多线程 / 事件驱动模型:
-
原理: 采用线程池或异步事件驱动模型来并发处理收到的消息,而不是在单个互斥锁下顺序处理。
-
优点: 充分利用多核CPU,提高消息处理能力和系统吞吐量。
-
挑战: 引入了复杂的并发控制和同步问题。
-
-
优化网络通信:
-
原理: 使用更高效的网络传输协议(如UDP而不是TCP,但需要自己实现可靠性),或者优化消息序列化/反序列化。
-
优点: 降低网络延迟和带宽消耗。
-
挑战: 增加了实现的复杂性。
-
8.2.3 可扩展性与健壮性:日志管理与状态恢复
-
检查点机制 (Checkpointing):
-
原理: 周期性地,所有节点会就一个特定的序列号达成共识,并记录此时状态机的快照。这个快照被称为“稳定检查点”。
-
作用:
-
垃圾回收: 所有序列号小于稳定检查点对应的请求及其日志都可以安全地从内存和磁盘中删除,防止日志无限增长。
-
快速恢复: 新加入的节点或从故障中恢复的节点,可以直接从最新的稳定检查点加载状态,而无需处理所有历史请求。
-
-
实现: 引入
CHECKPOINT
消息,节点收集2f+1
个CHECKPOINT
消息后,确认检查点稳定。
-
-
视图同步 (View Synchronization):
-
原理: 确保所有正常节点都处于相同的视图中。当节点发现自己处于旧视图时,会主动请求进入新视图。
-
作用: 提高系统在视图切换后的收敛速度。
-
-
动态成员管理:
-
原理: 允许节点动态加入或退出系统,而无需停机。
-
挑战: 增加了协议的复杂性,需要一套额外的协议来管理成员变更。
-
8.2.4 客户端优化
-
轻量级客户端:
-
原理: 客户端无需连接所有节点,只需连接少量节点。当收到
f+1
个相同回复时,即可确认。 -
优点: 减少客户端的连接负担和资源消耗。
-
挑战: 客户端需要知道当前的主节点,或者能够通过某种机制发现新的主节点。
-
-
客户端请求重发与重定向:
-
原理: 客户端在发送请求后会启动计时器。如果超时未收到足够回复,客户端会向下一个可能的节点(例如,通过
(current_primary_id + 1) % N
轮询)重新发送请求,或者触发视图切换。 -
作用: 提高客户端在主节点故障时的请求成功率。
-
8.3 PBFT的变种与优化算法
PBFT本身也有许多变种和改进,以适应不同的应用场景或解决特定问题:
-
Credit-PBFT: 引入信用机制,根据节点的历史表现调整其权重,从而提高性能或安全性。
-
Aardvark: 优化了视图切换过程,使其更加轻量和高效。
-
Zyzzyva: 引入了乐观路径,在无故障情况下可以减少消息数量,提高性能。
-
BFT-SMaRt: 一个流行的PBFT实现框架,提供了模块化和高性能的PBFT服务。
这些变种通常在消息数量、延迟、吞吐量、安全性等方面进行了权衡和优化。
第九章:分布式共识算法的“百花齐放”——PBFT之外的世界
除了PBFT,分布式共识领域还有许多其他重要的算法,它们在设计理念、适用场景和容错模型上各有侧重。了解它们,将帮助你更全面地理解分布式系统的共识问题。
9.1 CAP定理与共识算法的权衡
在讨论其他共识算法之前,我们必须先了解分布式系统中的一个核心理论——CAP定理。
CAP定理指出,在一个分布式系统中,以下三个特性最多只能同时满足两个:
-
C (Consistency) - 一致性: 所有节点在同一时间看到的数据是相同的。
-
A (Availability) - 可用性: 无论任何节点故障,系统都能响应用户的请求。
-
P (Partition Tolerance) - 分区容错性: 即使网络出现分区(节点之间无法通信),系统也能继续运行。
在实际的分布式系统中,分区容错性 (P) 是一个不可避免的现实。因此,共识算法通常需要在一致性 (C) 和可用性 (A) 之间进行权衡。
-
CP系统 (Consistency + Partition Tolerance): 优先保证一致性,当网络分区发生时,为了保证数据一致性,系统可能会拒绝服务(牺牲可用性)。PBFT就属于CP系统。
-
AP系统 (Availability + Partition Tolerance): 优先保证可用性,当网络分区发生时,系统仍然可以提供服务,但可能导致数据不一致(牺牲一致性)。例如,一些最终一致性(Eventual Consistency)的NoSQL数据库。
9.2 崩溃容错 (Crash Fault Tolerance, CFT) 共识算法
这类算法主要解决节点崩溃(Crash) 问题,即节点可能突然停止工作,但不会发送恶意消息。它们通常比拜占庭容错算法更简单、效率更高。
-
Raft:
-
核心思想: 通过“领导者选举”(Leader Election)和“日志复制”(Log Replication)来达成共识。系统在任何时候只有一个领导者,所有请求都通过领导者处理,领导者负责将请求复制到其他副本节点。
-
优点: 易于理解和实现,广泛应用于各种分布式系统(如Etcd、Consul)。
-
缺点: 无法容忍拜占庭故障。
-
适用场景: 需要强一致性,且节点之间相互信任(或可以防止恶意行为)的场景,如分布式配置服务、分布式锁服务。
-
-
Paxos:
-
核心思想: 一种非常经典的分布式共识算法,通过多轮投票来达成对某个值的共识。它比Raft更复杂,但理论上可以解决更一般化的共识问题。
-
优点: 理论上最优的崩溃容错算法。
-
缺点: 难以理解和实现,工程实践中通常使用其简化版本或Raft。
-
适用场景: 与Raft类似,但在某些特定场景下可能提供更高的灵活性。
-
9.3 拜占庭容错 (Byzantine Fault Tolerance, BFT) 共识算法
这类算法能够容忍节点发送任意恶意消息(即拜占庭故障)。PBFT就是其中最著名的代表。
-
BFT类算法的特点:
-
更高的安全性: 能够抵御恶意节点的攻击。
-
更复杂: 通常需要更多的消息交互和更复杂的协议逻辑。
-
性能开销: 由于需要多轮消息确认和签名验证,通常比CFT算法的吞吐量低、延迟高。
-
节点数量限制: 通常要求 N≥3f+1,这意味着随着节点数量的增加,通信开销呈平方级增长 (O(N2)),限制了其可扩展性。
-
-
其他BFT变种:
-
除了我们之前提到的PBFT优化版本,还有一些新的BFT算法,如HotStuff(Facebook Libra/Diem项目曾采用),它通过链式投票优化了性能。
-
9.4 权益证明 (Proof of Stake, PoS) 类共识算法
在区块链领域,除了工作量证明(PoW)之外,权益证明(PoS)及其变种也广泛用于达成共识。它们通常不直接解决拜占庭容错问题,而是通过经济激励和惩罚机制来保证系统的安全和活性。
-
DPoS (Delegated Proof of Stake) - 委托权益证明:
-
核心思想: 持币者投票选举出少数“见证人”(或称为“超级节点”),由这些见证人轮流负责打包区块和达成共识。
-
优点: 交易确认速度快,吞吐量高,资源消耗低。
-
缺点: 中心化程度相对较高,安全性依赖于见证人的诚实性,容易形成寡头。
-
适用场景: 对交易速度和效率要求高的区块链项目,如EOS、TRON。
-
-
Tendermint (BFT共识引擎):
-
核心思想: Tendermint 是一个将区块链应用层和共识层解耦的BFT共识引擎。它使用了一种类似于PBFT的共识算法,但针对区块链场景进行了优化。
-
优点: 强一致性,快速最终性(一旦区块被提交,就不会被回滚),高吞吐量。
-
缺点: 仍然受限于BFT算法的扩展性限制。
-
适用场景: Cosmos、Binance Smart Chain等区块链项目。
-
9.5 共识算法的选择
选择合适的共识算法取决于应用场景的具体需求:
-
对一致性要求极高,且节点数量有限,可以容忍一定性能开销: PBFT及其变种(如联盟链、私有链)。
-
对一致性要求高,但节点之间相互信任,追求易用性和高性能: Raft(如分布式数据库、配置中心)。
-
对交易速度和吞吐量要求极高,可以接受一定程度的中心化: DPoS(如公链)。
-
需要抵御无许可环境下的任意恶意行为: PoW(如比特币),但性能较低。
小结与展望
老铁们,恭喜你!你已经完成了“一文教你C语言彻底学会PBFT算法仿真”的全部学习旅程!
在这一终章中,我们:
-
回顾了当前PBFT仿真器的局限性,并将其与实际PBFT系统面临的挑战联系起来。
-
深入探讨了PBFT的各种优化方向,包括安全性增强(数字签名)、性能优化(消息聚合、流水线)、可扩展性(检查点)和客户端优化。
-
介绍了CAP定理,并以此为基础,展望了PBFT之外的分布式共识算法世界,包括崩溃容错算法(Raft、Paxos)和权益证明类算法(DPoS、Tendermint)。
现在,你不仅能够用C语言实现PBFT的核心逻辑,还能对分布式共识算法的整体格局、它们之间的权衡和适用场景有了一个清晰的认识。这套知识体系将是你未来在分布式系统、区块链等领域深入学习和实践的宝贵财富!
下一步,你可以尝试:
-
在现有PBFT仿真器上实现部分优化:
-
尝试引入OpenSSL库实现消息的数字签名和验签。
-
实现更完善的检查点机制和垃圾回收。
-
尝试实现简单的消息批量处理。
-
-
学习并实现其他共识算法:
-
Raft算法是一个很好的起点,它的原理相对PBFT更易理解,且在工业界应用广泛。
-
你可以尝试用C语言或你熟悉的任何其他语言(如Go、Python)实现一个Raft仿真器。
-
-
深入研究分布式系统理论:
-
探索更多关于分布式事务、分布式存储、分布式调度等领域的知识。
-
分布式系统是一个充满挑战和机遇的领域。希望这个系列能为你打开一扇大门,激发你对分布式技术更深层次的探索欲望!
如果你在学习过程中有任何疑问,或者对代码有任何改进的想法,随时在评论区告诉我,咱们一起交流,一起成为分布式系统和区块链的“大神”!
感谢你的陪伴,祝你在技术探索的道路上越走越远!