作者 | VV一笑ヽ
责编 | Carol
出品 | 区块链大本营(blockchain_camp)
封面 | CSDN 付费下载于视觉中国
如果有一个 p2p 的 demo,我们要怎么才能应用到区块链当中?
今天就来一起尝试一下吧!
首先,我们需要模拟网络中的多个节点相互通讯,我们假设现在的情况是有AB两个节点整个过程如下图所示:
梳理流程
让我们来梳理一下整个流程,明确在p2p网络中需要做的事情。
启动节点A。A首先创建一个创世区块
创建钱包A1。调用节点A提供的API创建一个钱包,此时A1的球球币为0。
A1挖矿。调用节点A提供的挖矿API,生成新的区块,同时为A1的钱包有了系统奖励的球球币。
启动节点B。节点B要向A同步信息,当前的区块链,当前的交易池,当前的所有钱包的公钥。
创建钱包B1、A2,调用节点A和B的API,要广播(通知每一个节点)出去创建的钱包(公钥),目前节点只有两个,因此A需要告诉B,A2的钱包。B需要告诉A,B1的钱包。
A1转账给B1。调用A提供的API,同时广播交易。
A2挖矿记账。调用A提供的API,同时广播新生成的区块。
总结一下,就是节点刚开始加入到区块链网络中,需要同步其他节点的
区块链信息
钱包信息
交易信息
已经处于网络中的某个节点,在下述情况下需要通知网络中的其他节点
发生新的交易
创建新的钱包
挖矿产生新区块
P2P的大致流程为下方几点,我们后边的实现会结合这个过程。
client→server 发送消息,一般是请求数据
server收到消息后,向client发送消息 (调用service,处理后返回数据)
client收到消息处理数据(调用service,对数据处理)
相关代码
在实现的过程中,由于消息类型较多,封装了一个消息对象用来传输消息,对消息类型进行编码,统一处理,消息对象Message,实现了Serializable接口,使其对象可序列化:
public class Message implements Serializable {/** * 消息内容,就是我们的区块链、交易池等所需要的信息,使用JSON.toString转化到的json字符串 */private String data;/** * 消息类型 */private int type;}
涉及到的消息类型(type)有:
/** * 查询最新的区块 */private final static int QUERY_LATEST_BLOCK = 0;/** * 查询整个区块链 */private final static int QUERY_BLOCK_CHAIN = 1;/** * 查询交易集合 */private final static int QUERY_TRANSACTION = 2;/** * 查询已打包的交易集合 */private final static int QUERY_PACKED_TRANSACTION = 3;/** * 查询钱包集合 */private final static int QUERY_WALLET = 4;/** * 返回区块集合 */private final static int RESPONSE_BLOCK_CHAIN = 5;/** * 返回交易集合 */private final static int RESPONSE_TRANSACTION = 6;/** * 返回已打包交易集合 */private final static int RESPONSE_PACKED_TRANSACTION = 7;/** * 返回钱包集合 */private final static int RESPONSE_WALLET = 8;
由于代码太多,就不全部粘在这里了,以client同步其他节点钱包信息为例,结合上面的p2p网络交互的三个步骤,为大家介绍下相关的实现。
1、client→server 发送消息,一般是请求数据
在client节点的启动类首先创建client对象,调用client内部方法,连接server。
启动类main方法中关键代码,(端口参数配置在args中):
P2PClient p2PClient = new P2PClient();String url = "ws://localhost:"+args[0]+"/test"; p2PClient.connectToPeer(url);
P2PClient
中的connectToPeer
方法
public void connectToPeer(String url) throws IOException, DeploymentException { WebSocketContainer container = ContainerProvider.getWebSocketContainer(); URI uri = URI.create(url);this.session = container.connectToServer(P2PClient.class, uri);}
P2PClient
中,WebSocketContainer.connectToServer
的时候会回调onOpen
函数,假设我们只查询钱包公钥信息,此时服务端会接收到相应的请求。
@OnOpenpublic void onOpen(Session session) {this.session = session; p2PService.sendMsg(session, p2PService.queryWalletMsg());}
注意:我把解析消息相关的操作封装到了一个service 中,方便server和client的统一使用。给出相应的queryWalletMsg
方法:
public String queryWalletMsg() {return JSON.toJSONString(new Message(QUERY_WALLET));}
以及之前提到的sendMsg
方法:
@Overridepublic void sendMsg(Session session, String msg) {session.getAsyncRemote().sendText(msg);}
2、server收到消息后,向client发送消息 (调用service,处理后返回数据)
server收到消息,进入P2PServer
中OnMessage
方法
/** * 收到客户端发来消息 * @param msg 消息对象 */@OnMessagepublic void onMessage(Session session, String msg) { p2PService.handleMessage(session, msg);}
p2PService.handleMessage
就是解析接收到的消息(msg),根据类型的不同调用其他的方法(一个巨型switch语句,这里就介绍一小部分),这里我们接收到了client传来的信息码QUERY_WALLET
。
@Overridepublic void handleMessage(Session session, String msg) { Message message = JSON.parseObject(msg, Message.class);switch (message.getType()){case QUERY_WALLET: sendMsg(session, responseWallets());break;case RESPONSE_WALLET: handleWalletResponse(message.getData());break; ...... }
根据信息码是QUERY_WALLET
,调用 responseWallets()
方法,得到数据。
private String responseWallets() {String wallets = blockService.findAllWallets();return JSON.toJSONString(new Message(RESPONSE_WALLET, wallets));}
这里我把区块链的相关操作也封装到了一个service中,下面给出findAllWallets
的具体实现,其实就是遍历钱包集合,统计钱包公钥,没有什么难度。
@Overridepublic String findAllWallets() { List wallets = new ArrayList<>(); myWalletMap.forEach((address, wallet) ->{ wallets.add(Wallet.builder().publicKey(wallet.getPublicKey()).build()); }); otherWalletMap.forEach((address, wallet) ->{ wallets.add(wallet); });return JSON.toJSONString(wallets);}
得到数据之后,返回给client:
因此我们的 responseWallets()
方法中,最后一句话新建了一个message对象,并设置了信息码为RESPONSE_WALLET
,在handleMessage
中调用了sendmsg
方法回传给client。
case QUERY_WALLET: sendMsg(session, responseWallets()); break;
3、client收到消息处理数据(调用service,对数据处理)
client收到了请求得到的数据,进入P2PClient
中的OnMessage
方法
@OnMessagepublic void onMessage(String msg) { p2PService.handleMessage(this.session, msg);}
同样进入我们上面提到的p2PService.handleMessage
方法,此时收到的信息码为RESPONSE_WALLET
,进入handleWalletResponse
方法
case RESPONSE_WALLET: handleWalletResponse(message.getData()); break;
handleWalletResponse
的实现, 解析接收到的钱包公钥信息,并存储到client节点的blockService
中。
private void handleWalletResponse(String msg) { List wallets = "\"[]\"".equals(msg)?new ArrayList<>():JSON.parseArray(msg, Wallet.class); wallets.forEach(wallet -> { blockService.addOtherWallet(walletService.getWalletAddress(wallet.getPublicKey()),wallet ); });}
在具体实现中,由于使用到了注入服务的方式,在向server(@ServerEndpoint)和client(@ClientEndpoint)中使用@Autowired 注解注入Bean的时候,由于Spring boot 单例的特点,而websocket每次都会创建一个新的对象,所以在使用服务的时候会导致出现空指针异常,因此,我们创建了一个工具类Springtil,每次需要服务时,都从Spring容器中获取到我们所需要的bean,下面给出工具类代码。
public class SpringUtil implements ApplicationContextAware {public static ApplicationContext applicationContext; @Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {if (SpringUtil.applicationContext != null) { SpringUtil.applicationContext = applicationContext; } }/** * 获取applicationContext */public static ApplicationContext getApplicationContext() {return applicationContext; }
/** * 通过name获取 Bean. */public static Object getBean(String name) {return getApplicationContext().getBean(name); }/** * 通过class获取Bean. */public static T getBean(Class clazz) {return getApplicationContext().getBean(clazz); }
/** * 通过name,以及Clazz返回指定的Bean */public static T getBean(String name, Class clazz) {return getApplicationContext().getBean(name, clazz); }}
因此测试之前我们首先需要设定SpringUtil
中的applicationContext
,下面给出启动类(为了简单测试,两个节点共用一个启动类,根据args的不同来分别处理)以及相关节点的配置。
public static void main(String[] args) { System.out.println("Hello world"); SpringUtil.applicationContext = SpringApplication.run(Hello.class, args);if (args.length>0){ P2PClient p2PClient = new P2PClient(); String url = "ws://localhost:"+args[0]+"/test";try { p2PClient.connectToPeer(url); } catch (Exception e) { e.printStackTrace(); } }
使用时,我们需要手动获取bean
//之前是这样//@Autowired//private P2PService p2PService;//改正后,去掉Autowired,每次使用都手动获取beanprivate P2PService p2PService;@OnOpenpublic void onOpen(Session session) {//如果不使用那些,在这里会报空指针异常,p2PService 为 null p2PService = SpringUtil.getBean(P2PService.class);//新增这句话从IVO容器中获取bean p2PService.sendMsg(session, p2PService.queryWalletMsg());}
Hello节点,测试时作为server
Test节点,测试时作为Client。
到此,我们就实现了p2p网络中server节点与client节点的交互过程。建议你也可以尝试一下,然后在评论区和我们讨论哦!
《原力计划【第二季】- 学习力挑战》正式开始!
即日起至 3月21日,千万流量支持原创作者,更有专属【勋章】等你来挑战
推荐阅读
Libra新编程语言 :Move 的所有权模型灵感来源原来是它……
从哈希函数、哈希冲突、开散列出发,一文告诉你哈希思想与哈希表构造到底是什么!
留德武汉程序员在疫区:凌晨下载数据,网速影响工作
中国开发者真实画像:Java长盛,偏爱Windows操作系统,鲜少参与开源项目
用于小型图形挖掘研究的瑞士军刀:空手道俱乐部的图表学习Python库
全方位解析阿里云核心技术竞争力,CSDN 独家在线峰会来了!
老铁们求在看!????