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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM