PHP从零实现区块链(网页版三)数据持久化与CLI

本文介绍了如何使用PHP和Laravel框架实现一个简单的区块链系统,包括数据持久化、路由设置、Cache::put的使用以及迭代器在遍历区块链中的应用。作者还展示了如何在CLI环境中操作并演示了命令行接口的表单提交功能。
摘要由CSDN通过智能技术生成

源码地址:PHP从零实现区块链(三)数据持久化与CLI - 简书

注:本例只是从网页版实现一下原理,源码非本人所写,只是将原帖的源码更改了一下,变成网页版

一些准备:

因为下面的例子用到了Cache::put之类的方法,往缓存文件写入数据,这个是在laravel框架下使用的,所以我们得先安装laraver框架。

关于怎么安装laravel框架,可以参考这篇:ubuntu下安装laravel框架并调用config()-CSDN博客

安装好laravel框架后,建好了一个名为mylaravel6 的laravel框架项目。

我们为这个项目添加一个路由控制器,用来转到我们的php程序。

首先在mylaravel6/app/Http/Controllers目录下添加一个AppController控制器类。

新建AppController.php文件,代码如下:

<?php
namespace App\Http\Controllers;
class AppController extends Controller
{

public function app(){
 echo("hello app");
 }
 
}

接着我们,进入mylaravel6/routes文件夹,打开web.php,添加一个路由,指向我们的类中的app方法,添加如下代码:

use App\Http\Controllers\AppController;
Route::get('/app', [AppController::class, 'app']);

好,接着我们输入localhost:8000/app,测试访问一下:

OK,正常工作(添加路由你们要注意一下自己的laravel版本,不同的版本方法不一样)。

例子迁移:(因为只是为了运行例子,能跑起来就好,我们尽量简单就行,就不考虑太多了)

直接把我们的例子复制过来,就是之前app.php,block.php blockchain等几个文件。

复制到Controllers目录下,跟之前的AppController.php同样目录。

然后进行更改,将app.php里的代码,写在appcontroller.php里的app函数中,app.php就弃用了。

AppController.php代码如下:

<?php
namespace App\Http\Controllers;
class AppController extends Controller
{

public function app(){

$time1 = time();
$bc = BlockChain::NewGenesisBlock();
$bc->addBlock('i am 2 block');
$bc->addBlock('i am 3 block');
$time2 = time();

$spend = $time2 - $time1;
foreach ($bc->blocks as $block){
    print_r($block);
    echo('<hr>');
 }
echo('花费时间(s):'.$spend);


 }
 
}

然后还要做一些更改,就是把之前require_once 'block.php';之类的全部改用namespace App\Http\Controllers;代替,不然会有冲突。

没有require_one的.php文件,最前面也要加上namespace App\Http\Controllers;统一命名,不然识别不到。

还有将block.php改为Block.php,大小写严格。

接着运行一下(我将难度调为了20)

OK,运行正常,成功转移。

接下来我们测试一下Cache::put的使用

在config目录一下添加一个cache.php文件(此文件如果框架自带,把原来的改为cache1.php)

添加代码如下:

<?php
return [
    'default' => 'file',
    'stores' => [
        'file' => [
            'driver' => 'file',
            'path' => storage_path(),
        ],
    ],
    'prefix' => 'bc_'
];

上面配置的意思是指明缓存存储为file类型,意思就是以文本文件存储。

然后存储路径就是storage_path(),(这个函数是获得storage的路径,可以用echo输出查看)

当然你也可以显式指定路径。

接着我们在AppController的app()方法中调用Cache::put测试一下,如下代码:

 Cache::put('name', 'zhengyong');

  echo storage_path(); 

  $name = Cache::get('name');
  echo '<br>';
  echo $name;

(注意开头引用一下Cache,添加use Cache;不然识别不到Cache类)

运行结果如下:

可以看到缓存文件是存储在sorage目录下的,调用cache::put后,在这个目录下就生成一个

6a/e9目录,然后生成了一串字符命名的文本文件,里面就是存储有我们键值的数据,如下:

好,一切都准备好了,接下来我们来正式研究代码.

将BlockChain.php更改代码如下:

<?php
namespace App\Http\Controllers;
use Cache;
class BlockChain implements \Iterator
{
 /**
     * // 存放最后一个块的hash
     * @var string $tips
     */
    public $tips;

    public function __construct(string $tips)
    {
        $this->tips = $tips;
    }

    // 加入一个块到区块链中
    public function addBlock(string $data)
    {
        // 获取最后一个块
        $prevBlock = unserialize(Cache::get($this->tips));

        $newBlock = new Block($data, $prevBlock->hash);

        // 存入最后一个块到数据库,并更新 l 和 tips
        Cache::put($newBlock->hash, serialize($newBlock));
        Cache::put('l', $newBlock->hash);
        $this->tips = $newBlock->hash;
    }

    // 新建区块链
    public static function NewBlockChain(): BlockChain
    {
        if (Cache::has('l')) {
            // 存在区块链
            $tips = Cache::get('l');
        } else {
            $genesis = Block::NewGenesisBlock();

            Cache::put($genesis->hash, serialize($genesis));

            Cache::put('l', $genesis->hash);

            $tips = $genesis->hash;
        }
        return new BlockChain($tips);
    }
}

代码解释:

因为现在块都存储在缓存文件里了,所以BlockChain类里不需要block[]数组来存储了。

自然它的构造函数也不需要传block了,而是传入最后一个块的哈希值

   public function __construct(string $tips)
    {
        $this->tips = $tips;
    }

然后赋给类新定义的变量tips。

那么这个哈希值是怎么传的呢?

首先我们得调用这句:

$bc = BlockChain::NewBlockChain();

在这函数里面如果缓存文件已经有块了(Cache::has('l')判断),那就通过cache:;get读取到最后一个块的哈希值。

如果没有,那就这样得到最后一个块的哈希值。

调用block块的创世块函数创建一个$genesis = Block::NewGenesisBlock();

(注意NewGenesisBlock已经转到Block类里去了,不在BlockChain里了,当然你也可以写到BlockChain,我们这里还是按源码来)

然后把这个创世块写入到缓存文件,它的键值对格式是这样,键就是哈希值,块就是序列化的后的block对象。

然后再创建一种键值对,键就是固定的l(注意这不是数字1,是last第一个字母),而值就是保存最后一个块的哈希值:

Cache::put('l', $genesis->hash);

最后得到最后一个块的哈希值后,就调用BlockChain的构造函数,把哈希值传过去。

return new BlockChain($tips);

这就是BlockChain类里的NewBlockChain函数做的事。

接下来说说addBlock是怎么工作的:

通过最后一个块的哈希值(tips),读取最后一个块数据(反序列化)。得到最后一个块的block对象后,我们就得到最后一个块的哈希值了(就这里而言我觉得直接取tips也是可以的)。

然后调用block的构造函数,new block创建一个新区块,传入数据和上一个区块的哈希值。

接着就把新的区块添加到缓存文件里,然后更新一下l键值,和tips。因为有新的区块,缓存文件和blockchain对象的最后一个哈希值当然得是新区块的了。

Block.php

因为在blockchain调用了$genesis = Block::NewGenesisBlock();创建创世区块

所以我们得在block类中写一个NewGenesisBlock,添加如下代码:

  public static function NewGenesisBlock()
    {
        return $block = new Block('Genesis Block', '');
    }

好,上面已经完成了写入缓存部分。

我们怎么读取呢?通过Cache::get('l'),得到块最后一个哈希值,然后再通过哈希值获得块。

接着再通过这个块里存储的上一个哈希值,获得上一个块。

遍历逻辑就是这样的。

例子中是用了迭代器实现的,你也可以用for循环语句直接读取。

关于迭代器,你可以看作是重写了foreach,按你的方法遍历,只不过只对你重写的类对象有效。

它里面有这几个重写方法:

rewind()

current()

key()

next()

valid()

调用foreach,然后具体怎么执行,按什么顺序,这里我就不详细解释了。

网上有一个很好的例子,如下:

class MyIterator implements Iterator
{
     private $var = array();

     public function __construct($array)
     {
         if (is_array($array)) {
            $this->var = $array;
         }
     }

     public function rewind() {
         echo "倒回第一个元素\n";
        reset($this->var);
     }

     public function current() {
        $var = current($this->var);
         echo "当前元素: $var\n";
         return $var;
     }

     public function key() {
        $var = key($this->var);
         echo "当前元素的键: $var\n";
         return $var;
     }

     public function next() {
        $var = next($this->var);
         echo "移向下一个元素: $var\n";
         return $var;
     }

     public function valid() {
        $var = $this->current() !== false;
         echo "检查有效性: {$var}\n";
         return $var;
     }
}

$values = array(1,2,3);
$it = new MyIterator($values);

foreach ($it as $k => $v) {
     print "此时键值对 -- key $k: value $v\n\n";
}

运行结果:

倒回第一个元素
当前元素: 1
检查有效性: 1
当前元素: 1
当前元素的键: 0
此时键值对 -- key 0: value 1

移向下一个元素: 2
当前元素: 2
检查有效性: 1
当前元素: 2
当前元素的键: 1
此时键值对 -- key 1: value 2

移向下一个元素: 3
当前元素: 3
检查有效性: 1
当前元素: 3
当前元素的键: 2
此时键值对 -- key 2: value 3

移向下一个元素: 
当前元素: 
检查有效性:

你们可以自行研究这个例子分析。

好了,回到正文,我们迭代器是这样,在BlockChain类加再增加如下代码:

class BlockChain implements \Iterator
{
    // ......

    /**
     * 迭代器指向的当前块Hash
     * @var string $iteratorHash
     */
    private $iteratorHash;

    /**
     * 迭代器指向的当前块Hash
     * @var Block $iteratorBlock
     */
    private $iteratorBlock;

    /**
     * @inheritDoc
     */
    public function current()
    {
        return $this->iteratorBlock = unserialize(Cache::get($this->iteratorHash));
    }

    /**
     * @inheritDoc
     */
    public function next()
    {
        return $this->iteratorHash = $this->iteratorBlock->prevBlockHash;
    }

    /**
     * @inheritDoc
     */
    public function key()
    {
        return $this->iteratorHash;
    }

    /**
     * @inheritDoc
     */
    public function valid()
    {
        return $this->iteratorHash != '';
    }

    /**
     * @inheritDoc
     */
    public function rewind()
    {
        $this->iteratorHash = $this->tips;
    }
}

注意是在BlockChain类里面,不是BlockChain文件里。

让class BlockChain implements \Iterator  继承迭代器。

代码解释:(这里只讲一些关键的地方吧)

这里面有两个成员变量:

  private $iteratorHash;//这个是当前块的哈希值
  private $iteratorBlock;//这个是当前块的block对象

遍历开始时,会调用rewind函数。

我们将tips赋值给iteratorHash,迭代器就得到了最后一个块的哈希值了。

然后在current()函数中,通过哈希值反序列化,就得到最后一个块的对象。

return $this->iteratorBlock = unserialize(Cache::get($this->iteratorHash));

接着在next()函数中。

将当前块的的前一个哈希值赋给iteratorHash。然后继续current()读取。

大概就是这样一个循环。

注意我们的读取,是从下往上读的,一直读到创世区块才停止。

好,差不多了。

接下来我们先测试一下:

AppController调用代码如下:

<?php
namespace App\Http\Controllers;
use Cache;
class AppController extends Controller
{

public function app(){

  
$time1 = time();
$bc = BlockChain::NewBlockChain();
$bc->addBlock('i am 2 block');
$bc->addBlock('i am 3 block');  
$time2 = time();

$spend = $time2 - $time1;
foreach ($bc as $block){
    print_r($block);
    echo('<hr>');
 }
echo('花费时间(s):'.$spend);

$lastHash=Cache::get('l');
 echo ($lastHash);

 }
 
}

打开浏览器,运行结果如下:

OK,正常工作。

打开缓存目录,生成了四个文件:

正好对应着,三个区块,和一个'l'-哈希值 键值对。

接着我们关闭浏览器,然后把

$bc->addBlock('i am 2 block');
$bc->addBlock('i am 3 block');  

这两句注释掉,然后再打开,测试一下是否能正常读取。

结果也是OK的。说明块已经正常保存。

PS:后面我调用Cache::flush();删除缓存时,把storage目录下的所有缓存都删除了,app和framework,也没有了,因为是在网页执行,所以造成打开网页报错。

后面调用composer create-project laravel/laravel mylaravel7 --prefer-dist

重新创建一个,把那两个缓存文件复制一份才正常。

所以你们要删除就一个一个删,或者手动删除。当然你们自写一个函数也是可以的。

这里就不举例了。

或者你们可以在storage目录下再创建一个目录,然后cache.php配置指向这个目录就行。

接下来我们还要增加一个管理操作,添加区块,和显示所有块的页面。

这里我就简单的用表单的功能代替命令行操作了。

为了简便,就不添加路由了,直接在public网站根目录,添加一个command.php页面,代码如下:

<!doctype html> 
 <html>
 <head></head>
 <body>

 <form name="form1" method="post" action="/app" target=“_blank”>
    区块数据:
 <input type="text" name="data">
 <input type="submit" name="add" value="添加">
 <br>
 <input type="submit" name="query" value="查询所有区块">
 </form>

</body>
 </html>

然后路由的app方法改成post路由,因为我表单用的是post请求,修改web.php:

use App\Http\Controllers\AppController;
Route::post('/app', [AppController::class, 'app']);

接着浏览器打开command.php提交表单数据,会报419错误。

这个是由于laravel为了安全,post请求需要带上令牌。

我们,直接关掉这个验证:

在app\Http\Kernel.php目录下,注释掉这一行:

  //\App\Http\Middleware\VerifyCsrfToken::class,

当然如果你要带上令牌,网上有另一种方法,需要添加路由访问,把command.php写为模版文件之类的,才能让那个命令有效果。这里从简,不采用了。

接写来,我们重写AppController的App函数,处理表单提交过来的post数据,如下代码:

<?php
namespace App\Http\Controllers;
use Cache;
use Illuminate\Http\Request;
class AppController extends Controller
{

public function app(Request $request){
    $bc = BlockChain::NewBlockChain();
    if($request->get('add')!="")
    {
        //添加区块
        $data=$request->get('data');
        $time1 = time();
        $bc->addBlock($data); 
        $time2 = time();
        $spend = $time2 - $time1;
        echo('已添加,花费时间(s):'.$spend);
        echo('<br>新添加块的哈希值是:<br>'.$bc->tips);
        echo('<hr/>所有区块信息:<br>');
        foreach ($bc as $block){
            print_r($block);
            echo('<hr>');
         }
    }
    else
    {
        //显示所有区块
        echo('所有区块信息:<br>');
        foreach ($bc as $block){
            echo('<hr>');
            print_r($block);
           
         }
        
    }

 }
 
}

接下来,我们打开command.php测试一下:

ok,总算大功造成,所有功能测试下来都正常。

本章完结,谢谢观看。 

  • 8
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Bczheng1

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值