如果有一個 p2p 的 demo,我們要怎麼才能應用到區塊鏈當中?
今天就來一起嘗試一下吧!
首先,我們需要模擬網路中的多個節點相互通訊,我們假設現在的情況是有ab兩個節點整個過程如下圖所示:
梳理流程
讓我們來梳理一下整個流程,明確在p2p網路中需要做的事情。
- 啟動節點a。a首先建立一個創世區塊
- 建立錢包a1。呼叫節點a提供的api建立一個錢包,此時a1的球球幣為0。
- a1挖礦。呼叫節點a提供的挖礦api,生成新的區塊,同時為a1的錢包有了系統獎勵的球球幣。
- 啟動節點b。節點b要向a同步資訊,當前的區塊鏈,當前的交易池,當前的所有錢包的公鑰。
- 建立錢包b1、a2,呼叫節點a和b的api,要廣播(通知每一個節點)出去建立的錢包(公鑰),目前節點只有兩個,因此a需要告訴b,a2的錢包。b需要告訴a,b1的錢包。
- a1轉賬給b1。呼叫a提供的api,同時廣播交易。
- a2挖礦記賬。呼叫a提供的api,同時廣播新生成的區塊。
總結一下,就是節點剛開始加入到區塊鏈網路中,需要同步其他節點的
- 區塊鏈資訊
- 錢包資訊
- 交易資訊
已經處於網路中的某個節點,在下述情況下需要通知網路中的其他節點
- 發生新的交易
- 建立新的錢包
- 挖礦產生新區塊
p2p的大致流程為下方几點,我們後邊的實現會結合這個過程。
- client→server 傳送訊息,一般是請求資料
- server收到訊息後,向client傳送訊息 (呼叫service,處理後返回資料)
- client收到訊息處理資料(呼叫service,對資料處理)
相關程式碼
在實現的過程中,由於訊息型別較多,封裝了一個訊息物件用來傳輸訊息,對訊息型別進行編碼,統一處理,訊息物件message,實現了serializable介面,使其物件可序列化:
public class message implements serializable {
/**
* 訊息內容,就是我們的區塊鏈、交易池等所需要的資訊,使用json.tostring轉化到的json字串
*/
private string data;
/**
* 訊息型別
*/
private int type;
}
涉及到的訊息型別(type)有:
/**
* 查詢最新的區塊
*/
private final static int query_latest_block = 0;
/**
* 查詢整個區塊鏈
*/
private final static int query_block_chain = 1;
/**
* 查詢交易集合
*/
private final static int query_transaction = 2;
/**
* 查詢已打包的交易集合
*/
private final static int query_packed_transaction = 3;
/**
* 查詢錢包集合
*/
private final static int query_wallet = 4;
/**
* 返回區塊集合
*/
private final static int response_block_chain = 5;
/**
* 返回交易集合
*/
private final static int response_transaction = 6;
/**
* 返回已打包交易集合
*/
private final static int response_packed_transaction = 7;
/**
* 返回錢包集合
*/
private final static int response_wallet = 8;
由於程式碼太多,就不全部粘在這裡了,以client同步其他節點錢包資訊為例,結合上面的p2p網路互動的三個步驟,為大家介紹下相關的實現。
1、client→server 傳送訊息,一般是請求資料
在client節點的啟動類首先建立client物件,呼叫client內部方法,連線server。
啟動類main方法中關鍵程式碼,(埠引數配置在args中):
p2pclient p2pclient = new p2pclient();
string url = "ws://localhost:"+args[0]+"/test";
p2pclient.connecttopeer(url);
p2pclient中的connecttopeer方法
public void connecttopeer(string url) throws ioexception, deploymentexception {
websocketcontainer container = containerprovider.getwebsocketcontainer();
uri uri = uri.create(url);
this.session = container.connecttoserver(p2pclient.class, uri);
}
p2pclient中,websocketcontainer.connecttoserver的時候會回撥onopen函式,假設我們只查詢錢包公鑰資訊,此時服務端會接收到相應的請求。
@onopen
public void onopen(session session) {
this.session = session;
p2pservice.sendmsg(session, p2pservice.querywalletmsg());
}
注意:我把解析訊息相關的操作封裝到了一個service 中,方便server和client的統一使用。給出相應的querywalletmsg方法:
public string querywalletmsg() {
return json.tojsonstring(new message(query_wallet));
}
以及之前提到的sendmsg方法:
@override
public void sendmsg(session session, string msg) {
session.getasyncremote().sendtext(msg);
}
2、server收到訊息後,向client傳送訊息 (呼叫service,處理後返回資料)
server收到訊息,進入p2pserver中onmessage方法
/**
* 收到客戶端發來訊息
* @param msg 訊息物件
*/
@onmessage
public void onmessage(session session, string msg) {
p2pservice.handlemessage(session, msg);
}
p2pservice.handlemessage就是解析接收到的訊息(msg),根據型別的不同呼叫其他的方法(一個巨型switch語句,這裡就介紹一小部分),這裡我們接收到了client傳來的資訊碼query_wallet。
@override
public void handlemessage(session session, string msg) {
message message = json.parseobject(msg, message.class);
switch (message.gettype()){
case query_wallet:
sendmsg(session, responsewallets());
break;
case response_wallet:
handlewalletresponse(message.getdata());
break;
......
}
根據資訊碼是query_wallet,呼叫 responsewallets()方法,得到資料。
private string responsewallets() {
string wallets = blockservice.findallwallets();
return json.tojsonstring(new message(response_wallet, wallets));
}
這裡我把區塊鏈的相關操作也封裝到了一個service中,下面給出findallwallets的具體實現,其實就是遍歷錢包集合,統計錢包公鑰,沒有什麼難度。
@override
public string findallwallets() {
list wallets = new arraylist<>();
mywalletmap.foreach((address, wallet) ->{
wallets.add(wallet.builder().publickey(wallet.getpublickey()).build());
});
otherwalletmap.foreach((address, wallet) ->{
wallets.add(wallet);
});
return json.tojsonstring(wallets);
}
得到資料之後,返回給client:
因此我們的 responsewallets()方法中,最後一句話新建了一個message物件,並設定了資訊碼為response_wallet,在handlemessage中呼叫了sendmsg方法回傳給client。
case query_wallet:
sendmsg(session, responsewallets());
break;
3、client收到訊息處理資料(呼叫service,對資料處理)
client收到了請求得到的資料,進入p2pclient中的onmessage方法
@onmessage
public void onmessage(string msg) {
p2pservice.handlemessage(this.session, msg);
}
同樣進入我們上面提到的p2pservice.handlemessage方法,此時收到的資訊碼為response_wallet,進入handlewalletresponse方法
case response_wallet:
handlewalletresponse(message.getdata());
break;
handlewalletresponse的實現, 解析接收到的錢包公鑰資訊,並儲存到client節點的blockservice中。
private void handlewalletresponse(string msg) {
list wallets = "\"[]\"".equals(msg)?new arraylist<>():json.parsearray(msg, wallet.class);
wallets.foreach(wallet -> {
blockservice.addotherwallet(walletservice.getwalletaddress(wallet.getpublickey()),wallet );
});
}
在具體實現中,由於使用到了注入服務的方式,在向server(@serverendpoint)和client(@clientendpoint)中使用@autowired 註解注入bean的時候,由於spring boot 單例的特點,而websocket每次都會建立一個新的物件,所以在使用服務的時候會導致出現空指標異常,因此,我們建立了一個工具類springtil,每次需要服務時,都從spring容器中獲取到我們所需要的bean,下面給出工具類程式碼。
public class springutil implements applicationcontextaware { public static applicationcontext applicationcontext; @override public void setapplicationcontext(applicationcontext applicationcontext) throws beansexception { if (springutil.applicationcontext != null) { springutil.applicationcontext = applicationcontext; } } /** * 獲取applicationcontext */ public static applicationcontext getapplicationcontext() { return applicationcontext; }
/** * 透過name獲取 bean. */ public static object getbean(string name) { return getapplicationcontext().getbean(name); } /** * 透過class獲取bean. */ public static t getbean(class clazz) { return getapplicationcontext().getbean(clazz); }
/** * 透過name,以及clazz返回指定的bean */ public static t getbean(string name, class clazz) { return getapplicationcontext().getbean(name, clazz); } }
因此測試之前我們首先需要設定springutil中的applicationcontext,下面給出啟動類(為了簡單測試,兩個節點共用一個啟動類,根據args的不同來分別處理)以及相關節點的配置。
public static void main(string[] args) {
system.out.println("hello world");
springutil.applicationcontext = springapplication.run(hello.class, args);
if (args.length>0){
p2pclient p2pclient = new p2pclient();
string url = "ws://localhost:"+args[0]+"/test";
try {
p2pclient.connecttopeer(url);
} catch (exception e) {
e.printstacktrace();
}
}
使用時,我們需要手動獲取bean
//之前是這樣//@autowired//private p2pservice p2pservice;//改正後,去掉autowired,每次使用都手動獲取beanprivate p2pservice p2pservice;@onopenpublic void onopen(session session) {//如果不使用那些,在這裡會報空指標異常,p2pservice 為 null p2pservice = springutil.getbean(p2pservice.class);//新增這句話從ivo容器中獲取bean p2pservice.sendmsg(session, p2pservice.querywalletmsg());}
hello節點,測試時作為server
test節點,測試時作為client。
到此,我們就實現了p2p網路中server節點與client節點的互動過程。
僅作分享學習用 來源:區塊鏈大本營 作者: vv一笑