背景
因为Libra学习rust,想用rust写些东西,而刚好看区块链共识机制的PBFT,就用原生Rust实现一个PBFT的分布式系统,业余中间断断续续写,代码比较零散。 中间也有些逻辑值得记录, 主要在Rust的多线程,网络通信的处理上。
总体架构
PBFT 节点实现
Node: 节点服务,提供PBFT的能力,
核心功能:处理client request, pre-prepare, prepare,commit,消息,最后reply给client,
其他:视图转换, check point创建,节点管理,消息签名与验签。
主业务流程
这里不一一讲解每个阶段,其中一阶段代码,有兴趣可以clone下来看。
```cpp
pub fn receiveCommit(& mut self, msg:Bft_Commit_Message, mut executor:&mut Command_Executor) {
// check sign for node
let mut sign_msg = msg.clone();
let msg_digest = msg.get_msg_digest();
sign_msg.set_msg_digest(String::new());
let msg_node_id = sign_msg.get_node_id();
let pub_key_result = self.get_node_pub_key(&msg_node_id);
if pub_key_result.is_none() {
error!("can not found node id for {}", sign_msg.get_node_id());
return;
}
let signMsgStr = json::encode(&sign_msg).unwrap();
if !Bft_Signtor::check_sign(signMsgStr.as_str(), pub_key_result.unwrap().as_str(), msg_digest.as_str()) {
error!("commit msg sign not pass for {}", sign_msg.get_node_id());
return;
}
info!("check pass, process commit msg");
// check pass add to prepare cache;
if self.commit_cache.contains_key(&msg.get_sequence_num()) {
let list = self.commit_cache.get_mut(&msg.get_sequence_num()).unwrap();
list.push(msg.clone());
} else {
let mut commit_msg_list = Vec::new();
commit_msg_list.push(msg.clone());
self.commit_cache.insert(msg.get_sequence_num(), commit_msg_list);
}
self.doReplay(msg.get_sequence_num(), executor);
}
通信模块
对P2P的网络不熟悉,根据自己理解,实现了一个非阻塞式的网络通信模块,用于Node跟其他Node或者Client之间的通信。
非阻塞式通信
这里实现的是一个非阻塞的通信服务, 可以用于跟其他节点或者Client来接发消息,基于Rust原生的TCP(TcpListener, TcpStream)实现。每个网络连接TCPStream都分配一个线程作为worker,处理连接进来的作为reader worker,只用于读,而外发到其他则创建一个TCPStream用于写,并起一个write worker。
通信模块只暴露startListener, sendmssage方法, 都支持非阻塞。 非阻塞通过Rust的channel来实现。
Read worker 线程一直尝试读取TCPStream消息,当读取消息后,完成消息协议格式处理,把消息扔到channel。
Write worker 线程一直监听channel是否有消息进来,有消息则把消息转成String后,通过TCPStream发送出去。
message parser: 作为一个独立线程,因为rust的channel支持一对多,等待所有worker读的消息,根据消息类型,调对应外部PBFT Node的方法处理消息。
同步消息通信
当然通信模块也支持同步发送消息,阻塞等待对方返回, 但同步同样基于上诉的异步过程,但在消息协议中会注明是同步,并分配一个唯一id, 为同步消息专门开通一个channel,当writer发送完消息,就通过channel的receiver阻塞,等待reply,而message parser收到消息,发现是reply并且是同步的,直接扔到同步的channel中。
通信消息协议
因为基于TCP实现的网络通信,这里定义了一个简单协议,通信模块的worker负责协议的解析和文本序列化。
协议格式:
command version sync id length
body
body 为文本, 格式内容由上层业务决定,这里的PBFT格式为消息的Json文本。
Command 预留reply,作为同步消息的同步响应消息, 其他上层业务定义的业务命令,这里是PBFT的receive, pre-prepare, prepare, commit
业务执行模块
这里的业务执行模块,只是做了简单的key-value缓存,支持 put key=value, get key, delete key 命令,业务器解析命令, 通过Rust原生Map缓存键值对。支持持久化,在checkpoint阶段,把未持久化的值放入文本。 另外实时记录执行的命令,理论支持上checkpoint恢复后再通过实时命令恢复缓存。
就是基于PBFT实现了一个分布式的缓存系统, 而command executor为接口,可以切换其他的实现把这分布系统实现其他能力。
Message parse
消息解析模块,根据command来解析PBFT消息格式,做业务路由,创建一个新线程处理消息。 这里涉及Rust的多线程编程。
```cpp
//code
let node_mutex: Arc<Mutex<Btf_Node>> = Arc::new(Mutex::new(node));
let mutex = Arc::clone(&node_mutex);
thread::Builder::new().name(i.to_string()).spawn(move|| {
let mut node = mutex.lock().unwrap();
let node_msg = node_msg_result.unwrap();
let result = node.doPrepare(node_msg, &mut executor);
if result.is_some() {
let (view_num, sequece_num) = result.unwrap();
}
……
});
Rust多线程编程:
Rust都是通过thread创建新的线程,而rust没有自己的runtime线程模型,使用的系统的的线程, 线程间有几种通信或并发控制方式。
1、 mutex用于共享一个变量,使用变量前先获得锁,因为rust有ownership的原因,每个变量只能有一个owner,而多个线程持有一个变量,需要通过Arc实现,通过Arc让多线程同时持有mutex。Metux再由 lock then use的机制,并发操作共享变量。
2、 线程间通信,通过channel来实现,有点类似q + wait-notify的作用,做系统间的协同操作,上文的通信模块就多处使用channel。 一个channel能有多个sender但只能有一个receiver,receiver可以阻塞等待消息,也可以非阻塞。
这里创建读网络线程,读线程读到网络数据后,通过sender通知业务线程。
```rust
let (msg_sender, msg_receiver) = channel();
thread::Builder::new().name("bft_node_listener".to_string()).spawn(move|| {
let mut thread_index = 0;
for stream in listener.incoming() {
info!("receive one connection");
let mut stream = stream.unwrap();
let mut msg_sender_reader = Sender::clone(&msg_sender_sub);
Default_TCP_Communication::create_new_reader(msg_sender_reader, stream);
}
});
// 阻塞式获取读线程通过channel投递的消息对象, 转交给message parser.
let msg_result = msg_receiver.recv();
if msg_result.is_ok() {
let msg_box:Box<BftCommunicationMsg> = msg_result.unwrap();
……
3、 Send ,Sync接口,对多线程编程,这两个是特殊接口,两个接口都不需要手工实现,只要struct的成员都实现了, 对象就默认实现,而基础类型都实现了这两个接口。
Send: 实现了该接口的,可以在线程之间传送变量的ownership。
Sync: 实现了该接口, 可以在线程之间出送变量的引用。
总结
至此说明链原生Rust写PBFT过程中重点处理的逻辑,按PBFT论文实现消息的多阶段状态机处理,checkpoint处理,这里没具体提到PBFT逻辑,具体可以参考之前多文章“区块链四” 另外重点提到原生多通信模块,具备简单异步同步通信能力; 另外Rust多线程特性也在此次实现中多次用到。
往期:
区块链四:共识机制——PBFT算法深入讲解
区块链三:深入解析比特币交易原理
区块链二:比特币的区块数据结构
区块链一 :区块链应用介绍和展望