关键字:区块链 可靠信道 BFT-SMaRt Socket SSL/TLS 网络通信
信道的可靠是BFT的前提。(参见两军问题)
本文通过跟踪BFT-SMaRt通信层源码,研究节点间可靠信道的实现原理。本文涉及区块链方面的内容较少,重点研究使用Java语言建立可靠网络通道的技术,请选择性阅读。
通信层系统,是分布式网络中获得可靠且认证的点对点通道的保证。BFT-SMaRt的安全通信是基于SSL/TLS标准。
- 节点之间建立互为信任的Socket IO连接,实现点对点的消息处理。
- 节点与客户端之间建立健壮性、可用性更高的Netty NIO连接,实现大规模的消息处理。
本文主要介绍第一种情况:在BFT-SMaRt中,作为服务端的节点之间的连接构建方法。
一、引子
接上一篇 BFT-SMaRt的理论与实践 ,启动分布式计数器服务示例程序时,需输入命令:
runscripts/smartrun.sh bftsmart.demo.counter.CounterServer 0
命令调用的是CounterServer类的内容,先查看CounterServer的类结构。
接着再看一下CounterServer的类图。
通过CounterServer的类图可以清晰地展示它的关系结构。其父类DefaultSingleRecoverable实现了Recoverable和SingleExecutable接口,而SingleExecutable继承了Executable。追本溯源,根部是以下两个接口:
- Recoverable:恢复程序,实现此接口的类应该实现状态转移协议。通常,类应该同时实现这个接口和一个Executable。
- Executable:执行程序,实现此接口,可接收无序的客户端请求。如果要支持有序的请求,可以选择其子类接口FIFOExecutable、BatchExecutable或SingleExecutable。
回到CounterServer的源码,它是所有官方示例中架构最简单的,这会提高我们的学习效率。CounterServer有两个类属性:
- counter,计数字段,保存计数器的状态值。
- iterations,操作次数,日志记录以及数据恢复时使用。
下面开始项目调试,我们为命令手动配置添加请求参数“0”作为节点id,然后启动命令进入CounterServer的main入口函数:
public static void main(String[] args){
if(args.length < 1) {
System.out.println("Use: java CounterServer <processId>");
System.exit(-1);
}
new CounterServer(Integer.parseInt(args[0]));
}
进来先校验参数个数,然后调用CounterServer构造函数。函数内创建了一个ServiceReplica对象。
public CounterServer(int id) {
new ServiceReplica(id, this, this);
}
传入的第一个参数是命令带入的唯一参数,即节点id。后面两个参数的值都是this,是将CounterServer分别作为执行程序和恢复程序。
二、名词统一
在本文的研究中,会涉及到一些由本系统提出,并且非常重要的名词概念。为避免后续发生同一件事的称呼混乱,造成困扰,在这里统一声明。
1. 节点id
replicaId、processId、remoteId、TTPid指的都是节点id,但包含以下几种情况:
- replicaId:节点作为一个副本的时候。
- processId:一个处理单元作为节点的时候。
- remoteId:外部的节点id。
- TTPid:设置的TTP节点的id。
2. 节点
Replica是分布式系统中的副本,在区块链网络中代表一个服务节点,节点不一定是一台机器,也可能是一个处理单元,下面统一称作节点。注意,所有节点都是用一套代码编译部署的环境。
3. 本地节点
本文依据命令runscripts/smartrun.sh bftsmart.demo.counter.CounterServer 0
,因此本地节点都指的是CounterServer,id为0的节点。本文只研究本地作为节点的情况。而本地作为客户端的情况(CounterClient作为入口),在后续文章介绍。
4. 配置域
组网配置文件host.config所描述的,由确定数量且顺序编号的节点所组成的网络,我们可以称之为配置域。下面就是示例节点的host.config的内容。
#server id, address and port (the ids from 0 to n-1 are the service replicas)
0 127.0.0.1 11000 11001
1 127.0.0.1 11010 11011
2 127.0.0.1 11020 11021
3 127.0.0.1 11030 11031
5. TTP
在system.config系统配置文件中有相关配置项:
system.ttp.id
该配置项的值是一个节点id,所以只能配置一个,我们称之为TTPid。一般是在配置域外,可用作向系统添加和删除节点。注意,根据约定,TTPid一定大于配置域任意id。
6. 陌生域
那么如果一个节点id既不属于配置域,又不是ttpid,我们称之为陌生id。除配置域和ttp以外的所有空间,我们称之为陌生域。陌生id的加入,在主流区块链产品例如比特币等,都会有完整的解决方案。BFT-SMaRt的分布式网络中除了启动时的配置域和TTP,也允许陌生id的接入,后面会有相关介绍。换句话讲,P2P网络最精彩的部分就是与陌生域的自由联系。当然了,如果是成熟的联盟链产品,会通过权限控制管理配置域、TTP和陌生域。
三、节点服务类
进入ServiceReplica的构造函数就意味着离开了示例程序,深入到了BFT-SMaRt标准库内容。ServiceReplica类可以被称为本地节点服务类,主要用作管理本地作为节点的基础服务,包括网络通信和节点间消息共识。这个类从DeliveryThread接收消息,并管理应用程序的执行和对客户端的回复。对于顺序消息逐个执行的应用程序,ServiceReplica接收一致决定的批处理,逐个交付,并使用批处理回复。在应用程序成批执行消息的情况下,该批消息被交付给应用程序,ServiceReplica不需要成批组织应答。
/**
* Constructor
*
* @param id 节点(副本)ID
* @param configHome 配置文件
* @param executor 执行器
* @param recoverer 恢复器
* @param verifier 请求校验器
* @param replier 请求回复器
* @param loader 加载签名器
*/
public ServiceReplica(int id, String configHome, Executable executor, Recoverable recoverer, RequestVerifier verifier, Replier replier, KeyLoader loader) {
this.id = id;
// 读取配置文件,构建配置域视图实例。
this.SVController = new ServerViewController(id, configHome, loader); // NEXT TODO
this.executor = executor; // 传入执行程序
this.recoverer = recoverer; // 传入恢复程序
this.replier = (replier != null ? replier : new DefaultReplier()); // 回复的包装类
this.verifier = verifier; // null,判断请求有效性,只是true/flase,未展开有效判断的逻辑。
this.init(); // 节点初始化(重点)
// 节点环境,上下文内容,属共识层内容,在恢复程序和回复消息时都被需要。
this.recoverer.setReplicaContext(replicaCtx); // NEXT TODO
this.replier.setReplicaContext(replicaCtx); // NEXT TODO
}
回复器接口Replier只有一个实现类DefaultReplier。它的功能就是在正常的回复开始前,保证对节点上下文ReplicaContext的校验。当ReplicaContext为空时,会挂起等待,直到有值时,才会走正常的回复。 ServerViewController是共识层的内容,本篇不展开。接下来,进入执行init初始化函数。
private void init() {
try {
cs = new ServerCommunicationSystem(this.SVController, this); // 创建本地节点通信系统
} catch (Exception ex) {
logger.error("Failed to initialize replica-to-replica communication system", ex);
throw new RuntimeException("Unable to build a communication system.");
}
if (this.SVController.isInCurrentView()) {
logger.info("In curre