乾貨 | 程式設計小白模擬簡易比特幣系統,手把手帶你寫一波(附程式碼)

買賣虛擬貨幣

如果有一個 p2p 的 demo,我們要怎麼才能應用到區塊鏈當中?

今天就來一起嘗試一下吧!

首先,我們需要模擬網路中的多個節點相互通訊,我們假設現在的情況是有ab兩個節點整個過程如下圖所示:

梳理流程

讓我們來梳理一下整個流程,明確在p2p網路中需要做的事情。

  1. 啟動節點a。a首先建立一個創世區塊
  2. 建立錢包a1。呼叫節點a提供的api建立一個錢包,此時a1的球球幣為0。
  3. a1挖礦。呼叫節點a提供的挖礦api,生成新的區塊,同時為a1的錢包有了系統獎勵的球球幣。
  4. 啟動節點b。節點b要向a同步資訊,當前的區塊鏈,當前的交易池,當前的所有錢包的公鑰。
  5. 建立錢包b1、a2,呼叫節點a和b的api,要廣播(通知每一個節點)出去建立的錢包(公鑰),目前節點只有兩個,因此a需要告訴b,a2的錢包。b需要告訴a,b1的錢包。
  6. a1轉賬給b1。呼叫a提供的api,同時廣播交易
  7. a2挖礦記賬。呼叫a提供的api,同時廣播新生成的區塊

總結一下,就是節點剛開始加入到區塊鏈網路中,需要同步其他節點的

  • 區塊鏈資訊
  • 錢包資訊
  • 交易資訊

已經處於網路中的某個節點,在下述情況下需要通知網路中的其他節點

  • 發生新的交易
  • 建立新的錢包
  • 挖礦產生新區塊

p2p的大致流程為下方几點,我們後邊的實現會結合這個過程。

  1. client→server 傳送訊息,一般是請求資料
  2. server收到訊息後,向client傳送訊息 (呼叫service,處理後返回資料)
  3. 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一笑

免責聲明:

  1. 本文版權歸原作者所有,僅代表作者本人觀點,不代表鏈報觀點或立場。
  2. 如發現文章、圖片等侵權行爲,侵權責任將由作者本人承擔。
  3. 鏈報僅提供相關項目信息,不構成任何投資建議

推荐阅读

;