区块链简单实现之p2p网络多节点同步

本文最后更新于:3 个月前

承接上文:区块链的简单实现,我们已经实现了一个简单的区块链数据结构。
现状:区块存放在数组中,程序重启就是一条新链,并且也只有单节点,没有引入多节点。
目标:将区块保存为文件,实现多节点之间的同步。

将区块保存为json文件

之前的类BlockChaincreatBlock方法:

1
2
3
4
5
6
7
8
9
10
creatBlock(data){
var lastBlock=this.getLastBlock();
var newIndex=lastBlock.index+1;
var newTime=new Date().getTime()/1000;
var newPreHash=lastBlock.hash;
var newhash=Block.caHash(newIndex,newPreHash,newTime,data.toString());
return this.blocks[this.blocks.length]= new Block.Block(
newIndex, newPreHash, newTime, data, newhash
);
}

可以看的出来,是直接保存在数组blocks中,我们稍作改进:

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
creatBlock(data){
var lastBlock=this.getLastBlock();
var newIndex=lastBlock.index+1;
var newTime=new Date().getTime()/1000;
var newPreHash=lastBlock.hash;
var creator= this.path;//简单的用路径来代替用户,creator是Block新增的一个属性
var newhash=Block.caHash(newIndex,newPreHash,newTime,data.toString(),creator);
var newblock= new Block.Block(
newIndex, newPreHash, newTime, data, newhash,creator
);
fs.writeFile("./blcs/"+this.path+"/block"+this.head.nums+".json",JSON.stringify(newblock,"","\t"),function (err){
if (err!=null)
console.log(err);
});

//此处注释掉的为后续增加的代码,此时可暂不阅读
// this.head.nums++;
// this.head.existsHash.push(newhash);
// if (!this.head.creators.includes(newblock.creator))
// this.head.creators.push(newblock.creator);

// fs.writeFile("./blcs/"+this.path+"/head.json",JSON.stringify(this.head,"","\t"),function (err){
// if (err!=null)
// console.log(err);
// });
return this.blocks[this.blocks.length]=newblock;
}

增加了一步写文件的操作,将newblock对象写入./blcs/path/blockx.json,如图:

区块被写入为json文件

如此,便能解决区块的存储问题。

节点

一个节点可以简单理解为一个用户,他们各有一条链,理想情况下,他们链上的内容不应起冲突并且应该能够实时同步,但我们都知道,一个分布式系统想要实时同步几乎是不可能的,我们需要一种合理的同步机制,尽量减少错误的发生。

p2p网络能帮助我们实现各节点的同步与通信,我们希望在每个节点生成区块或者接收区块时能广播通知网络中的其他节点也接收区块。

不确定性

但是有一个摆在面前的问题:

  • 我们无法得知网络中各节点的信息。

解决这个问题的一个思路是,建立一个中心化的服务器来存放各个节点的信息,但是显然这样不合我们的思路。
还有一个思路,就是在每个区块中保存一些有关节点的信息。

区块里保存节点信息

我采取的办法是,在每个区块增加一个creator的属性,用来保存产生它的那个节点的信息,便于我们能在网络中找到这个节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Block {
constructor(index, previousHash, timestamp, data, hash, creator) {
this.index = index;
this.previousHash = previousHash;
this.timestamp = timestamp;
this.data = data;
this.hash = hash.toString();
this.creator = creator;//保存产生它的节点的信息
}
toString(){
return "\n{\nindex:"+this.index+",\npreviousHash:'"+this.previousHash+"',\ntimestamp:"+this.timestamp+",\ndata:'"+this.data+"',\nhash:'"+this.hash+"',\ncreator:'"+this.creator+ "'\n}\n"
}
}

比如我本地运行的各个节点是运行在不同的端口号,我就可以通过在creator属性里保存端口号从而能找到节点,放在更大的网络里,我们可以保存产生它的节点的ip地址。

这样在其他节点接收到区块后,就可以从自己已有的区块里和其他节点联系上了,那么就可以实现广播。

并未向所有节点广播

其实很容易看出来,上面的思路不是真的广播,因为只有那些产生过区块并广播出去的节点才可能收到来自其他节点的广播。

那么那些没有产生过区块的节点呢?很多节点都是产生交易单然后经由矿工打包成区块,那么他们就不能收到广播,这是不合理的。不过这已经不在我们目前讨论范围了,因为我们区块的数据并没有什么实际含义,我们可以简单的认为节点就是各个矿机,随便拿着数据然后在做工作量证明然后签名发给其他节点。

简单模拟

我本地模拟多个节点采取的办法是,将程序运行在多个端口,让它们在各端口间同步数据,其实也具备p2p网络的特性了:

  • 不知道有哪些端口号有区块链程序在运行
  • 不知道运行的区块链程序是否已经停止运行
  • 各个端口之间可以互相发送http请求和响应

所以我给各个端口上的区块链程序之间同步信息的解决方法如下:

各个节点都会从自己的链中整理出各个区块的hashcreator,然后遵循以下策略:

  1. 接受一个区块后,要向自己知道的节点广播。
  2. 新生成一个区块后,要向自己知道的节点广播。
  3. 在接收到一个区块时判断自己是否已经接受过该区块的hash值,如果接受过就不再接受也不广播,否则接受并广播。
  4. 在接收到区块时都应当先作验证,验证区块是否有效。
  5. 接收到一个区块时,发现该区块的序号远大于自己序号最大的区块,应当从区块发出节点拉取前面的区块。

上述规则看似能解决问题,但是存在一个重大的问题:
在网络起步阶段时,各个节点都还没有接收过其他节点的区块,那么是无法从0到有的。
这时候需要我们给定一个默认的联系节点,只要网络运行起来了,哪怕这个默认节点停用了也无所谓。

广播的代码:

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
function broadcastBlock(block) {
var blockString=JSON.stringify(block,"","\t");
console.log("broadcast");
var options = {
hostname: 'localhost',
port: 2343,//默认的联系节点的端口
path: '/receive',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': Buffer.byteLength(blockString)
}
};


var ports=BlockChain.head.creators;//获取链中所有区块的creator,然后逐一发送
for (var i=0;i<ports.length;i++){
options.port=ports[i];
try {
var req=http.request(options,function (res) {
res.on('end',function () {
console.log("已经发送至:"+ports[i]);
});
res.on('data',function (chunk) {
console.log("没什么");
console.log(chunk);
})
res.on('error',function (err) {
console.log(port[i]+"数据传输出错");
})
});
req.on('error',function (err) {
console.log((port[i])+"未上线,未向其发送2");
req.end();
//console.log(err);
})
req.write(blockString);
req.end();
//console.log("已经发送至:"+ports[i]);
}catch (err) {
console.log(ports[i]+"发送失败");
}

}

}

实现效果:

上次运行保存的区块信息:
启动前

分别运行程序在2343,2344,2345三个端口上,然后从2343端口抓取的区块:

2343端口的节点的区块信息
在2343端口生成一个区块:
输入数据段
生成区块成功

到2344和2345端口查看是否同步过来了:
其实之间看自己文件有没有多就知道了,下图中明显同步了。
在这里插入图片描述

完整的代码:

你可以从仓库里获取完整代码:

github: https://github.com/Lixingwei0623/ablockchain.
gitee: https://gitee.com/li-xingwei/simple-block-chain.