主页 > 最新imtoken官方下载 > 编程小白模拟一个简单的比特币系统,带你写一波! (带代码) |精选博客
编程小白模拟一个简单的比特币系统,带你写一波! (带代码) |精选博客
制作 |区块链营(blockchain_camp)
封面 |视觉中国CSDN付费下载
如果有p2p的demo,我们如何应用到区块链当中?
今天就试试吧!
首先,我们需要模拟网络中的多个节点相互通信。我们假设目前的情况是有两个节点A和B,整个过程如下图所示:
@ >
合并流程
我们梳理一下整个流程,明确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的大致流程如下,我们后面的实现会结合这个流程。
客户端→服务器发送消息,通常是请求数据
服务器收到消息后,向客户端发送消息(调用服务,处理后返回数据)
客户端接收消息并处理数据(调用服务处理数据)
相关代码
在执行过程中,由于消息的种类很多。它封装了一个消息对象来传递消息,对消息类型进行编码,统一处理。消息对象Message实现了Serializable接口,使其对象可序列化:
public class Message implements Serializable {/** * 消息内容,就是我们的区块链、交易池等所需要的信息,使用JSON.toString转化到的json字符串 */private String data;/** * 消息类型 */private int 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;
因为代码太多,这里就不一一贴了。以客户端同步其他节点的钱包信息为例,结合以上p2p网络交互的三个步骤,介绍一下相关实现。
1、客户端→服务器发送消息,通常是请求数据
在客户端节点的启动类中,首先创建客户端对象,调用客户端内部方法,连接服务端。
启动类main方法中的关键代码,(端口参数在args中配置):
P2PClient p2PClient = new P2PClient();String url = "ws://localhost:"+args[0]+"/test"; p2PClient.connectToPeer(url);
P2PClient中的connectToPeer方法
p>
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中,onOpen函数会在WebSocketContainer.connectToServer时回调。假设我们只查询钱包公钥信息,那么服务器就会收到相应的请求。
@OnOpenpublic void onOpen(Session session) {this.session = session; p2PService.sendMsg(session, p2PService.queryWalletMsg());}
注意:我把解析消息相关的操作封装成一个服务,方便服务端和客户端统一使用。给出对应的queryWalletMsg方法:
public String queryWalletMsg() {return JSON.toJSONString(new Message(QUERY_WALLET));}
还有前面提到的sendMsg方法:
@Overridepublic void sendMsg(Session session, String msg) {session.getAsyncRemote().sendText(msg);}
2、服务器收到消息后,向客户端发送消息(调用服务,处理后返回数据)
服务器收到消息,进入P2PServer中的OnMessage方法
/** * 收到客户端发来消息 * @param msg 消息对象 */@OnMessagepublic void onMessage(Session session, String msg) { p2PService.handleMessage(session, msg);}
p2PService.handleMessage就是解析接收到的消息(msg),根据类型调用其他方法(一个巨大的switch语句,这里介绍一小部分),这里我们从客户端收到信息码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));}
这里我将区块链的相关操作封装成一个服务。下面给出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);}
获取数据后,返回给客户端:
所以在我们的responseWallets()方法中挖比特币程序,最后一句新建了一个消息对象,并将信息码设置为RESPONSE_WALLET,并在handleMessage中调用sendmsg方法返回给客户端。
case QUERY_WALLET: sendMsg(session, responseWallets()); break;
3、客户端接收消息并处理数据(调用服务处理数据)
客户端收到请求并获取数据,进入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的实现,解析接收到的钱包公钥信息,存储在客户端节点的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 ); });}
在具体实现中,由于注入服务的方式,在服务端(@ServerEndpoint)和客户端(@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());}
你好节点,测试作为服务器
测试节点,并在测试期间作为客户端。
至此,我们已经实现了p2p网络中服务端节点和客户端节点的交互过程。我建议你尝试一下,并在评论中与我们讨论!
“原力计划[第二季]-学习挑战”正式开始!
即日起至3月21日,千万流量支持原作者,更有专属【勋章】等你挑战