PBFT算法java实现(上)

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的数据结构:

1@Data 2public class Node extends NodeBasicInfo{ 3 4 /** 5 * 单例设计模式 6 * @return 7 */ 8 public static Node getInstance(){ 9 return node; 10 } 11 private Node(){} 12 13 private static Node node = new Node(); 14 15 /** 16 * 判断结点是否运行 17 */ 18 private boolean isRun = false; 19 20 /** 21 * 视图状态,判断是否ok, 22 */ 23 private volatile boolean viewOK; 24} 25 26@Data 27public class NodeBasicInfo { 28 /** 29 * 结点地址的信息 30 */ 31 private NodeAddress address; 32 /** 33 * 这个代表了结点的序号 34 */ 35 private int index; 36 37} 38 39@Data 40public class NodeAddress { 41 /** 42 * ip地址 43 */ 44 private String ip; 45 /** 46 * 通信地址的端口号 47 */ 48 private int port; 49 50}

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

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

1@Data 2public class PbftMsg { 3 /** 4 * 消息类型 5 */ 6 private int msgType; 7 8 /** 9 * 消息体 10 */ 11 private String body; 12 13 /** 14 * 消息发起的结点编号 15 */ 16 private int node; 17 18 /** 19 * 消息发送的目的地 20 */ 21 private int toNode; 22 23 /** 24 * 消息时间戳 25 */ 26 private long time; 27 28 /** 29 * 检测是否通过 30 */ 31 private boolean isOk; 32 33 /** 34 * 结点视图 35 */ 36 private int viewNum; 37 38 /** 39 * 使用UUID进行生成 40 */ 41 private String id; 42 43 private PbftMsg() { 44 } 45 46 public PbftMsg(int msgType, int node) { 47 this.msgType = msgType; 48 this.node = node; 49 this.time = System.currentTimeMillis(); 50 this.id = IdUtil.randomUUID(); 51 this.viewNum = AllNodeCommonMsg.view; 52 } 53 54 @Override 55 public boolean equals(Object o) { 56 if (this == o) { 57 return true; 58 } 59 if (o == null || getClass() != o.getClass()) { 60 return false; 61 } 62 PbftMsg msg = (PbftMsg) o; 63 return node == msg.node && 64 time == msg.time && 65 viewNum == msg.viewNum && 66 body.equals(msg.body) && 67 id.equals(msg.id); 68 } 69 70 @Override 71 public int hashCode() { 72 return Objects.hash(body, node, time, viewNum, id); 73 } 74}

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

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

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

1public class AllNodeCommonMsg { 2 /** 3 * 获得最大失效结点的数量 4 * 5 * @return 6 */ 7 public static int getMaxf() { 8 return (size - 1) / 3; 9 } 10 11 /** 12 * 获得主节点的index序号 13 * 14 * @return 15 */ 16 public static int getPriIndex() { 17 return (view + 1) % size; 18 } 19 20 /** 21 * 保存结点对应的ip地址和端口号 22 */ 23 public static ConcurrentHashMap<Integer, NodeBasicInfo> allNodeAddressMap = new ConcurrentHashMap<>(2 << 10) ; 24 25 /** 26 * view的值,0代表view未被初始化 27 * 当前视图的编号,通过这个编号可以算出主节点的序号 28 */ 29 public volatile static int view = 0; 30 /** 31 * 区块链中结点的总结点数 32 */ 33 public static int size = allNodeAddressMap.size()+1; 34}

逻辑流程

上面的定义看一看就行了,在这里我们主要是理解好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:

1 /** 2 * 发送view请求 3 * 4 * @return 5 */ 6 public boolean pubView() { 7 log.info("结点开始进行view同步操作"); 8 // 初始化view的msg 9 PbftMsg view = new PbftMsg(MsgType.GET_VIEW, node.getIndex()); 10 // 将消息进行广播 11 ClientUtil.clientPublish(view); 12 return true; 13 }

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

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

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

1 /** 2 * 将自己的view发送给client 3 * 4 * @param channelContext 5 * @param msg 6 */ 7 private void onGetView(ChannelContext channelContext, PbftMsg msg) { 8 log.info("server结点回复视图请求操作"); 9 int fromNode = msg.getNode(); 10 // 设置消息的发送方 11 msg.setNode(node.getIndex()); 12 // 设置消息的目的地 13 msg.setToNode(fromNode); 14 // 设置消息的view 15 msg.setViewNum(AllNodeCommonMsg.view); 16 String jsonView = JSON.toJSONString(msg); 17 MsgPacket msgPacket = new MsgPacket(); 18 try { 19 msgPacket.setBody(jsonView.getBytes(MsgPacket.CHARSET)); 20 // 将消息发送给client 21 Tio.send(channelContext, msgPacket); 22 } catch (UnsupportedEncodingException e) { 23 log.error(String.format("server结点发送view消息失败%s", e.getMessage())); 24 } 25 }

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

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

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

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

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

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

client的代码:

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

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

服务端代码:

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

1 /** 2 * 重新设置view 3 * 4 * @param channelContext 5 * @param msg 6 */ 7 private void changeView(ChannelContext channelContext, PbftMsg msg) { 8 if (node.isViewOK()) { 9 return; 10 } 11 long count = collection.getViewNumCount().incrementAndGet(msg.getViewNum()); 12 13 if (count >= 2 * AllNodeCommonMsg.getMaxf() + 1 && !node.isViewOK()) { 14 collection.getViewNumCount().clear(); 15 node.setViewOK(true); 16 AllNodeCommonMsg.view = msg.getViewNum(); 17 log.info("视图变更完成OK"); 18 } 19 }

总结

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

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

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

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

项目地址:GitHub

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )

Shodan的http.favicon.hash语法详解与使用技巧

  在Shodan搜索中有一个关于网站icon图标的搜索语法,http.favicon.hash,我们可以使用这个语法来搜索出使用了同一icon图标的网站,不知道怎么用的朋友请参考我上一篇(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fwww.cnblogs.com%2Fmia

PBFT算法java实现(上) - HelloWorld