在這篇博客中,我會經過Java 去實現PBFT中結點的加入,以及認證。其中使用socket實現網絡信息傳輸。html
關於PBFT算法的一些介紹,你們能夠去看一看網上的博客,也能夠參考個人上上一篇博客,關於怎麼構建P2P網絡能夠參考個人上一篇博客。java
該項目的地址:GitHubnode
使用maven構建項目,固然,也能夠不使用,這個就看本身的想法吧。git
須要使用到的Java包:github
首先的首先,咱們須要來定義一下結點的數據結構。算法
首先是結點Node的數據結構:json
@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