一 . 搭建基于Gossip协议的区块链系统
1.练习目标
基于Gossip协议原理,使用pull模式构建区块链网络通信,在区块链网络中加入5个节点。
-
(1)目标节点随机选择邻居节点发送请求,询问是否有新数据。
-
(2)邻居节点向目标节点发送新数据的请求响应,告知目标节点是否有新数据
-
(3)目标节点判别接收到的请求响应,若有新数据则更新自身的“数据池”。跳转至步骤1循环执行。
2.任务内容
-
1).创建区块链网络(Network)、节点(Peer) 以及消息(Mesge)对象模型。
-
(2)定义socket客户端和服务端功能,其中客户端功能为询问是否有数据请求,服务端功能为针对询问内容给予相应。
-
(3)以HTTPPOST接山形式,实现新消息创建。
-
(4)以HTTPGET接u形式,实现节点“数据池”查询功能
-
(5)以Flask服务端的方式,启动网络5个节点
3.具体实现
3.1系统设计思路
根据目标要求,每个节点(Peer)需要包括服务端和客户端两方面内容:
在服务端方面,需要构建以Flask服务端框架为基础的HTTP服务端以及Socket 服务端。其中HTTP服务端需要包括新消息添加以及节点“数据池”查询的接口。Socket 服务端需要包括接收新数据询问和接收数据接口。
在节点系统中Socket通信主要目标为实现节点间数据同步,假设有节点A及邻居节点B,具体实现流程为:
-
1)节点A的Socket客户端向邻居节点B的Socket服务端发送新数据询问请求
-
2)邻居节点B接收到数据询问后根据请求数据的版本信息查询本节点中是否有新数据,将结果打包为数据包后通过客户端发送给节点A的Socket服务端指定接口
-
3)节点A的Socke服务端接收新数据包后判断是有新数据,如果有的话就存储进数据池”。
3.2创建区块链网络的实体模型
基于系统设计思路,可以开始构建基于Gossip 协议的区块链网络,首先创建包括消息对象模型(Message)、节点对象模型(Peer)、区块链网络对象模型(Netork)三个实体。
创建gossip_app项目,在项目中创建models.py存储代码。
(1).消息对象模型(Message)
class Message(object):
def __init__(self, data, c_time):
"""
初始化消息对象
:param data:传递的数据
:param c_time:创建时间
"""
self.data = data
self.c_time = c_time
#消息的版本号,用于跟踪最新的数据
self.version = int(c_time.timestamp())
def to_dict(self):
"""
将信息结构转换为字典
:return:消息的字典对象
"""
return {
'data': self.data,
'c_time': self.c_time.strftime("%Y-%m-%d %H:%M:%S"),
'version': self.version
}
-
Message对象模型属性及说明
属性名 数据类型 属性说明 data 字符串(string) 消息内容 c_time 时间(datetime) 消息创建的时间 version 整型(int) 消息的版本号,用于跟踪最新数据 -
Message对象的方法及说明
方法名 方法说明 入参 返回值 to_dict 将消息结构转为字典 将Message对象内容转为字典(dict)
(2).节点对象模型(Peer)
import socketio
class Peer(object):
def __init__(self, name, host, port):
"""
初始化节点
:param name:节点名称
:param host:节点的主机ip
:param host:节点的端口
"""
self.name = name
self.host = host
self.port = port
self.version = 0 #节点当前最新的数据版本号
self.pool = []
self.sio = socketio.Client()
def add_message(self, message):
"""
在"数据池"中添加信息
:param message:传入的message对象
"""
self.pool.append(message)
-
Peer对象模型的属性及说明:
属性名 数据类型 属性说明 name 字符串(string) 消息内容 host 时间(datetime) 消息创建的时间 port 整型(int) 消息的版本号,用于跟踪最新数据 version 整型(int) 节点“数据池”中当前最新的数据版本号 sio socketio的客户端 节点内置的Socket客户端 -
表5-4 Peer对象模型的方法及说明
方法名 方法说明 入参 返回值 add_message 在"数据池"中添加信息 message:新消息
(3).区块链网络实体(Network)
import networkx as nx
import matplotlib.pyplot as plt
from datetime import datetime
class Network(object):
def __init__(self, name):
"""
初始化区块链网络
:paeam name:
"""
self.peers = [] #网络存在节点
self.name = name #网络名称
self.G = nx.Graph() #网络中的定义的network网络拓扑
def add_peer(self, peer):
"""
在网络中新增节点
"""
self.peers.append(peer)
self.G.add_node(peer.name, host=peer.host, port=peer.port)
def add_edge(self, s_peer, e_peer):
"""
在网络中新增节点间的边
"""
e = (s_peer, e_peer)
self.G.add_edge(*e)
def del_peer(self, peer_name):
"""
删除指定名称的peer节点
"""
for i, peer in enumerate(self.peers):
if peer_name == peer.name:
del self.peers[i]
self.G.remove_node(peer_name)
def draw_network(self):
"""
绘制网络
"""
pos = nx.spring_layout(self.G, iterations=100)
nx.draw(self.G, pos, with_labels=True)
plt.show()
-
Network对象的属性内容说明:
属性名 数据类型 属性说明 peers 列表 所有网络中存在的节点 name 字符串 网络名称 G netowkx初始化图形 网络中定义的networkx网络拓扑 -
Network对象的方法及说明
方法名 方法说明 入参 返回值 add_peer 在网络中新增节点 peer:网络新节点 add_edge 在网络中新增节点间的边 s_ peer: 起始节点名称 e_peer:结束节点名称 del_peer 删除指定名称的peer节点 peer_ name:节点名称 draw_network 绘制网络
3.3.定义通用返回
在gossip_app项目目录中创建http_res.py用于定义HTTP请求接口。
#HTTP响应
empty_res = {'code':404,'data':'empty data'} #空数据响应
success_res = {'code':200,'data':'ok'} #成功响应
3.4.定义通用处理函数
在gossip_app项目中创建services.py文件,在其中加入相关通用的处理函数。
-
系统通用处理函数及相应的说明
-
函数名 函数说明 入参 返回值 generate_network 生成区块链网络,初始化Network 实体, 以及在Network实体 中加入节点(node)和 边(node) network _name:网络名称 peer _list: 已生成的节点列表 生成的区块 链网络实体对象 new_msg_service HTTP接口处理函 数,添加新消息处理 HTTP POST请求体内容 c _peer当前节点对象函数 通用返回 send_version Socket客户端处理函 数,随机发送最新数 据版本服务 peer: 当前节点 network: 当前区块链网络 peer_version_ services 用于接收socket客户 端请求,message中包 含节点版本(version) rec msg: “键值对"形式 c_peer:当前服务端节点 sio:当前服务端节点的客户 peer_message_ service Socket服务端接收数 据服务,如果code 为0表示没有新消 息,code为1表示有新消息 msg_json:接收消息 C_peer: 当前服务端节点
根据项目在启动执行的不同阶段,将通用处理函数分为四类,分别为:启动执行函数(generate_network),HTTP服务端接口处理函数(new_msg_service),Socket 客户端处理函数(send_version),Socket 服务端处理函数(peer_services,peer_message_service).
import networkx as nx
import models
from datetime import datetime
import random
from flask import jsonify
import http_res
import time
import socketio
def generate_network(network_name, peer_list):
"""
生成区块链网络,初始化Network实体,以及在Network实体中加入节点(node)和边(node)
:param network_name:网络名称
:param peer_list:已生成的节点列表
:return:生成的区块链网络实体对象
"""
g_network = models.Network(network_name)
for index, peer in enumerate(peer_list):
g_network.add_peer(peer)
if index == len(peer_list)-1:
#如果是最后一个节点,那就让最后一个节点和第一个节点首位相连
g_network.add_edge(peer.name, peer_list[0].name)
else:
#否则,建立本节点与下一个节点的边
g_network.add_edge(peer.name, peer_list[index+1].name)
return g_network
def new_msg_service(body, c_peer):
"""
HTTP接口处理函数,添加新消息处理函数
:param body:HTTP POST请求体内容
:param c_peer:当前节点对象
:return:
"""
if 'data' not in body:
return jsonify(http_res.empty_res)
#创建新的消息(message)对象
msg = models.Message(body['data'],datetime.now())
#将消息对象存储于当前节点中
c_peer.add_message(msg.to_dict())
c_peer.version = msg.version
return jsonify(http_res.success_res)
def send_version(peer, network):
"""
Socker客户端处理函数,随机发生最新数据版本服务
:param peer:当前节点
:param network:当前区块链网络
:return:
"""
print('start to send version!')
peer_name = peer.name
neighbours = list(network.G.adj[peer_name])
rand_index = random.randint(0, len(neighbours)-1)
neighbour_peer_name = neighbours[rand_index]
neighbour_peer = network.G.nodes()[neighbour_peer_name]
req_url = f"http://{neighbour_peer['host']}:{neighbour_peer['port']}"
res_url = f"http://{peer.host}:{peer.port}"
print(f'connect to peer {req_url}')
peer.sio=socketio.Client()
peer.sio.connect(req_url,wait_timeout=10)
send_msg = {
'version':peer.version,
'url':res_url
}
peer.sio.emit('peer-version', send_msg)
time.sleep(3)
peer.sio.disconnect()
def peer_version_services(rec_msg, c_peer, sio):
"""
用于接受socker客户端请求,message中包含节点版本(version)
:param rec_msg:key-value形式。包括version->请求peer的最新数据版本,url ->请求peer的url
:param c_peer:当前服务端节点
:param sio:当前服务端节点的客户端
:return:
"""
version = rec_msg['version']
url = rec_msg['url']
print(f"receive message : {version}")
#如果请求的消息版本号小于本节点最新节点版本号,则需取出两版本号之间的所有数据
res_arr = []
send_msg = {}
if version < c_peer.version:
#倒序遍历,从列表最后一个元素依次遍历至索引为0的内容
for i in range(len(c_peer.pool) - 1, -1, -1):
get_msg = c_peer.pool[i]
if version < get_msg['version']:
res_arr.insert(0, get_msg)
# 按固定格式返回数据
send_msg = {
'code':1, #1表示存在数据
'data':res_arr
}
else:
#当查询不到数据,则返回空数据
send_msg = {
'code':0, #0表示不存在新数据
'data':'empty'
}
sio = socketio.Client()
sio.connect(url, wait_timeout=10)
sio.emit('peer-message', send_msg)
time.sleep(3)
sio.disconnect()
def peer_message_service(msg_dict, c_peer):
"""
socket服务端接收数据服务,如果code为0表示没新消息code为1表示新消息
:param msg_json:接收消息
:param c_peer:当前服务端节点
:return:
"""
#msg_dict = json.loads(msg)
if 'code' not in msg_dict or 'data' not in msg_dict:
return
# code:0表示没有新数据不做任何操作
#code:1表示有新数据加入"数据池"中
if msg_dict['code'] == 0:
return
if msg_dict['code'] ==1:
for get_msg in msg_dict['data']:
msg = models.Message(get_msg['data'],
datetime.strptime(get_msg['c_time'], "%Y-%m-%d %H:%M:%S"))
c_peer.pool.append(msg.to_dict())
c_peer.version = msg_dict['data'][-1]['version']
3.5.创建Gossip网络的实体
在gossip app项目中创建entity.py 文件,在其中创建peer0至peer4五个节点实体以及对应区块链网络实体network
在创建network实体时将执行通用函数的generate_ network 函数,用于在程序启动时初始化去跨链网络。
import services
import models
# 用于创建节点和网络实体
peer0 = models.Peer('peer0', 'localhost', 5000)
peer1 = models.Peer('peer1', 'localhost', 5001)
peer2 = models.Peer('peer2', 'localhost', 5002)
peer3 = models.Peer('peer3', 'localhost', 5003)
peer4 = models.Peer('peer4', 'localhost', 5004)
peer_list = [peer0, peer1, peer2, peer3, peer4]
network = services.generate_network('test', peer_list)
3.6创建节点执行主体
在gossip app项目中分别创建app0.py, app1.py, app2.py, app3.py, app4.py分别代表节点0至节点4的区块链网络节点,分别在文件中加入系统功能组件,包括初始化系统功能、Socket客户端、Socket服务端。在代码通过APScheduler定时通过Socket客户端向邻居节点发送数据询问(send message).
from flask import Flask, request, jsonify
import flask_socketio
import services
import entity
from flask_apscheduler import APScheduler
network = entity.network # 获取区块链网络内容
c_peer = entity.peer0 # 取出当前进程的节点内容
http_port = 5000 # 定义HTTP使用接口
app = Flask(__name__)
# 创建socket客户端
scheduler = APScheduler()
class Config(object):
SCHEDULER_API_ENABLED = True
app.config.from_object(Config())
scheduler.init_app(app)
@scheduler.task('interval', id='send_message', seconds=5, misfire_grace_time=900)
def send_message():
services.send_version(c_peer, network)
# 创建socket服务端处理内容
peer_socketio = flask_socketio.SocketIO(app, cors_allowed_origins='*')
@peer_socketio.on('peer-version')
def peer_version(rec_msg):
services.peer_message_service(rec_msg, c_peer, c_peer.sio)
@peer_socketio.on('peer-message')
def peer_message(msg_dict):
services.peer_message_service(msg_dict, c_peer)
# 创建HTTP接口处理函数
@app.route('/new_msg', methods=['POST'])
def new_msg():
"""
用于接收用户新消息请求,数据将存储于对应节点的"数据池"中
:return:
"""
body = request.json
return services.new_msg_service(body, c_peer)
@app.route('/get_pool', methods=['GET'])
def get_pool():
"""
获取"数据池"里的数据
:return:
"""
return jsonify({
'code': 200,
'data': c_peer.pool
})
if __name__ == '__main__':
print(f"{'*' * 20}Starting peer0!{'*' * 20}")
scheduler.start()
peer_socketio.run(app, host='0.0.0.0', port=http_port, debug=False)
根据app0.py中的代码,可以复用至app1.py 至app4.py文件中,在其中只需修改节点和端口配置(根据实际节点修改,如节点1的端口是5001, 节点名为peer1)
-
app1.py
network = entity.network # 获取区块链网络内容
c_peer = entity.peer1 # 取出当前进程的节点内容
http_port = 5001 # 定义HTTP使用接口
-
app2.py
network = entity.network # 获取区块链网络内容 c_peer = entity.peer2 # 取出当前进程的节点内容 http_port = 5002 # 定义HTTP使用接口
-
app3.py
network = entity.network # 获取区块链网络内容 c_peer = entity.peer3 # 取出当前进程的节点内容 http_port = 5003 # 定义HTTP使用接口
-
app4.py
network = entity.network # 获取区块链网络内容 c_peer = entity.peer4 # 取出当前进程的节点内容 http_port = 5004 # 定义HTTP使用接口
已完成gossip app项目全部内容开发,其中包括models.py,
http_res.py,servicespy, entity.py 以及app0.py至app4.py文件
3.7.项目完成验证
分别执行app0.py至app4.py文件可执行节点
根据Gossip拓扑,peer0的邻居节点分别为peer1和peer4那么当peer1有新数据产生时,peer0 将通过询问后同步peer1数据。接下来,首先使用Postman向peer1调用新消息接口new_msg加入若干新数据。
接着,使用Postman调用peerl的get pool接口,查看“数据池”内容
接着,可以查看peer0的“数据池”,同样运用get_pool接口,但是请求的端口号需切换成peer0的5000