源码地址: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,总算大功造成,所有功能测试下来都正常。
本章完结,谢谢观看。