已经看完第一章的内容了吗,欢迎回来。
上一章我们介绍了关于怎么去编写自己的区块链,完成哈希和新块的校验。但是它只是在一个终端(结点)上跑。我们怎么样来连接其他结点以及贡献新的块呢,怎么样广播到其他结点告诉他们要更新区块了呢?
本章就是要告诉你这些。
// 区块链的核心部分
// 维护一个在启动时可以连接的对等节点列表。当一个完整的节点第一次启动时,它必须被自举(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): "); 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! "); pw.write(gson.toJson(blockChain)); } else { pw.write("HTTP 500: Invalid Block Error "); } LOGGER.info("add new block with vac:" + vac); } catch (Exception e) { LOGGER.error("not a number:", e); pw.write("not a number! "); } pw.write("Please enter a new number(vac):" + " "); // 调用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(" ------------broadcast------------- "); LOGGER.info(gson.toJson(blockChain)); pw = new PrintWriter(socket.getOutputStream()); // 发送到其他结点 pw.write("------------broadcast------------- "); 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)。