已經看完第一章的內容了嗎,歡迎回來。
上一章我們介紹了關於怎么去編寫自己的區塊鏈,完成哈希和新塊的校驗。但是它只是在一個終端(結點)上跑。我們怎么樣來連接其他結點以及貢獻新的塊呢,怎么樣廣播到其他結點告訴他們要更新區塊了呢?
本章就是要告訴你這些。
// 區塊鏈的核心部分
// 維護一個在啟動時可以連接的對等節點列表。當一個完整的節點第一次啟動時,它必須被自舉(bootstrapped)到網絡。
// 自舉過程完成后,節點向其對等節點發送一個包含其自身IP地址的addr消息。其對等的每個節點向它們自己的對等節點轉發這個信息,以便進一步擴大連接池。
// 塊廣播
// 在與對等節點建立連接后,雙方互發包含最新塊哈希值的getblocks消息。
// 如果某個節點堅信其擁有最新的塊信息或者有更長的鏈,它將發送一個inv消息(invite),其中包含至多500個最新塊的哈希值,以此來表明它的鏈更長。
// 收到的節點使用getdata來請求塊的詳細信息,而遠程的節點通過命令block來發送這些信息。
// 在500個塊的信息被處理完之后,節點可以通過getblocks請求更多的塊信息。這些塊在被接收節點認證之后得到確認。
// 新塊的確認也可通過礦工挖到並發布的塊來發現。其擴散過程和上述類似。
// 通過之前的連接,新塊以inv消息發布出去,而接收節點可以通過getdata請求這些塊的詳細信息。
我們會做什么
1.建立第一個結點,作為tcp server監聽
2.打開一個終端,連接到第一個結點上來,模擬新產生區塊
3.結點將新的區塊鏈以廣播的形式傳播到所有結點
我們不會做什么
與上一篇文章一樣,本教程的目的是模擬節點網絡,以便您可以直觀的認識區塊鏈網絡。所以不會像真實的區塊鏈網絡一樣去實現所有功能,對以上功能會進行簡化。
讓我們開始編碼吧!
除了上一章講過的區塊內容,我們將去掉http的部分,今天講的重點是TCP構建網絡
TCP 和 HTTP 有什么差別?
這里我們不會詳細討論,但是您需要知道的是TCP是一個傳輸數據的基本協議。HTTP建立在TCP之上,以在web和瀏覽器上使用此數據傳輸。當您查看一個網站時,您使用的是HTTP,它是由一個名為TCP的底層數據傳輸協議支持的。
在本教程中,我們將使用TCP,因為我們不需要在瀏覽器中查看任何內容。
我們創建一個Node.java的文件,來開始我們的編寫。
Imports
包聲明我們需要的導入。
import java.net.ServerSocket;
import java.net.Socket;
Review
我們的塊,以及塊相關操作的內容都沒有變,索性我們把這些內容放在一起,新建BlockUtils.java文件,內容如下:
public class BlockUtils { /** * 計算區塊的hash值 * * @param block * 區塊 * @return */ public static String calculateHash(Block block) { String record = (block.getIndex()) + block.getTimestamp() + (block.getVac()) + block.getPrevHash(); MessageDigest digest = DigestUtils.getSha256Digest(); byte[] hash = digest.digest(StringUtils.getBytesUtf8(record)); return Hex.encodeHexString(hash); } /** * 區塊的生成 * * @param oldBlock * @param vac * @return */ public static Block generateBlock(Block oldBlock, int vac) { Block newBlock = new Block(); newBlock.setIndex(oldBlock.getIndex() + 1); newBlock.setTimestamp(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())); newBlock.setVac(vac); newBlock.setPrevHash(oldBlock.getHash()); newBlock.setHash(calculateHash(newBlock)); return newBlock; } /** * 校驗區塊的合法性(有效性) * * @param newBlock * @param oldBlock * @return */ public static boolean isBlockValid(Block newBlock, Block oldBlock) { if (oldBlock.getIndex() + 1 != newBlock.getIndex()) { return false; } if (!oldBlock.getHash().equals(newBlock.getPrevHash())) { return false; } if (!calculateHash(newBlock).equals(newBlock.getHash())) { return false; } return true; } }
好了!我們已經基本得到了所有的區塊鏈相關函數,從第1章刪除了所有HTTP相關的內容。我們現在可以進行聯網了。
網絡
在此之前,讓我們聲明一個名為bcServer的全局變量(blockchainServer的簡稱),它是接收傳入塊的隊列。
我們從main函數開始,我們需要把結點作為TCP server啟動,監聽端口8333。
當網絡中有鏈接進來時,我們接收鏈接,並開線程處理它。這樣如果有很多結點連接過來,每個結點通訊都可以並行處理。
// 建立TCP監聽8333 serverSocket = new ServerSocket(8333); LOGGER.info("*** Node is started,waiting for others ***"); // 監聽對等網絡中的結點 for(;;) { final Socket socket = serverSocket.accept(); // 創建一個新的線程 ,和建立連接的結點通訊 new NodeThread(socket, blockChain).start(); // 模擬網絡結點廣播 new BroadcastThread(socket, blockChain).start(); }
讓我們來寫NodeThread的實現。當新的結點連上來,我們發出一條信息“請輸入一個資產值”,新結點輸入一個數值,創建一個新的塊,發送回鏈,我們將這個新的塊加入到鏈上。
代碼如下
BufferedReader br = null; PrintWriter pw = null; try { //提示結點輸入 pw = new PrintWriter(socket.getOutputStream()); pw.write("please enter a new number(vac):\n"); pw.flush(); String info = null; // 讀取結點發送的信息 br = new BufferedReader(new InputStreamReader(socket.getInputStream())); while ((info = br.readLine()) != null) { try { int vac = Integer.parseInt(info); // 根據vac創建區塊 Block newBlock = BlockUtils.generateBlock(blockChain.get(blockChain.size() - 1), vac); if (BlockUtils.isBlockValid(newBlock, blockChain.get(blockChain.size() - 1))) { blockChain.add(newBlock); pw.write("Success!\n"); pw.write(gson.toJson(blockChain)); } else { pw.write("HTTP 500: Invalid Block Error\n"); } LOGGER.info("add new block with vac:" + vac); } catch (Exception e) { LOGGER.error("not a number:", e); pw.write("not a number! \n"); } pw.write("Please enter a new number(vac):" + "\n"); // 調用flush()方法將緩沖輸出 pw.flush(); } } catch (Exception e) { LOGGER.error("TCP i/o error Or client closed", e); } finally { LOGGER.info("node closed:" + address.getHostAddress() + ",port:" + socket.getPort()); // 關閉資源 try { if (pw != null) { pw.close(); } if (br != null) { br.close(); } } catch (IOException e) { LOGGER.error("close error:", e); } }
模擬廣播
我們需要把得到的新鏈告訴所有鏈接到我們TCPServer的結點,因此,我們將模擬數據如何傳輸到所有的其他結點。
如何做呢,設置一個時間,定時把blockchain用json化寫入連接,通知所有連接結點。
代碼如下:
for (;;) { PrintWriter pw = null; try { Thread.sleep(30000); LOGGER.info("\n------------broadcast-------------\n"); LOGGER.info(gson.toJson(blockChain)); pw = new PrintWriter(socket.getOutputStream()); // 發送到其他結點 pw.write("------------broadcast-------------\n"); pw.write(gson.toJson(blockChain)); pw.flush(); } catch (InterruptedException e) { LOGGER.error("error:", e); } catch (IOException e) { LOGGER.error("error:", e); } }
可以到我的github查看完整的代碼
https://github.com/Mignet/blockchain
實驗步驟:
啟動第一個結點.Node
打開一個終端,執行命令nc localhost 8333
打開更多終端,執行命令nc localhost 8333
在任意終端輸入數字,看鏈的打印
每過30秒,看廣播到的鏈。
如果你有真正的網絡環境,想象一下,你的任意一台設備作為結點,都可能是第一個結點,它們直接通過tcp互相通訊,廣播,是不是也一樣類似上面的過程呢。
下一章,我們將談一談工作量證明算法(Proof Of Work)。