Solana之旅5:Web3访问

客户端

Json RPC API

RPC

RPC(Remote Procedure Calls )远程过程调用是一种协议,就是从一台机器(客户端)上通过参数传递的方式调用另一台机器(服务器)上的一个函数或方法(可以统称为服务)并得到返回的结果

RPC协议通常的实现有XML-RPC , JSON-RPC ,gRPC等,它们的通信方式基本相同, 所不同的只是传输数据的格式。

RPC是分布式架构的核心,按响应方式分如下两种:

  1. 同步调用:客户端调用服务方方法,等待直到服务方返回结果或者超时,再继续自己的操作。
  2. 异步调用:客户端把消息发送给中间件,不再等待服务端返回,直接继续自己的操作。

一个完整的RPC架构里面包含了四个核心的组件,分别是Client,Client Stub,Server以及Server Stub:

  1. 客户端(Client),服务的调用方。
  2. 客户端存根(Client Stub),存放服务端的地址消息,再将客户端的请求参数打包成网络消息,然后通过网络远程发送给服务方。
  3. 服务端(Server),真正的服务提供者。
  4. 服务端存根(Server Stub),接收客户端发送过来的消息,将消息解包,并调用本地的方法。

img

RPC的调用流程如下图所示:

img

该流程中的具体步骤是:

  1. 服务调用方(client)(客户端)以本地调用方式调用服务。
  2. client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;在Java里就是序列化的过程。
  3. client stub找到服务地址,并将消息通过网络发送到服务端。
  4. server stub收到消息后进行解码,在Java里就是反序列化的过程。
  5. server stub根据解码结果调用本地的服务。
  6. 本地服务执行处理逻辑。
  7. 本地服务将结果返回给server stub。
  8. server stub将返回结果打包成消息,主要也是Java里的序列化过程。
  9. server stub将打包后的消息通过网络并发送至消费方。
  10. client stub接收到消息,并进行解码, Java里的反序列化。
  11. 服务调用方(client)得到最终结果。

RPC框架的目标,就是要上面步骤里2~10给封装好,让用户像调用本地服务一样的调用远程服务,实现对客户端(调用方)透明化服务。这个听起来好像不难,但真正落地实现,就要面对以下几个难题:

  1. 通讯问题 : 主要是通过在客户端和服务器之间建立TCP/UDP连接,远程过程调用的所有交换的数据都在这个连接里传输;TCP连接可以是按需连接,调用结束后就断掉,也可以是长连接,多个远程过程调用共享同一个连接。
  2. 寻址问题: A服务器上的应用怎么告诉底层的RPC框架,如何连接到B服务器(如主机或IP地址)以及特定的端口,方法的名称是什么,这样才能完成调用。比如基于Web服务协议栈的RPC,就要提供一个endpoint URI。
  3. 序列化与反序列化 : 当A服务器上的应用发起远程过程调用时,方法的参数需要通过底层的网络协议如TCP传递到B服务器,由于网络协议是基于二进制的;内存中的参数的值要序列化成二进制的形式,也就是序列化(Serialize)或编组(marshall),再发送给B服务器;B服务器接收参数要将参数反序列化;同理,B服务器应用调用自己的方法处理后返回的结果也要序列化给A服务器,A服务器接收也要经过反序列化的过程。

Json RPC

像以太坊等主流区块链实现的RPC,都是基于Json RPC的。目前的版本是V2.0。

  1. JSON-RPC是一个无状态且轻量级的远程过程调用(RPC)协议实现规范。
  2. 它主要定义了一些数据结构及其相关的处理规则。
  3. 它允许运行在基于socket,http等诸多不同消息传输环境的同一进程中。
  4. 它使用JSON(RFC 4627)作为数据格式,这是它最大的特点。
  5. JSON支持4种基本类型
  • String
  • Numbers
  • Booleans
  • Null
  1. JSON还支持两种结构化类型:
  • Objects
  • Arrays
  1. 上述数据类型的第一个字母必须大写;客户端与服务端之间交换的成员名字,也是区分大小写的。
  2. 函数、方法、过程的称谓在该规范里是可互换的。
  3. 客户端:定义为请求对象的来源及响应对象的处理程序。
  4. 服务端:定义为响应对象的起源和请求对象的处理程序。
  5. 一个请求对象包括以下成员:
  • jsonrpc:指定JSON-RPC协议版本的字符串,必须准确写为“2.0”;
  • method:包含所要调用方法名称的字符串;
  • params:调用方法所需要的结构化参数值,该成员参数可以被省略;
  • id:已建立客户端的唯一标识id,值必须包含一个字符串、数值或NULL空值;如果不包含该成员则被认定为是一个通知。

下面就是一个请求对象的例子

{
    
    "jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1}
  • 通知:没有包含“id”成员的请求对象为通知, 作为通知的请求对象表明客户端对相应的响应对象并不感兴趣,本身也没有响应对象需要返回给客户端;
  • 参数结构:rpc调用如果存在参数则必须为基本类型或结构化类型的参数值,要么为索引数组,要么为关联数组对象。
    • 索引:参数必须为数组,并包含与服务端预期顺序一致的参数值;
    • 关联名称:参数必须为对象,并包含与服务端相匹配的参数成员名称;没有在预期中的成员名称可能会引起错误。名称必须完全匹配,包括方法的预期参数名以及大小写。
  1. 响应对象也会是一个JSON对象,它的成员包括:
  • jsonrpc:指定JSON-RPC协议版本的字符串,必须准确写为“2.0”。
  • result:该成员在成功时必须包含,其值由服务端中的被调用方法决定;当调用方法引起错误时必须不包含该成员。
  • error:当没有引起错误的时必须不包含该成员;该成员在失败时必须包含,且其值可以为以下对象:
    • code:使用数值表示该异常的错误类型, 必须为整数。
    • message:对该错误的简单描述字符串;该描述应尽量限定在简短的一句话。
    • data:包含关于错误附加信息的基本类型或结构化类型;该成员可忽略; 该成员值由服务端定义(例如详细的错误信息,嵌套的错误等)。
  • id:该成员必须包含;该成员值必须与请求对象中的id成员值一致;若在检查请求对象id时错误(例如参数错误或无效请求),则该值必须为空值。

下面是一个响应对象的例子:

{
    
    "jsonrpc": "2.0", "result": 19, "id": 1}

Solana Json RPC

Solana也是基于JSON RPC来实现客户RPC调用的。

  1. Solana实现了基于HTTP的RPC API:
  1. Solana也实现了若干基于WebSocket API:
  • 默认端口:8900
  • 节点访问:例如ws://localhost:8900
  1. 下面是已实现的HTTP API
  1. 下面则是已实现的WebSocket API

Web3 Javascript API

  1. Solana为Javascript创建了Web3.js包:@solana/web3.js。
  2. 下面我们直接来看一段样例代码:
var solana_web3 = require('@solana/web3.js');
function testMemo(connection, account){
    
    
    const instruction = new solana_web3.TransactionInstruction({
    
    
        keys: [],
        programId:new solana_web3.PublicKey('D8Cnv1UcThay2WijWP4SQ8G683UuVsKPaZEU7TNVKW1j'),
        data: Buffer.from('cztest'),
    });
    console.log("account:", account.publicKey.toBase58())
    solana_web3.sendAndConfirmTransaction(
        connection,
        new solana_web3.Transaction().add(instruction),
        [account],
        {
    
    
            skipPreflight: true,
            commitment: "singleGossip",
        },
    ).then(()=>{
    
    console.log("done")}).catch((e)=>{
    
    console.log("error",e)});
}
function main() {
    
    
    connection = new solana_web3.Connection("https://devnet.solana.com", 'singleGossip');
    const account = new solana_web3.Account()
    const lamports = 10*1000000000
    connection.requestAirdrop(account.publicKey, lamports).then(()=>{
    
    
        console.log("airdrop done")
        testMemo(connection, account)
    });
}
main()
  • 先看main()函数:
    • 通过solana_web3.Connection()创建了到Solana devnet的连接;
    • 通过solana_web3.Account()创建了一个新账户,后面将要用该账户与Solana集群进行交互;
    • 通过connection.requestAirdrop()从devnet水龙头申请10SOL,用于支付后面调用Solana合约的费用;
    • SOL申请之后,则会调用testMemo(connection, account),正式向Solana的合约发出请求。
  • 再看testMemo()函数:
    • 通过solana_web3.TransactionInstruction()创建针对具体Solana合约的请求instruction,例子中就是向该合约发送了一个字符串“cztest”;
    • 通过solana_web3.sendAndConfirmTransaction()向Solana集群发出Instrction,请求对应的合约服务;
    • 根据Solana集群的响应做出相应的处理:如果成功,就输出"done";如果失败,则输出错误日志。
  • 我们最后看看sendAndConfirmTransaction()函数中最后一个参数对象的两个成员:
    • skipPreflight,就是配置该笔交易在Solana接入节点提交它之前,是否要做预检查;缺省是要检的。
    • commitment,它则用来配置该笔交易希望Solana集群在多少个区块后,用户就认为该笔交易成功。

其中commitment可选的值,有以下几种:

export type Commitment =
    | 'processed'
    | 'confirmed'
    | 'finalized'
    | 'recent'
    | 'single'
    | 'singleGossip'
    | 'root'
    | 'max';

注:我们基于C/C++ 来实现一个访问Solana合约的客户端,也可以参考该Web3.js的Dapp的处理逻辑与流程的。

合约响应结果获取

除去去读取相应数据accounts,是否有类似Solidity合约的Event获取?该Event是否支持push模式,直接向客户端及时告知event的发生?

其实我们在前面 Solana JSON RPC API里已罗列了WebSocket的接口,这些接口,是实现了实时event的监听接口的。

所以我们只要按照标准的WebSocket连接,对关心的topic,进行sub,就可以获取到由Solana推过来的event的。具体的Javascript客户端通过WebSocket实时获取事件日志的步骤如下:

  1. 安装WebSocket相关的库包:
下载包
npm install --save express

npm install websocket

如果下载这监听的话:reconnecting  就会自带心跳机制不需从新加
(缺点只能TypeScript中使用)
npm install --save reconnecting-websocket
  1. JavaScript通过WebSocket访问Solana:
<html>

<head>
    <!--加入下面这行代码避免出现中文乱码-->
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>

<body>
<div id="result"></div>
</body>
<script type="text/javascript">
	//想要监听的地址
    var socketUrl = `ws://api.devnet`;
    var socket = new WebSocket(socketUrl);
    //连接打开事件
    //订阅日志事件logsSubscribe
    //mentions这个参数相当于(交易号)
    socket.onopen = function() {
      
      
        console.log("Socket 已打开");
        let rpc = {
      
      
            jsonrpc: "2.0",
            id: 1,
            method: "logsSubscribe",
            "params": [
                {
      
      
                    "mentions": [ "Ef2FSTK4Jk7Yc3AiKoWhDrhdPdATTxPAngRRNSgDTdEY" ]
                },
                {
      
      
                    "commitment": "finalized"
                }
            ]
            //这个属于监听全部信息
            //"params": [ "all" ]
        }
        socket.send(JSON.stringify(rpc));
    };
    //收到消息事件
    socket.onmessage = function(msg) {
      
      
        console.log("接受到消息:" + msg.data);
        var result = document.getElementById("result").innerHTML;
        result = result + "<br/>接收消息:" + msg.data;
        document.getElementById("result").innerHTML = result;
    };
    //连接关闭事件
    socket.onclose = function() {
      
      };
    //发生了错误事件
    socket.onerror = function() {
      
      
        console.log("发生错误!");
    }
</script>

</html>

这个例子,就是会去监听Solana上,是否有关于账户“Ef2FSTK4Jk7Yc3AiKoWhDrhdPdATTxPAngRRNSgDTdEY”的事情,或日志,有的话,它就会接收,并打印到Web的HTML页面中。

猜你喜欢

转载自blog.csdn.net/DongAoTony/article/details/124505598