主页 > imtoken安卓app > 编程小白模拟一个简单的比特币系统,带你写一波!(带代码)

编程小白模拟一个简单的比特币系统,带你写一波!(带代码)

imtoken安卓app 2023-02-14 07:35:11

出品 | 区块链营(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;
}

涉及的消息类型(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、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 函数。假设我们只查询钱包公钥信息,服务器会收到相应的请求。

@OnOpen
public 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 方法:

@Override
public void sendMsg(Session session, String msg) {
session.getAsyncRemote().sendText(msg);
}

2、服务端收到消息后向客户端发送消息(调用服务,处理后返回数据)

服务器接收到消息,进入P2PServer中的OnMessage方法

/**
 * 收到客户端发来消息
 * @param msg  消息对象
 */
@OnMessage
public void onMessage(Session session, String msg) {
    p2PService.handleMessage(session, msg);
}

p2PService.handleMessage就是解析接收到的消息(Msg),根据类型调用其他方法(一个巨无霸的Switch语句,这里只介绍一小部分)。在这里,我们从客户端收到信息代码 QUERY_WALLET。

@Override
public 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 的具体实现。其实就是遍历钱包集合,统计钱包公钥,并不难。

@Override
public 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方法:

@OnMessage
public 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 );
    });
}

在具体实现中,由于注入服务的方式,在使用@Autowired注解将bean注入Server(@ServerEndpoint)和Client(@ClientEndpoint)时,由于Spring Boot单例的特性。

而Websocket每次都会创建一个新的对象,所以在使用服务的时候,会导致空指针异常。因此,我们创建了一个工具类 Spring til,每次需要服务时,我们都从 Spring 容器中获取我们拥有的信息。所需的Bean,工具类代码如下。

public class SpringUtil implements ApplicationContextAware {
public static ApplicationContext applicationContext;
    @Override
public 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网络中Server节点与Client节点的交互过程。我建议您尝试一下,并在评论中与我们讨论!