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的資料結構:
@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