技术:jdk1.8 + maven3.0.5
http://qklbishe.com/
概述
区块链是分布式数据存储、点对点传输、共识机制、加密算法等计算机技术的新型应用模式。所谓共识机制是区块链系统中实现不同节点之间建立信任、获取权益的数学算法 。
详细
本demo旨在帮助你了解如何开发区块链技术。
创建一个简单的钱包。
使用我们的区块链发送带签名的交易。
一、运行效果
二、实现过程
①、准备一个钱包
让我们创建一个钱包类来保存公钥和私钥:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
package com.zxh;
import java.security.*;
import java.security.spec.ECGenParameterSpec;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
public class Wallet {
public PrivateKey privateKey;
public PublicKey publicKey;
public HashMap<String,TransactionOutput> UTXOs = new HashMap<String,TransactionOutput>();
public Wallet() {
generateKeyPair();
}
public void generateKeyPair() {
try {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance( "ECDSA" , "BC" );
SecureRandom random = SecureRandom.getInstance( "SHA1PRNG" );
ECGenParameterSpec ecSpec = new ECGenParameterSpec( "prime192v1" );
// Initialize the key generator and generate a KeyPair
keyGen.initialize(ecSpec, random); //256
KeyPair keyPair = keyGen.generateKeyPair();
// Set the public and private keys from the keyPair
privateKey = keyPair.getPrivate();
publicKey = keyPair.getPublic();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
|
公钥和私钥究竟是起到什么作用呢,其实公钥的作用就是地址,你可以分享你的公钥给别人以此来获取付款,而你的私钥的作用是为了对交易进行签名,这样其他人就不可以花费你的金额除非它拥有你的私钥,所以对于每个人而言我们必须保护好我们的私钥,不能透露我们的私钥信息给其他人。同时在我们进行交易的时候我们也会同时发送我们的公钥由此来验证我们的签名是有效的而且没有数据被篡改。
我们在密钥对KeyPair
生成私有和公钥。我们将使用椭圆曲线加密来生成我们的密钥对KeyPair
。让我们将generateKeyPair()
方法添加到我们的钱包类中,并在构造函数中调用它:
私钥用于签署我们不想被篡改的数据。公钥用于验证签名。
②、交易和数字签名
每笔交易将携带一定以下信息:
资金付款人的公匙信息。
资金收款人的公匙信息。
被转移资金的金额。
输入,它是对以前的交易的引用,证明发送者有资金发送。
输出,显示交易中收款方相关地址数量。(这些输出被引用为新交易的输入)
一个加密签名,证明该交易是由地址的发送者是发送的,并且数据没有被更改。(阻止第三方机构更改发送的数量)
让我们创建这个新的交易类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
import java.security.*; import java.util.ArrayList; public class Transaction {
public String transactionId; // this is also the hash of the transaction.
public PublicKey sender; // senders address/public key.
public PublicKey reciepient; // Recipients address/public key.
public float value;
public byte [] signature; // this is to prevent anybody else from spending funds in our wallet.
public ArrayList<TransactionInput> inputs = new ArrayList<TransactionInput>();
public ArrayList<TransactionOutput> outputs = new ArrayList<TransactionOutput>();
private static int sequence = 0 ; // a rough count of how many transactions have been generated.
// Constructor:
public Transaction(PublicKey from, PublicKey to, float value, ArrayList<TransactionInput> inputs) {
this .sender = from;
this .reciepient = to;
this .value = value;
this .inputs = inputs;
}
// This Calculates the transaction hash (which will be used as its Id)
private String calulateHash() {
sequence++; //increase the sequence to avoid 2 identical transactions having the same hash
return StringUtil.applySha256(
StringUtil.getStringFromKey(sender) +
StringUtil.getStringFromKey(reciepient) +
Float.toString(value) + sequence
);
}}
|
我们还应该创建空的transactioninput和transactionoutput类,不要担心我们可以在后面填写它们。 我们的交易类还将包含生成/验证签名和验证交易的相关方法,那么签名的意图是什么?签名在我们的区块链中执行了两个非常重要的任务:第一,签名用来保证只有货币的拥有者才可以用来发送自己的货币,第二,签名用来阻止其他人试图篡改提交的交易。
即私钥被用来签名数据,而公钥用来验证其完整性。
举个例子:张三想要发送2个加密货币给李四,他们用各自的钱包创建了交易,并提交到全网的区块链中作为一个新的区块,一个挖矿者试图篡改接受者把2个加密货币给小明,但是幸运的事,张三在交易数据中已经用私钥进行了签名,这就允许了全网中的任何节点使用张三的公匙进行验证数据是否已经被篡改(因为没有其他人的公钥可以用来验证小明发出的这笔交易)。
从前面的代码中我们可以看到我们的签名将是一堆字符串,所以让我们创建一个方法来生成它们。首先我们在StringUtil
类中创建产生签名的方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
public static byte [] applyECDSASig(PrivateKey privateKey, String input) {
Signature dsa;
byte [] output = new byte [ 0 ];
try {
dsa = Signature.getInstance( "ECDSA" , "BC" );
dsa.initSign(privateKey);
byte [] strByte = input.getBytes();
dsa.update(strByte);
byte [] realSig = dsa.sign();
output = realSig;
} catch (Exception e) {
throw new RuntimeException(e);
}
return output;
}
//Verifies a String signature
public static boolean verifyECDSASig(PublicKey publicKey, String data, byte [] signature) {
try {
Signature ecdsaVerify = Signature.getInstance( "ECDSA" , "BC" );
ecdsaVerify.initVerify(publicKey);
ecdsaVerify.update(data.getBytes());
return ecdsaVerify.verify(signature);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static String getStringFromKey(Key key) {
return Base64.getEncoder().encodeToString(key.getEncoded());
}
|
不用担心你不能理解这些方法的内容,你所需要知道的就是applyECDSASig
方法的输入参数为付款人的私钥和需要加密的数据信息,签名后返回字节数组。而verifyECDSASig
方法的输入参数为公钥、加密的数据和签名,调用该方法后返回true或false来说明签名是否是有效的。getStringFromKey
返回任何key
的编码字符串
让我们使用签名的方法在交易类中,增加generatesiganature()
和 varifiysignature()
方法
1
2
3
4
5
6
7
|
//Signs all the data we dont wish to be tampered with.public void generateSignature(PrivateKey privateKey) {
String data = StringUtil.getStringFromKey(sender) + StringUtil.getStringFromKey(reciepient) + Float.toString(value) ;
signature = StringUtil.applyECDSASig(privateKey,data); }
//Verifies the data we signed hasnt been tampered withpublic boolean verifiySignature() {
String data = StringUtil.getStringFromKey(sender) + StringUtil.getStringFromKey(reciepient) + Float.toString(value) ;
return StringUtil.verifyECDSASig(sender, data, signature);}
|
在现实中,您可能希望签名更多信息,例如使用的输出/输入和/或时间戳(现在我们只是签名最低限度的信息)签名将由矿工验证,只有签名验证成功后交易才能被添加到区块中去。
当我们检查区块链的有效性时,我们也可以检查签名
③、测试钱包和签名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
import java.security.Security; import java.util.ArrayList; import java.util.Base64; import com.google.gson.GsonBuilder; public class NoobChain {
public static ArrayList<Block> blockchain = new ArrayList<Block>();
public static int difficulty = 5 ;
public static Wallet walletA;
public static Wallet walletB;
public static void main(String[] args) {
//Setup Bouncey castle as a Security Provider
Security.addProvider( new org.bouncycastle.jce.provider.BouncyCastleProvider());
//Create the new wallets
walletA = new Wallet();
walletB = new Wallet();
//Test public and private keys
System.out.println( "Private and public keys:" );
System.out.println(StringUtil.getStringFromKey(walletA.privateKey));
System.out.println(StringUtil.getStringFromKey(walletA.publicKey));
//Create a test transaction from WalletA to walletB
Transaction transaction = new Transaction(walletA.publicKey, walletB.publicKey, 5 , null );
transaction.generateSignature(walletA.privateKey);
//Verify the signature works and verify it from the public key
System.out.println( "Is signature verified" );
System.out.println(transaction.verifiySignature());
}
|
确定要添加bounceycastle
依赖
我们创建了两个钱包walleta和walletb,然后打印了walleta的私钥和公钥。生成一个交易并使用walleta的私钥对其进行签名。
打印
1
|
Connected to the target VM, address: '127.0.0.1:55757' , transport: 'socket' Private and public keys:MHsCAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQEEYTBfAgEBBBiJzZiesBnBWwwB+uog+fJX84mx4lPUTUagCgYIKoZIzj0DAQGhNAMyAAQfPzad0zqBUGQAany2rRZE1+2ml5IOCZST8LywjBQT8ux+3UPVbr2u0+LaExxz1WI=MEkwEwYHKoZIzj0CAQYIKoZIzj0DAQEDMgAEHz82ndM6gVBkAGp8tq0WRNftppeSDgmUk /C8sIwUE/Lsft1D1W69rtPi2hMcc9ViIs signature verifiedtrue
|
接下来我们将创建并验证输入和输出,并把交易保存到区块链中去。
输入和输出 1:如何验证货币是你的
如果你拥有1比特币,你必须前面就得接收1比特币。比特币的账本不会在你的账户中增加一个比特币也不会从发送者那里减去一个比特币,发送者只能指向他/她之前收到过一个比特币,所以一个交易输出被创建用来显示一个比特币发送给你的地址(交易的输入指向前一个交易的输出)。
你的钱包余额是所有未使用的交易输出的总和
从这一个点出发,我们会依照比特币中的说明,把所有未使用的交易输出称为UTXO.
创建TransactionInput
类:
1
2
3
4
5
6
7
|
public class TransactionInput {
public String transactionOutputId; //Reference to TransactionOutputs -> transactionId
public TransactionOutput UTXO; //Contains the Unspent transaction output
public TransactionInput(String transactionOutputId) {
this .transactionOutputId = transactionOutputId;
}}
|
这个类将用于引用尚未使用的transactionoutput
。transactionOutputId
将用于查找相关的TransactionOutput
,允许矿工检查您的所有权。
创建TransactionOutputs
类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
import java.security.PublicKey; public class TransactionOutput {
public String id;
public PublicKey reciepient; //also known as the new owner of these coins.
public float value; //the amount of coins they own
public String parentTransactionId; //the id of the transaction this output was created in
//Constructor
public TransactionOutput(PublicKey reciepient, float value, String parentTransactionId) {
this .reciepient = reciepient;
this .value = value;
this .parentTransactionId = parentTransactionId;
this .id = StringUtil.applySha256(StringUtil.getStringFromKey(reciepient)+Float.toString(value)+parentTransactionId);
}
//Check if coin belongs to you
public boolean isMine(PublicKey publicKey) {
return (publicKey == reciepient);
}
}
|
交易输出类将显示从交易中发送给每一方的最终金额。这些作为新交易中的输入参考,作为证明你可以发送的金额数量。
输入和输出 2:处理交易
区块链可能会收到很多交易,而区块链可能会非常长,因为我们必须查找并检查其输入,所以可能需要非常长的时间来处理新的交易。为了解决这个问题,我们保存了一个额外的集合称之为为使用的交易作为可用的输入,所以在主函数中增加一个集合称为UTXO。
1
2
3
4
5
6
7
8
9
|
public class NoobChain {
public static ArrayList<Block> blockchain = new ArrayList<Block>();
public static HashMap<String,TransactionOutputs> UTXOs = new HashMap<String,TransactionOutputs>(); //list of all unspent transactions.
public static int difficulty = 5 ;
public static Wallet walletA;
public static Wallet walletB;
public static void main(String[] args) {
|
hashmap
允许我们使用键来查找值,但是您需要导入java.util.hashmap
重点来了,我们在交易类中增加一个processTransaction
方法,这个方法是把一切放在一起用来处理交易。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
//Returns true if new transaction could be created. public boolean processTransaction() {
if (verifiySignature() == false ) {
System.out.println( "#Transaction Signature failed to verify" );
return false ;
}
//gather transaction inputs (Make sure they are unspent):
for (TransactionInput i : inputs) {
i.UTXO = NoobChain.UTXOs.get(i.transactionOutputId);
}
//check if transaction is valid:
if (getInputsValue() < NoobChain.minimumTransaction) {
System.out.println( "#Transaction Inputs to small: " + getInputsValue());
return false ;
}
//generate transaction outputs:
float leftOver = getInputsValue() - value; //get value of inputs then the left over change:
transactionId = calulateHash();
outputs.add( new TransactionOutput( this .reciepient, value,transactionId)); //send value to recipient
outputs.add( new TransactionOutput( this .sender, leftOver,transactionId)); //send the left over 'change' back to sender
//add outputs to Unspent list
for (TransactionOutput o : outputs) {
NoobChain.UTXOs.put(o.id , o);
}
//remove transaction inputs from UTXO lists as spent:
for (TransactionInput i : inputs) {
if (i.UTXO == null ) continue ; //if Transaction can't be found skip it
NoobChain.UTXOs.remove(i.UTXO.id);
}
return true ;
}
//returns sum of inputs(UTXOs) values
public float getInputsValue() {
float total = 0 ;
for (TransactionInput i : inputs) {
if (i.UTXO == null ) continue ; //if Transaction can't be found skip it
total += i.UTXO.value;
}
return total;
} //returns sum of outputs:
public float getOutputsValue() {
float total = 0 ;
for (TransactionOutput o : outputs) {
total += o.value;
}
return total;
}
|
我们还添加了getinputsvalue
方法
通过这种方法,我们执行一些检查以确保交易有效,然后收集输入并生成输出。最重要的是,最后,我们抛弃了输入在我们的UTXO列表,这就意味着一个可以使用的交易输出必须之前一定是输入,所以输入的值必须被完全使用,所以付款人必须改变它们自身的金额状态。
红色箭头是输出。请注意,绿色输入是对之前输出的参考
最后让我们修改我们的wallet类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
import java.security.*; import java.security.spec.ECGenParameterSpec; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; public class Wallet {
public PrivateKey privateKey;
public PublicKey publicKey;
public HashMap<String,TransactionOutput> UTXOs = new HashMap<String,TransactionOutput>(); //only UTXOs owned by this wallet.
public Wallet() {...
public void generateKeyPair() {...
//returns balance and stores the UTXO's owned by this wallet in this.UTXOs
public float getBalance() {
float total = 0 ;
for (Map.Entry<String, TransactionOutput> item: NoobChain.UTXOs.entrySet()){
TransactionOutput UTXO = item.getValue();
if (UTXO.isMine(publicKey)) { //if output belongs to me ( if coins belong to me )
UTXOs.put(UTXO.id,UTXO); //add it to our list of unspent transactions.
total += UTXO.value ;
}
}
return total;
}
//Generates and returns a new transaction from this wallet.
public Transaction sendFunds(PublicKey _recipient, float value ) {
if (getBalance() < value) { //gather balance and check funds.
System.out.println( "#Not Enough funds to send transaction. Transaction Discarded." );
return null ;
}
//create array list of inputs
ArrayList<TransactionInput> inputs = new ArrayList<TransactionInput>();
float total = 0 ;
for (Map.Entry<String, TransactionOutput> item: UTXOs.entrySet()){
TransactionOutput UTXO = item.getValue();
total += UTXO.value;
inputs.add( new TransactionInput(UTXO.id));
if (total > value) break ;
}
Transaction newTransaction = new Transaction(publicKey, _recipient , value, inputs);
newTransaction.generateSignature(privateKey);
for (TransactionInput input: inputs){
UTXOs.remove(input.transactionOutputId);
}
return newTransaction;
}
}
|
你可以随时为钱包添加一些其他功能,例如记录您的交易历史记录。
④、添加交易到区块中
现在我们已经有了一个有效的交易系统,我们需要把交易加入到我们的区块链中。我们把交易列表替换我们块中无用的数据,但是在一个单一的区块中可能存放了1000个交易,这就会导致大量的hash计算,不用担心在这里我们使用了交易的merkle root
,稍后你会看到。让我们增加一个帮助方法来创建merkle root
在StringUtils
类中。
在StringUtils
类中创建getMerkleRoot
方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
//Tacks in array of transactions and returns a merkle root.public static String getMerkleRoot(ArrayList<Transaction> transactions) {
int count = transactions.size();
ArrayList<String> previousTreeLayer = new ArrayList<String>();
for (Transaction transaction : transactions) {
previousTreeLayer.add(transaction.transactionId);
}
ArrayList<String> treeLayer = previousTreeLayer;
while (count > 1 ) {
treeLayer = new ArrayList<String>();
for ( int i= 1 ; i < previousTreeLayer.size(); i++) {
treeLayer.add(applySha256(previousTreeLayer.get(i- 1 ) + previousTreeLayer.get(i)));
}
count = treeLayer.size();
previousTreeLayer = treeLayer;
}
String merkleRoot = (treeLayer.size() == 1 ) ? treeLayer.get( 0 ) : "" ;
return merkleRoot;
}
|
修来Block
类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
import java.util.ArrayList; import java.util.Date; public class Block {
public String hash;
public String previousHash;
public String merkleRoot;
public ArrayList<Transaction> transactions = new ArrayList<Transaction>(); //our data will be a simple message.
public long timeStamp; //as number of milliseconds since 1/1/1970.
public int nonce;
//Block Constructor.
public Block(String previousHash ) {
this .previousHash = previousHash;
this .timeStamp = new Date().getTime();
this .hash = calculateHash(); //Making sure we do this after we set the other values.
}
//Calculate new hash based on blocks contents
public String calculateHash() {
String calculatedhash = StringUtil.applySha256(
previousHash +
Long.toString(timeStamp) +
Integer.toString(nonce) +
merkleRoot
);
return calculatedhash;
}
//Increases nonce value until hash target is reached.
public void mineBlock( int difficulty) {
merkleRoot = StringUtil.getMerkleRoot(transactions);
String target = StringUtil.getDificultyString(difficulty); //Create a string with difficulty * "0"
while (!hash.substring( 0 , difficulty).equals(target)) {
nonce ++;
hash = calculateHash();
}
System.out.println( "Block Mined!!! : " + hash);
}
//Add transactions to this block
public boolean addTransaction(Transaction transaction) {
//process transaction and check if valid, unless block is genesis block then ignore.
if (transaction == null ) return false ;
if ((previousHash != "0" )) {
if ((transaction.processTransaction() != true )) {
System.out.println( "Transaction failed to process. Discarded." );
return false ;
}
}
transactions.add(transaction);
System.out.println( "Transaction Successfully added to Block" );
return true ;
}
}
|
需要注意的是我们还更新了Block
构造函数,因为我们不再需要传递字符串数据,并将merkle root
包含在计算哈希方法中。addTransaction
方法用来增加交易,只有满足条件下才可以成功的在区块中增加交易。
我们已经实现了一个可交易的区块链。
三、项目结构图
四、补充
本demo实现了可交易的区块链
允许用户创建钱包
使用椭圆曲线加密方式为钱包提供公钥和私钥
通过使用数字签名算法证明所有权,确保资金转移
允许用户在区块链上进行交易
java实现简单区块链毕业设计相关推荐
- 语言毕业设计选题及源代码_区块链毕业设计论文「11」
区块链作为一种崭新的.颠覆性的技术,是国内外活跃的研究领域和毕业设计选题方向. 本文列出最新的一组区块链方面的论文,希望可以对选择区块链毕业设计的同学们有所帮助, 这是汇智网编辑整理的区块链毕业设计论 ...
- 基于Java语言构建区块链(五)—— 地址(钱包)
基于Java语言构建区块链(五)-- 地址(钱包) 2018年03月25日 18:02:06 wangwei_hz 阅读数:1292更多 个人分类: 区块链bitcoin比特币 文章的主要思想和内容均 ...
- 基于Java语言构建区块链(六)—— 交易(Merkle Tree)
基于Java语言构建区块链(六)-- 交易(Merkle Tree) 2018年04月16日 10:21:35 wangwei_hz 阅读数:480更多 个人分类: 区块链比特币bitcoin 最终内 ...
- vtk删除一个actor_如何构建一个基于actor的简单区块链
vtk删除一个actor Scalachain is a blockchain built using the Scala programming language and the actor mod ...
- 基于Java语言构建区块链(四)—— 交易(UTXO)
基于Java语言构建区块链(四)-- 交易(UTXO) 2018年03月11日 00:48:01 wangwei_hz 阅读数:909 标签: 区块链比特币 更多 个人分类: 区块链 文章的主要思想和 ...
- python模拟一个简单的取款机,python简单区块链模拟详解
最近学习了一点python,那就试着做一做简单的编程练习. 首先是这个编程的指导图,如下: 对的,类似一个简单区块链的模拟. 代码如下: class DaDaBlockCoin: #index 索引, ...
- JAVA从零开始开发区块链技术
课程目录 (1)\第一课:目录中文件数:26个 ├─(1) Java区块链开发(一).01.mp4 ├─(2) Java区块链开发(一).02.mp4 ├─(3) Java区块链开发(一).03.mp ...
- 基于Java的Android区块链开发之生成助记词(位数可选)
基于Java的Android区块链开发之生成助记词 位数可选 具体实现代码 这里使用bitcoinj库,来实现生成bip39的12个助记词,引用库 implementation 'org.bitcoi ...
- 【区块链】Go 实现简单区块链
本文主要利用 Go 语言对区块链模型进行了简单的实现,通过 GoLand 创建链式结构和一个简单的 http server,对外暴露读写接口,运行 rpc 并以地址访问形式向区块链发送数据和读取数据. ...
最新文章
- 作为一名合格的JAVA程序员需要点亮那些技能树
- 图灵——2015技术类新书TOP20
- 深度学习(DL)与卷积神经网络(CNN)学习笔记随笔-01-CNN基础知识点
- C++17中那些值得关注的特性(上)
- deprecated_@Deprecated新外观可能是什么?
- windows下集成maven+eclipse开发环境二:集成maven到eclipse,并使用nexus作为maven仓库...
- 要怎么样做一个合格点的程序猿呢?
- 微信小程序多图上传带进度提示的代码实例
- Unity3D研究院之获取摄像机的视口区域(转)
- C++ 檔案、資料夾、路徑處理函式庫:boost::filesystem
- 前台传JSON到后台
- BiLSTM-CRF模型中的CRF层讲解
- php 读xml的两种方式
- 117. php empty()函数坑
- 链表常见操作java实现二:反转链表,从尾到头倒序遍历
- 微信小程序上传图片到阿里云存储
- 软件工程之软件质量管理
- 期货手续费怎么计算?
- 如何画神经网络模型图
- 数据结构——简单实现森林转化为二叉树
热门文章
- 【文献阅读】Silhouette based View embeddings for Gait Recognit
- @我的生活必备软件:大集中(强烈推荐)
- DRV8814PWPR
- Python word_cloud 样例 标签云系列(三)
- 【C语言】探究整型数据在内存中的存储
- 仿网易云音乐源码html5
- 宽带信号的DOA估计学习笔记(一)
- INS-06101nbsp;IPnbsp;ADRESSnbsp;OFnbsp;LOCALHOST
- Flink系列之Flink集群搭建
- 二叉树的前序中序后序遍历图示