只用120行Java代码写一个自己的区块链-4实现真正的p2p网络
在之前的文章中,我们模拟了节点网络通讯,很多朋友反馈说,他们想看真正的节点网络通讯而不是单节点的模拟。本章将满足你们。😌
我将本章的内容放在了com.v5ent.real.p2p包中,大家可以在源码中找到我更新的代码。
通过本文,你将可以做到:
- 创建自己的真实peer-to-peer网络
- 多个节点通过p2p网络同步区块内容
- 在自我节点实现RPC通讯,并向区块中写数据
- 在自我节点查看整个区块内容
- 不含虚拟货币在内的其他区块链知识
学习本章至少需要两台可ping通的机器(虚拟机也可以),并且安装了jdk8(如果只有jdk6,可以把代码改为jdk6支持的形式)。
基于之前的文章,本章将重写p2p部分的内容。我们首先要理清思路,提出区块链通讯需要解决的问题。
1.如果我第一次作为全节点启动,我需要干什么
2.如果我已经启动,别的节点和我通讯,我需要交互哪些信息
3.如果我已经启动,我怎么和自己的客户端交互
这些问题有一个很好的参考对象,就是比特币,我们先来看看比特币的通讯过程
一、比特币节点连接建立
1.寻找比特币网络中的有效节点,此步骤通常有两种方法:
(1)使用“DNS种子”(DNS seeds),DNS种子提供比特币节点的IP地址列表,Bitcoin Core客户端提供五种不同的DNS种子,通常使用默认方式。这里不展开谈论。
(2)手动通过-seednode命令指定一个比特币节点的IP地址作为比特币种子节点(为什么叫种子,我的理解就是,根据种子,得到更多)
这里我们使用简单的方式来处理节点,用一个文件来存储地址列表-peers.list,第一次连接的时候更新这个文件,获取更多连接时也更新这个文件。简单演示我们直接把要测试的机器ip和端口加进去。
我有两台机,我把端口8015用作新的p2p网络的默认监听端口,那么我的peers.list的内容是:
10.16.0.205:8015
10.16.3.77:8015
2.与发现的有效比特币节点进行初始“握手”,建立连接
节点发送一条包含基本认证内容的version消息开始“握手”通信过程,该消息包括如下内容:
-
nVersion:客户端的比特币P2P协议所采用的版本(例如:70002)。
-
nLocalServices:一组该节点支持的本地服务列表,当前仅支持NODE_NETWORK
-
nTime:当前时间
-
addrYou:当前节点可见的远程节点的IP地址(上例中NodeB IP)
-
addrMe:当前节点的IP地址(上例中NodeA IP)
-
subver:指示当前节点运行的软件类型的子版本号(例如:”/Satoshi:0.9.2.1/”)
-
BestHeight:当前节点区块链的区块高度(初始为0,即只包含创世区块)
我们简化一下,这里我们最关心的就是区块高度bestHeight,我们就传递这个好了。区块高度就是区块链的长度。
1 if ("VERSION".equalsIgnoreCase(cmd)) { 2 // 对方发来握手信息,我方发给对方区块高度和最新区块的hash 3 pt.peerWriter.write("VERACK " + blockChain.size() + " " + blockChain.get(blockChain.size() - 1).getHash()); 4 }else if ("VERACK".equalsIgnoreCase(cmd)) { 5 // 获取区块高度 6 String[] parts = payload.split(" "); 7 bestHeight = Integer.parseInt(parts[0]); 8 //哈希暂时不校验 9 }
3.新节点建立更多的连接,使节点在网络中被更多节点接收,保证连接更稳定
这里我们就两台机,如果你有更多机器,可以实现一下这个通讯,本章我们简单实现一下。
1 if ("ADDR".equalsIgnoreCase(cmd)) { 2 // 对方发来地址,建立连接并保存 3 if (!peers.contains(payload)) { 4 String peerAddr = payload.substring(0, payload.indexOf(":")); 5 int peerPort = Integer.parseInt(payload.substring(payload.indexOf(":") + 1)); 6 peerNetwork.connect(peerAddr, peerPort); 7 peers.add(payload); 8 PrintWriter out = new PrintWriter(peerFile); 9 for (int k = 0; k < peers.size(); k++) { 10 out.println(peers.get(k)); 11 } 12 out.close(); 13 } 14 } else if ("GET_ADDR".equalsIgnoreCase(cmd)) { 15 //对方请求更多peer地址,随机给一个 16 Random random = new Random(); 17 pt.peerWriter.write("ADDR " + peers.get(random.nextInt(peers.size()))); 18 }
4.交换“区块清单”(注:该步骤仅在全节点上会执行,且从与节点建立连接就开始进行)本系列内容只使用全节点。
全节点
全节点沿着区块链按时间倒叙一直追溯到创世区块,建立一个完整的UTXO数据库,通过查询UTXO是否未被支付来验证交易的有效性。
SPV节点
SPV节点通过向其他节点请求某笔交易的Merkle路径(Merkle树我可能会在后续章节讲到),如果路径正确无误,并且该交易之上已有6个或以上区块被确认,则证明该交易不是双重支付。
全节点在连接到其他节点后,需要构建完整的区块链,如果是新节点,它仅包含静态植入客户端中的0号区块(创世区块)。注意了,创世区块是静态的(硬编码)。
如前文所言,我们在区块链中取最长的链,区块高度比我高,我就向对方获取区块。
1 else if ("BLOCK".equalsIgnoreCase(cmd)) { 2 //把对方给的块存进链中 3 LOGGER.info("Attempting to add block..."); 4 LOGGER.info("Block: " + payload); 5 Block newBlock = gson.fromJson(payload, Block.class); 6 if (!blockChain.contains(newBlock)) { 7 // 校验区块,如果成功,将其写入本地区块链 8 if (BlockUtils.isBlockValid(newBlock, blockChain.get(blockChain.size() - 1))) { 9 if (blockChain.add(newBlock) && !catchupMode) { 10 LOGGER.info("Added block " + newBlock.getIndex() + " with hash: ["+ newBlock.getHash() + "]"); 11 peerNetwork.broadcast("BLOCK " + payload); 12 } 13 } 14 } 15 } else if ("GET_BLOCK".equalsIgnoreCase(cmd)) { 16 //把对方请求的块给对方 17 LOGGER.info("Sending block[" + payload + "] to peer"); 18 Block block = blockChain.get(Integer.parseInt(payload)); 19 if (block != null) { 20 LOGGER.info("Sending block " + payload + " to peer"); 21 pt.peerWriter.write("BLOCK " + gson.toJson(block)); 22 } 23 }
到这里,我们基本上完成了p2p网络中关于区块的通讯。
其实比特币等虚拟货币中还有很多通讯,关于交易的,这里我们不需要,不做讨论。
让我们开始编码吧!
整合上文提到的所有通讯,Node.java的代码如下
1 private static final Logger LOGGER = LoggerFactory.getLogger(Node.class); 2 3 /** 本地区块链 */ 4 private static List<Block> blockChain = new LinkedList<Block>(); 5 6 public static void main(String[] args) throws IOException, InterruptedException { 7 int port = 8015; 8 LOGGER.info("Starting peer network... "); 9 PeerNetwork peerNetwork = new PeerNetwork(port); 10 peerNetwork.start(); 11 LOGGER.info("[ Node is Started in port:"+port+" ]"); 17 ArrayList<String> peers = new ArrayList<>(); 18 File peerFile = new File("peers.list"); 19 if (!peerFile.exists()) { 20 String host = InetAddress.getLocalHost().toString(); 21 FileUtils.writeStringToFile(peerFile, host+":"+port); 22 } 23 for (Object peer : FileUtils.readLines(peerFile)) { 24 String[] addr = peer.toString().split(":"); 25 peerNetwork.connect(addr[0], Integer.parseInt(addr[1])); 26 } 27 TimeUnit.SECONDS.sleep(2); 28 29 peerNetwork.broadcast("VERSION"); 30 31 // hard code genesisBlock 32 Block genesisBlock = new Block(); 33 genesisBlock.setIndex(0); 34 genesisBlock.setTimestamp("2017-07-13 22:32:00");//my son's birthday 35 genesisBlock.setVac(0); 36 genesisBlock.setPrevHash(""); 37 genesisBlock.setHash(BlockUtils.calculateHash(genesisBlock)); 38 blockChain.add(genesisBlock); 39 40 final Gson gson = new GsonBuilder().create(); 41 LOGGER.info(gson.toJson(blockChain)); 42 int bestHeight = 0; 43 boolean catchupMode = true; 44 45 /** 46 * p2p 通讯 47 */ 48 while (true) { 49 //对新连接过的peer写入文件,下次启动直接连接 50 for (String peer : peerNetwork.peers) { 51 if (!peers.contains(peer)) { 52 peers.add(peer); 53 FileUtils.writeStringToFile(peerFile, peer); 54 } 55 } 56 peerNetwork.peers.clear(); 57 58 // 处理通讯 59 for (PeerThread pt : peerNetwork.peerThreads) { 60 if (pt == null || pt.peerReader == null) { 61 break; 62 } 63 List<String> dataList = pt.peerReader.readData(); 64 if (dataList == null) { 65 LOGGER.info("Null ret retry."); 66 System.exit(-5); 67 break; 68 } 69 70 for (String data:dataList) { 71 LOGGER.info("Got data: " + data); 72 int flag = data.indexOf(' '); 73 String cmd = flag >= 0 ? data.substring(0, flag) : data; 74 String payload = flag >= 0 ? data.substring(flag + 1) : ""; 75 if (StringUtils.isNotBlank(cmd)) { 76 if ("VERSION".equalsIgnoreCase(cmd)) { 77 // 对方发来握手信息,我方发给对方区块高度和最新区块的hash 78 pt.peerWriter.write("VERACK " + blockChain.size() + " " + blockChain.get(blockChain.size() - 1).getHash()); 79 }else if ("VERACK".equalsIgnoreCase(cmd)) { 80 // 获取区块高度 81 String[] parts = payload.split(" "); 82 bestHeight = Integer.parseInt(parts[0]); 83 //哈希暂时不校验 84 } else if ("GET_BLOCK".equalsIgnoreCase(cmd)) { 85 //把对方请求的块给对方 86 LOGGER.info("Sending block[" + payload + "] to peer"); 87 Block block = blockChain.get(Integer.parseInt(payload)); 88 if (block != null) { 89 LOGGER.info("Sending block " + payload + " to peer"); 90 pt.peerWriter.write("BLOCK " + gson.toJson(block)); 91 } 92 } else if ("BLOCK".equalsIgnoreCase(cmd)) { 93 //把对方给的块存进链中 94 LOGGER.info("Attempting to add block..."); 95 LOGGER.info("Block: " + payload); 96 Block newBlock = gson.fromJson(payload, Block.class); 97 if (!blockChain.contains(newBlock)) { 98 // 校验区块,如果成功,将其写入本地区块链 99 if (BlockUtils.isBlockValid(newBlock, blockChain.get(blockChain.size() - 1))) { 100 if (blockChain.add(newBlock) && !catchupMode) { 101 LOGGER.info("Added block " + newBlock.getIndex() + " with hash: ["+ newBlock.getHash() + "]"); 102 peerNetwork.broadcast("BLOCK " + payload); 103 } 104 } 105 } 106 }else if ("GET_ADDR".equalsIgnoreCase(cmd)) { 107 //对方请求更多peer地址,随机给一个 108 Random random = new Random(); 109 pt.peerWriter.write("ADDR " + peers.get(random.nextInt(peers.size()))); 110 } else if ("ADDR".equalsIgnoreCase(cmd)) { 111 // 对方发来地址,建立连接并保存 112 if (!peers.contains(payload)) { 113 String peerAddr = payload.substring(0, payload.indexOf(":")); 114 int peerPort = Integer.parseInt(payload.substring(payload.indexOf(":") + 1)); 115 peerNetwork.connect(peerAddr, peerPort); 116 peers.add(payload); 117 PrintWriter out = new PrintWriter(peerFile); 118 for (int k = 0; k < peers.size(); k++) { 119 out.println(peers.get(k)); 120 } 121 out.close(); 122 } 123 } 124 } 125 } 126 } 127 128 // ******************************** 129 // 比较区块高度,同步区块 130 // ******************************** 131 132 int localHeight = blockChain.size(); 133 134 if (bestHeight > localHeight) { 135 catchupMode = true; 136 LOGGER.info("Local chain height: " + localHeight); 137 LOGGER.info("Best chain Height: " + bestHeight); 138 TimeUnit.MILLISECONDS.sleep(300); 139 140 for (int i = localHeight; i < bestHeight; i++) { 141 LOGGER.info("请求块 " + i + "..."); 142 peerNetwork.broadcast("GET_BLOCK " + i); 143 } 144 } else { 145 if (catchupMode) { 146 LOGGER.info("[p2p] - Caught up with network."); 147 } 148 catchupMode = false; 149 } 150 151 152 153 // **************** 154 // 循环结束 155 // **************** 156 TimeUnit.MILLISECONDS.sleep(200); 157 } 158 }
PeerNetwork简单封装了一下p2p通讯的细节,篇幅有限我这里只列出核心交互,具体实现可以去看我的github源码中的类:PeerThread、PeerReader、PeerWriter。
RPC
接下来,我们将讨论关于本地节点客户端的概念。在比特币中,除了bitcoin-core,还有bitcoin-cli,这是做什么的呢。
它其实是用来做本地节点交互的,比如我作为本地节点,我需要发起交易,需要查看我的资产等等,后来发展出gui界面,就是大家俗称的钱包。
在本章中,我们也需要这样一个客户端通讯,用来将我们的vac写入链中(之前的文章,我们是用控制台输入的,实际的做法是提供加密的rpc调用)我们接下来实现RPC服务
首先我们要在Node.java中加入通讯逻辑
LOGGER.info("Starting RPC daemon... ");
RpcServer rpcAgent = new RpcServer(port+1);
rpcAgent.start();
LOGGER.info("[ RPC agent is Started in port:"+(port+1)+" ]");
for循环体中增加
/** * 处理RPC服务 */ for (RpcThread th:rpcAgent.rpcThreads) { String request = th.req; if (request != null) { String[] parts = request.split(" "); parts[0] = parts[0].toLowerCase(); if ("getinfo".equals(parts[0])) { String res = gson.toJson(blockChain); th.res = res; } else if ("send".equals(parts[0])) { try { int vac = Integer.parseInt(parts[1]); // 根据vac创建区块 Block newBlock = BlockUtils.generateBlock(blockChain.get(blockChain.size() - 1), vac); if (BlockUtils.isBlockValid(newBlock, blockChain.get(blockChain.size() - 1))) { blockChain.add(newBlock); th.res = "write Success!"; peerNetwork.broadcast("BLOCK " + gson.toJson(newBlock)); } else { th.res = "RPC 500: Invalid vac Error\n"; } } catch (Exception e) { th.res = "Syntax (no '<' or '>'): send <vac> <privateKey>"; LOGGER.error("invalid vac", e); } } else { th.res = "Unknown command: \"" + parts[0] + "\""; } } }
独立线程处理
RpcThread.java
package com.v5ent.real.p2p; import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.Socket; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * 处理单个rpc连接 * @author Mignet */ public class RpcThread extends Thread { private static final Logger LOGGER = LoggerFactory.getLogger(RpcThread.class); private Socket socket; String res; String req; /** * 默认构造函数 * @param socket */ public RpcThread(Socket socket){ this.socket = socket; } @Override public void run(){ try{ req = null; res = null; PrintWriter out = new PrintWriter(socket.getOutputStream(), true); BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); String input; out.println("================Welcome RPC Daemon=============="); while((input = in.readLine()) != null){ if ("HELP".equalsIgnoreCase(input)){ out.println("############################################## COMMANDS ###############################################"); out.println("# 1) getinfo - Gets block chain infomations. #"); out.println("# 2) send <vac> - Write <vac> to blockChain #"); out.println("#######################################################################################################"); } else { req = input; while (res == null){ TimeUnit.MILLISECONDS.sleep(25); } out.println(res); req = null; res = null; } } } catch (Exception e){ LOGGER.info("An RPC client has disconnected.",e); } } }
RpcServer.java
package com.v5ent.real.p2p; import java.net.ServerSocket; import java.util.ArrayList; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * RPC服务 * * 注意:不要把这个端口开放给外网 * @author Mignet */ public class RpcServer extends Thread { private static final Logger LOGGER = LoggerFactory.getLogger(RpcServer.class); private int port; private boolean runFlag = true; List<RpcThread> rpcThreads; /** * 默认配置 */ public RpcServer() { this.port = 8016; this.rpcThreads = new ArrayList<>(); } /** * 指定端口 * @param port Port to listen on */ public RpcServer(int port) { this.port = port; this.rpcThreads = new ArrayList<>(); } @Override public void run() { try { ServerSocket socket = new ServerSocket(port); while (runFlag) { RpcThread thread = new RpcThread(socket.accept()); rpcThreads.add(thread); thread.start(); } socket.close(); } catch (Exception e){ LOGGER.error("rpc error in port:" + port,e); } } }
跑起来
1.使用mvn的install命令打包
2.新建peers.list,把要组建网络的ip地址填入去,在本机执行jar命令,启动第一个节点。注意,这时候它会尝试连接别的节点,连接不上
3.把jar包和peers.list上传到其他机器,启动第二个节点
4.我们用cmd在本机打开新的窗口,执行nc 127.0.0.1 8016,连接到本机节点的rpc服务,输入help查看支持的命令:
5.节点1(本机)增加一个区块:在rpc命令中,我们实现了1,查看区块链;2,写入vac数据,来验证一下
我们先输入getinfo查看一下,然后send 88,看到写入成功了,再输入getinfo,果然看到了新的块在链中。
我们也可以看到节点控制台的输出中关于新增区块的信息
6.验证是不是同步了:我们看一下另一台机器
我们在这台机器上也使用nc localhost 8016来连接看看区块
7.我们再从这个结点写入一个块(send 666)
看看本机接收到了没
很完美。
到此基本演示了区块链通讯真实的样子。当然,这里有很多可以改进的地方,比如安全性,比如命令的模式,比如不用sleep而是用Future,比如使用netty等更高效更成熟的通讯框架。
如果想利用区块链来发行数字货币,那么在此基础上,还要有公私钥签名交易,交易通讯,校验,使用共识算法来选举及奖励货币等。
还有什么区块链知识是本系列没有提到的吗?有的,使用默克尔树来快速验证区块和整个链.
关于币的问题也可以问,比如UTXO模型可以讲一讲吗?如果有问题请大家留言