PBFT 算法的java实现(上)

在这篇博客中,我会通过Java 去实现PBFT中结点的加入,以及认证。其中使用socket实现网络信息传输。

关于PBFT算法的一些介绍,大家可以去看一看网上的博客,也可以参考我的上上一篇博客,关于怎么构建P2P网络可以参考我的上一篇博客

该项目的地址:GitHub

使用前的准备

使用maven构建项目,当然,也可以不使用,这个就看自己的想法吧。

需要使用到的Java包:

  • t-io:使用t-io进行网络socket通信,emm,这个框架的文档需要收费(699RMB),但是这里我们只是简单的使用,不需要使用到其中很复杂的功能。
  • fastjson:Json 数据解析
  • lombok:快速的get,set以及toString
  • hutool:万一要用到呢?
  • lombok:节省代码
  • log4j:日志
  • guava:Google的一些并发包

结点的数据结构

首先的首先,我们需要来定义一下结点的数据结构。

首先是结点Node的数据结构:

@Data
public class Node extends NodeBasicInfo{ /**
* 单例设计模式
* @return
*/
public static Node getInstance(){
return node;
}
private Node(){} private static Node node = new Node(); /**
* 判断结点是否运行
*/
private boolean isRun = false; /**
* 视图状态,判断是否ok,
*/
private volatile boolean viewOK;
} @Data
public class NodeBasicInfo {
/**
* 结点地址的信息
*/
private NodeAddress address;
/**
* 这个代表了结点的序号
*/
private int index; } @Data
public class NodeAddress {
/**
* ip地址
*/
private String ip;
/**
* 通信地址的端口号
*/
private int port; }

上面的代码看起来有点多,但实际上很少(上面是3个类,为了展示,我把它们放在了一起)。上面定义了Node应该包含的属性信息:ip,端口,序列号index,view是否ok。

结点的信息很简单。接下来我们就可以看一看PbftMsg的数据结构了。PbftMsg代表的是进行Pbft算法发送信息的数据结构。

@Data
public class PbftMsg {
/**
* 消息类型
*/
private int msgType; /**
* 消息体
*/
private String body; /**
* 消息发起的结点编号
*/
private int node; /**
* 消息发送的目的地
*/
private int toNode; /**
* 消息时间戳
*/
private long time; /**
* 检测是否通过
*/
private boolean isOk; /**
* 结点视图
*/
private int viewNum; /**
* 使用UUID进行生成
*/
private String id; private PbftMsg() {
} public PbftMsg(int msgType, int node) {
this.msgType = msgType;
this.node = node;
this.time = System.currentTimeMillis();
this.id = IdUtil.randomUUID();
this.viewNum = AllNodeCommonMsg.view;
} @Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
PbftMsg msg = (PbftMsg) o;
return node == msg.node &&
time == msg.time &&
viewNum == msg.viewNum &&
body.equals(msg.body) &&
id.equals(msg.id);
} @Override
public int hashCode() {
return Objects.hash(body, node, time, viewNum, id);
}
}

PBFTMSG这里我只是简单的定义了一下,并不是很严谨。在这里主要说下重要的属性:

msgType代表的是Pbft算法的消息类型,因为pbft算法有不同类型的请求消息。

同样,我们需要保存一些状态数据:

public class AllNodeCommonMsg {
/**
* 获得最大失效结点的数量
*
* @return
*/
public static int getMaxf() {
return (size - 1) / 3;
} /**
* 获得主节点的index序号
*
* @return
*/
public static int getPriIndex() {
return (view + 1) % size;
} /**
* 保存结点对应的ip地址和端口号
*/
public static ConcurrentHashMap<Integer, NodeBasicInfo> allNodeAddressMap = new ConcurrentHashMap<>(2 << 10) ; /**
* view的值,0代表view未被初始化
* 当前视图的编号,通过这个编号可以算出主节点的序号
*/
public volatile static int view = 0;
/**
* 区块链中结点的总结点数
*/
public static int size = allNodeAddressMap.size()+1;
}

逻辑流程

上面的定义看一看就行了,在这里我们主要是理解好PBFT算法的流程。在下面我们将好好的分析一下PBFT算法的流程。

合抱之木始于毫末,万丈高楼起于垒土。所有所有的开始,我们都需要从节点的加入开始说起。

在前前面的博客,我们知道一个在PBFT算法中有一个主节点,那么主节点是怎么出来的呢?当然是通过view算出来的。

设:结点数为N,当前视图为view,则主结点的id为:

$$primaryId = (view +1) mod N$$

因此,当一个节点启动的时候,他肯定是迷茫的,不知道自己是谁,这个时候就需要找一个节点问问目前是什么情况,问谁呢?肯定是问主节点,但是主节点是谁呢?在区块链中的节点当然都知道主节点是谁。这个时候,新启动的节点(姑且称之为小弟)就会向所有的节点去询问:大哥们,你们的view是多大啊,能不能行行好告诉小弟我!然后大哥们会将自己的view告诉小弟。但是小弟又担心大哥们骗他给他错误的view,所以决定当返回的view满足一定的数量的时候,就决定使用该view。

那么这个一定数量是多少呢?

quorum:达到共识需要的结点数量 $quorum = \lceil \frac {N + f +1 }{2 }\rceil $

说了这么多理论方面的东西,现在让我们来讲一讲代码方面是怎么考虑。

定义好两个简单的数据结构,我们就可以来想一想Pbft算法的流程了。

代码流程

首先的首先,我们先定义:节点的序号从0开始,view也从0开始,当然这个时候size肯定不是0,是1。so,主节点的序号是$primaryId = (0+1)%1 = 0$。

既然我们使用socket通信,使用的是t-io框架。我们就从服务端和客户端的方面来理解这个view的获取过程。神笔马良来了!!


这个从socket的角度的解释下过程。

首先区块链中的节点作为服务端,新加入的节点叫做客户端(遵循哲学态度,client发送请求询问server)。因为有多个server,因此对于D节点来说,就需要多个客户端分别对应不同的服务端发送请求。然后服务端将view返回给client。

然后说下代码,服务端接受到client发送的请求后,就将自己的view返回给client,然后client根据view的num决定哪一个才是真正的view。这里可以分为3个步骤:客户端请求view,服务端返回view,客户端处理view。

客户端请求view:

    /**
* 发送view请求
*
* @return
*/
public boolean pubView() {
log.info("结点开始进行view同步操作");
// 初始化view的msg
PbftMsg view = new PbftMsg(MsgType.GET_VIEW, node.getIndex());
// 将消息进行广播
ClientUtil.clientPublish(view);
return true;
}

上面的代码很简单,就是客户端向服务端广播PbftMsg,然后该消息的类型是GET_VIEW类型(也就是告诉大哥们,我是来请求view的)。

既然客户端广播了PBFT消息,当然服务端就会接受到。

下面是server端的代码,至于服务端是怎么接收到的,参考我的上一篇博客,或者别人的博客。当服务端接受到view的请求消息后,就会将自己的view发送给client。

    /**
* 将自己的view发送给client
*
* @param channelContext
* @param msg
*/
private void onGetView(ChannelContext channelContext, PbftMsg msg) {
log.info("server结点回复视图请求操作");
int fromNode = msg.getNode();
// 设置消息的发送方
msg.setNode(node.getIndex());
// 设置消息的目的地
msg.setToNode(fromNode);
// 设置消息的view
msg.setViewNum(AllNodeCommonMsg.view);
String jsonView = JSON.toJSONString(msg);
MsgPacket msgPacket = new MsgPacket();
try {
msgPacket.setBody(jsonView.getBytes(MsgPacket.CHARSET));
// 将消息发送给client
Tio.send(channelContext, msgPacket);
} catch (UnsupportedEncodingException e) {
log.error(String.format("server结点发送view消息失败%s", e.getMessage()));
}
}

然后是client接受到server返回的消息,然后进行处理。

    /**
* 获得view
*
* @param msg
*/
private void getView(PbftMsg msg) {
// 如果节点的view好了,当然也就不要下面的处理了
if (node.isViewOK()) {
return;
}
// count代表有多少位大哥返回该view
long count = collection.getViewNumCount().incrementAndGet(msg.getViewNum());
// count >= 2 * AllNodeCommonMsg.getMaxf()则代表该view 可以
if (count >= 2 * AllNodeCommonMsg.getMaxf() + 1 && !node.isViewOK()) {
collection.getViewNumCount().clear();
node.setViewOK(true);
AllNodeCommonMsg.view = msg.getViewNum();
log.info("视图初始化完成OK");
}
}

在这里大家可能会发现一个问题,我在第二个if中还是使用了!node.isViewOK()。那是因为我发现在多线程的情况下,即使view设置为true了,下面的代码还是会执行,也就是说log.info("视图初始化完成OK");会执行两次,因此我又加了一个view检测。

同样,我们可以来实现一下视图变更(ViewChange)的算法。

什么时候会产生viewChange呢?当然是主节点失效的时候,就会进行viewchange的执行。当某一个节点发现主节点失效时(也即是断开连接的时候),他就会告诉所有的节点(进行广播):啊!!不好了,主节点GG了,让我们重新选择一个主节点吧。因此,当节点收到quorum个重新选举节点的消息时,他就会将改变自己的视图。

这里有一个前提,就是当主节点和客户端断开的时候,客户端会察觉到。

client的代码:

重新选举view就是将目前的veiw+1,然后讲该view广播出去。

    /**
* 发送重新选举的消息
* 这个onChangeView是通过其它函数调用的,msg的内容如下所示
* PbftMsg msg = new PbftMsg(MsgType.CHANGE_VIEW,node.getIndex());
*/
private void onChangeView(PbftMsg msg) {
// view进行加1处理
int viewNum = AllNodeCommonMsg.view + 1;
msg.setViewNum(viewNum);
ClientUtil.clientPublish(msg);
}

服务端代码:

服务端代码和前面的的代码很类似。

    /**
* 重新设置view
*
* @param channelContext
* @param msg
*/
private void changeView(ChannelContext channelContext, PbftMsg msg) {
if (node.isViewOK()) {
return;
}
long count = collection.getViewNumCount().incrementAndGet(msg.getViewNum()); if (count >= 2 * AllNodeCommonMsg.getMaxf() + 1 && !node.isViewOK()) {
collection.getViewNumCount().clear();
node.setViewOK(true);
AllNodeCommonMsg.view = msg.getViewNum();
log.info("视图变更完成OK");
}
}

总结

在这里,大家可能会有个疑惑,为什么进行广播消息不是使用服务端去广播消息,反而是使用client一个一个的去广播消息。原因有一下两点:

  • 因为没有购买t-io文档,因此我也不知道server怎么进行广播消息。因为它取消了学生优惠,现在需要699¥,实在是太贵了(当然这个贵是针对与我而言的,不过这个框架还是真的挺好用的)舍不得买。

  • 为了是思路清晰,client就是为了请求数据,而server就是为了返回数据。这样想的时候,不会是自己的思路断掉

在这里为止,我们就简单的实现了节点加入和view的变迁(当然是最简单的实现,emm,大佬勿喷)。在下篇博客中,我将会介绍共识过程的实现。如果这篇博客有错误的地方,望大佬指正。可以在评论区留言或者邮箱联系。

项目地址:GitHub

PBFT算法java实现的更多相关文章

  1. PBFT 算法 java实现(下)

    PBFT 算法的java实现(下) 在上一篇博客中(如果没有看上一篇博客建议去看上一篇博客),我们介绍了使用Java实现PBFT算法中节点的加入,view的同步等操作.在这篇博客中,我将介绍PBFT算 ...

  2. 归并排序算法 java 实现

    归并排序算法 java 实现 可视化对比十多种排序算法(C#版) [直观学习排序算法] 视觉直观感受若干常用排序算法 算法概念 归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Di ...

  3. 快速排序算法 java 实现

    快速排序算法 java 实现 快速排序算法Java实现 白话经典算法系列之六 快速排序 快速搞定 各种排序算法的分析及java实现 算法概念 快速排序是C.R.A.Hoare于1962年提出的一种划分 ...

  4. 堆排序算法 java 实现

    堆排序算法 java 实现 白话经典算法系列之七 堆与堆排序 Java排序算法(三):堆排序 算法概念 堆排序(HeapSort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,可以利用数组的特 ...

  5. Atitit 电子商务订单号码算法(java c# php js 微信

    Atitit 电子商务订单号码算法(java c# php js  微信 1.1. Js版本的居然钱三爷里面没有..只好自己实现了. 1.2. 订单号标准化...长度16位 1.3. 订单号的结构 前 ...

  6. 无向图的最短路径算法JAVA实现

    一,问题描述 给出一个无向图,指定无向图中某个顶点作为源点.求出图中所有顶点到源点的最短路径. 无向图的最短路径其实是源点到该顶点的最少边的数目. 本文假设图的信息保存在文件中,通过读取文件来构造图. ...

  7. 无向图的最短路径算法JAVA实现(转)

    一,问题描述 给出一个无向图,指定无向图中某个顶点作为源点.求出图中所有顶点到源点的最短路径. 无向图的最短路径其实是源点到该顶点的最少边的数目. 本文假设图的信息保存在文件中,通过读取文件来构造图. ...

  8. 基于FP-Tree的关联规则FP-Growth推荐算法Java实现

    基于FP-Tree的关联规则FP-Growth推荐算法Java实现 package edu.test.ch8; import java.util.ArrayList; import java.util ...

  9. 双色球机选算法java实现

    双色球机选算法java实现 一.代码 package com.hdwang; import java.util.Random; /** * Created by admin on 2017/1/10. ...

随机推荐

  1. django框架中的静态文件引入

    首先在项目文件中新建文件夹static 之后在settings.py中配置路径 如下图所示: 下一步在你刚创建的static文件夹中添加app的文件夹名称,例如:teacher,如下图: 之后在tea ...

  2. 干货!直击JVM底层 —— Java Class字节码文件解析

    目录 前言 如何阅读class文件 基本概念 无符号数&表 常量池 魔数(magic number) & 版本号 常量池 访问标志 类引索&父类引索&接口引索集合 字段 ...

  3. cannot insert multiple commands into a prepared statement问题原因及解决办法

    问题是这样,我在对数据库进行写操作(添加.删除.修改)时,我想同时删除两个表中的两条关联数据,像这样 let sql = ` DELETE FROM bridge_parts WHERE id = $ ...

  4. restapi-sql

    身份验证,确定该成员是交过费的机构的成员,包含(用户名)和(密码) 各个表中的属性,有关timetemp等特殊类型,LocalDate等日期等具体格式. 引入了传输过程的不同的数据格式导致的两个错误, ...

  5. python循环语句(while和for)

    循环语句分成两种,while循环 和 for循环 作用:可以使指定的代码块重复指定的次数 while循环: # 语法: # while 条件表达式 : # 代码块 # else : # 代码块 # 执 ...

  6. 2018 Multi-University Training Contest 10

      Recently, TeaTree acquire new knoledge gcd (Greatest Common Divisor), now she want to test you. As ...

  7. 图解 Kubernetes

    容器 在了解 Kubernetes 之前,让我们先了解一个容器. 因为如果不了解容器就没法聊容器编排. 容器就是...一个你塞入所有材料的容器. "材料"是指你的应用代码.依赖库, ...

  8. CF6B President's Office 题解

    看到大致思路一致的题解,决定发一篇运用STL不用dfs的题解     好久不发题解,心里不爽 思路: 1.输入的同时找到总统桌子的位置,用vector<pair <int,int> ...

  9. Uva1014:Remember the Word

    Neal is very curious about combinatorial problems, and now here comes a problem about words. Knowing ...

  10. 总是在起头可是能怎么办呢 Python数据分析

    目录 前言1 第1章准备工作5 本书主要内容5 为什么要使用Python进行数据分析6 重要的Python库7 安装和设置10 社区和研讨会16 使用本书16 致谢18 第2章引言20 来自bit.l ...