Golang实现区块链(三)—数据持久化(2)添加CLI交互接口


title: Golang实现区块链(三)—数据持久化(2)实现命令行查询
tags: go,blockchain,BoltDB


上篇文章我们使用BoltDb实现了对区块的读写,但是我们还是有很多的问题,例如我们每次运行,程序依然会从创世区块开始生成区块,还有我们缺少对生成的区块进行查询的功能。本文我们将完善这些问题。

改进

之前我们对Blockchain_GenesisBlokc 只简单的实现了区块的写入,现在我们重新设计它的逻辑:

  • 打开DB文件
  • 检测是否已经有区块链存在
  • 如果存在
    • 创建新区块链实例
    • 把刚建的这个区块链信息的作为最后一块区块hash塞到DB中。
  • 如果不存在
    • 创建新的创世区块
    • 存储到DB中
    • 把创世区块的hash作为末端hash
    • 创建新的区块链,把它的信息指向创世区块

判断区块是否存在

首先我们先定义一个dbExists函数来判断,存储区块的桶是否存在,如果桶存在,说明我们之前生成过区块,并且写入了桶,所有判断桶是否存在,就是判断是否存在已经生成好的区块信息。

注意: 这里判断桶,我们排除手动创建的桶,即默认只有创世区块才能生成桶。

func dbExists() bool {
	if _, err := os.Stat(dbName); os.IsNotExist(err)  {
		return false
	}
	return true
}

修改Blockchain_GenesisBlokc

接着我们对Blockchain_GenesisBlokc 进行完善

func Blockchain_GenesisBlokc() *Blockchain {
	if dbExists(){
		fmt.Println("区块已经存在。。。")
		db, err := bolt.Open(dbName, 0600, nil)
		if err != nil {
			log.Panicf("open the Dbfailed! %v\n", err)
		}
		var blockchain *Blockchain
		err =db.View(func(tx *bolt.Tx) error {
			b:=tx.Bucket([]byte(bkName))
			tip:=b.Get([]byte("l")) //这里使用的是 read-only事务的 Get 方法,从l中读取最后一块区块的编码,我们挖下一新块时会作为参数用到。
			blockchain= &Blockchain{tip,db}
			return nil
		})
		if err !=nil{
			log.Panicf("get the block from db failed! %v\n", err)
		}
	  return  blockchain
	}else{
		db, err := bolt.Open(dbName, 0600, nil)
		if err != nil {
			log.Panicf("open the Dbfailed! %v\n", err)
		}
		//defer db.Close()
		var tip []byte //存储数据库中的区块哈希

		err = db.Update(func(tx *bolt.Tx) error {
			b := tx.Bucket([]byte(bkName))
			if b == nil {
				b, err = tx.CreateBucket([]byte(bkName))
				if err != nil {
					log.Panicf("create the bucket [%s] failed! %v\n", bkName, err)
				}
			}
			if b != nil {
				genesisBlock := NewGenesisBlock()
				//存储创世区块
				err = b.Put(genesisBlock.Hash, genesisBlock.Serialize())
				if err != nil {
					log.Panicf("put the data of genesisBlock to Dbfailed! %v\n", err)
				}
				//存储最新区块链哈希
				err = b.Put([]byte("l"), genesisBlock.Hash)//在挖出新块,将其序列化存储到数据库后,把最新的区块hash值更新到 l 值中。
				if err != nil {
					log.Panicf("put the hash of latest block to Dbfailed! %v\n", err)
				}
				tip = genesisBlock.Hash
			}
			return nil
		})
		if err != nil {
			log.Panicf("update the data of genesis block failed! %v\n", err)
		}
		return &Blockchain{tip, db}
	}

}

添加迭代器

现在所有产生的区块都会被保存到数据库里面,我们想要区块能够顺序打印,BoltDB 允许对一个 bucket 里面的所有 key 进行迭代,但是所有的 key 都以字节序进行存储。当数据很大的时候,我们并不想让所有的数据都直接加载到内存中,所有我们需要一个迭代器,来一个一个读取区块。

迭代器结构

每当要对链中的块进行迭代时,就会创建一个迭代器,里面存储了当前迭代的块哈希(CurrentHash)和数据库的连接(DB)。

// 区块链迭代器结构
type BlockChainIterator struct {
	DB *bolt.DB 		// 数据库
	CurrentHash []byte	// 当前区块的哈希值
}

创建迭代器对象

在每次我们要去遍历整个区块链中的区块时会创建一个该遍历器。遍历器会保存当前遍历到的区块hash和保持与数据库的链接,后者也使得遍历器和该区块链在逻辑上是结合的,因为遍历器数据库连接用的是区块链的同一个,所以,Blockchain 会负责创建遍历器:

//  创建迭代器对象
func (blc *BlockChain) Iterator() *BlockChainIterator {
	return &BlockChainIterator{blc.DB, blc.Tip}
}

遍历

因为我们是通过Tip来遍历的,所以迭代器的是从新到旧进行获取。
BlockchainIterator 只做一件事:它负责返回区块链中的下一个区块:

func (bcit *BlockChainIterator)Next() *Block  {
	var block *Block
	err := bcit.DB.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte(blockTableName))
		if nil != b {
			// 获取指定哈希的区块数据
			currentBlockBytes := b.Get(bcit.CurrentHash)
			block = DeserializeBlock(currentBlockBytes)
			// 更新迭代器中当前区块的哈希值
			bcit.CurrentHash = block.PrevBlockHash
		}
		return nil
	})
	if nil != err {
		log.Panicf("iterator the db of blockchain failed! %v\n", err)
	}
	return block
}

命令行交互接口

到了这里区块链的持久化已经完成了,但是我们还没有一个交互接口,能让我们手动添加区块和打印区块链,现在我们就来实现它。

CLI结构

type CLI struct {
	BC *BlockChain
}

打印使用方法

我们先设计下调用命令:
createblockchain – 创建区块链.
addblock -data DATA – 交易数据
printchain – 输出区块链的信息

// 展示用法
func PrintUsage() {
	fmt.Println("Usage:")
	fmt.Printf("\tcreateblockchain -- 创建区块链.\n")
	fmt.Printf("\taddblock -data DATA -- 交易数据\n")
	fmt.Printf("\tprintchain -- 输出区块链的信息\n")
}

校验

校验,如果只输入了程序命令,就输出指令用法并且退出程序

func IsValidArgs() {
	if len(os.Args) < 2 {
		PrintUsage() // 打印用法
		os.Exit(1) // 退出程序
	}
}

接口

// 添加区块
func (cli *CLI) addBlock(data string) {
	cli.BC.AddBlock([]byte(data))
}
// 输出区块链信息
func (cli *CLI) printchain() {
	cli.BC.PrintChain()
}

// 创建区块链
func (cli *CLI) createBlockchainWithGenesis() {
	CreateBlockChainWithGenesisBlock()
}

调用

使用flag 获取命令行输出参数,然后通过switch来判断参数内容执行相应的函数。

func (cli *CLI) Run() {
	// 1. 检测参数数量
	IsValidArgs()
	// 2. 新建命令
	addBlockCmd := flag.NewFlagSet("addblock", flag.ExitOnError)
	printChainCmd := flag.NewFlagSet("printchain", flag.ExitOnError)
	createBlCWithGenesisCmd := flag.NewFlagSet("createblockchain", flag.ExitOnError)
	// 3. 获取命令行参数
	flagAddBlockArg:=addBlockCmd.String("data","send 100 BTC to everyone","交易数据")
	switch os.Args[1] {
	case "addblock":
		err := addBlockCmd.Parse(os.Args[2:])
		if nil != err {
			log.Panicf("parse cmd of add block failed! %v\n", err)
		}
	case "printchain":
		err := printChainCmd.Parse(os.Args[2:])
		if nil != err {
			log.Panicf("parse cmd of printchain failed! %v\n", err)
		}
	case "createblockchain":
		err := createBlCWithGenesisCmd.Parse(os.Args[2:])
		if nil != err {
			log.Panicf("parse cmd of create block chain failed! %v\n", err)
		}
	default:
		PrintUsage()
		os.Exit(1)
	}
	// 添加区块命令
	if addBlockCmd.Parsed() {
		if *flagAddBlockArg == "" {
			PrintUsage()
			os.Exit(1)
		}
		cli.addBlock(*flagAddBlockArg)
	}

	// 输出区块链信息命令
	if printChainCmd.Parsed() {
		cli.printchain()
	}
	// 创建区块链
	if createBlCWithGenesisCmd.Parsed() {
		cli.createBlockchainWithGenesis()
	}
}

运行

在运行前我们首先要修改下main函数

func main() {
	blockChain := BLC.Blockchain_GenesisBlokc()
	defer blockChain.Db.Close();
	//blockChain.AddBlock("Send 100 btc to troy")
	//blockChain.AddBlock("Send 50 btc to Alice")
	//blockChain.AddBlock("Send 20 btc to Bob")
	BLC.PrintUsage()
	cli := BLC.CLI{blockChain}
	cli.Run()
}

运行结果

我们打开终端,进入项目所在文件夹,输入go build ./ 构建项目,得到执行文件。
不输入参数直接执行文件输出:

区块已经存在。。。
Usage:
        createblockchain -- 创建区块链.
        addblock -data DATA -- 交易数据
        printchain -- 输出区块链的信息

因为我们前面已经往数据库中添加过区块,所有本文直接演示打印区块链。在终端输入 ./Blockchain03 - printblockchain (Blockchain03是本文项目的名称) 输出:

区块已经存在。。。
Usage:
        createblcwithgenesis -- 创建区块链.
        addblock -data DATA -- 交易数据
        printblockchain -- 输出区块链的信息
——————————————打印区块链———————————————————————
----------------------------------------
        Heigth : 3
        TimeStamp : 1539140742
        PrevBlockHash : 00000c20760b2347db31e5caffa6ca26869af13d2eba487e3be3dc593cad9265
        Hash : 000002f3549162393dd88ccd0b550b039b6faa7af13f2db7a27442d36708253e
        Data : Send 20 btc to Bob
        Nonce : 569774
----------------------------------------
        Heigth : 2
        TimeStamp : 1539140728
        PrevBlockHash : 000003b8099976553fe07fc59421072b19a99b0bbdc25032d3b161420a32c840
        Hash : 00000c20760b2347db31e5caffa6ca26869af13d2eba487e3be3dc593cad9265
        Data : Send 50 btc to Alice
        Nonce : 2809923
----------------------------------------
        Heigth : 1
        TimeStamp : 1539140726
        PrevBlockHash : 000006c70de89f4c92f0fe5c57f96a7b65882096d4fe50c6ad737f8feb485661
        Hash : 000003b8099976553fe07fc59421072b19a99b0bbdc25032d3b161420a32c840
        Data : Send 100 btc to troy
        Nonce : 369529
----------------------------------------
        Heigth : 0
        TimeStamp : 1539089785
        PrevBlockHash : 
        Hash : 000006c70de89f4c92f0fe5c57f96a7b65882096d4fe50c6ad737f8feb485661
        Data : first block
        Nonce : 523303

总结

好了我们终于实现了区块的持久化,还有完善了遍历信息来支持按序打印所有的区块,已经交互命令,下一章我们将了解到交易的实现。

你可能感兴趣的:(go,#,goland实现区块链)