草庐IT

Solana之旅5:Web3访问

DongAoTony 2023-04-21 原文

客户端

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),接收客户端发送过来的消息,将消息解包,并调用本地的方法。

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

该流程中的具体步骤是:

  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页面中。

有关Solana之旅5:Web3访问的更多相关文章

  1. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

    类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

  2. ruby-on-rails - 在混合/模块中覆盖模型的属性访问器 - 2

    我有一个包含模块的模型。我想在模块中覆盖模型的访问器方法。例如:classBlah这显然行不通。有什么想法可以实现吗? 最佳答案 您的代码看起来是正确的。我们正在毫无困难地使用这个确切的模式。如果我没记错的话,Rails使用#method_missing作为属性setter,因此您的模块将优先,阻止ActiveRecord的setter。如果您正在使用ActiveSupport::Concern(参见thisblogpost),那么您的实例方法需要进入一个特殊的模块:classBlah

  3. ruby - 续集在添加关联时访问many_to_many连接表 - 2

    我正在使用Sequel构建一个愿望list系统。我有一个wishlists和itemstable和一个items_wishlists连接表(该名称是续集选择的名称)。items_wishlists表还有一个用于facebookid的额外列(因此我可以存储opengraph操作),这是一个NOTNULL列。我还有Wishlist和Item具有续集many_to_many关联的模型已建立。Wishlist类也有:selectmany_to_many关联的选项设置为select:[:items.*,:items_wishlists__facebook_action_id].有没有一种方法可以

  4. ruby - 有没有办法从 ruby​​ case 语句中访问表达式? - 2

    我想从then子句中访问c​​ase语句表达式,即food="cheese"casefoodwhen"dip"then"carrotsticks"when"cheese"then"#{expr}crackers"else"mayo"end在这种情况下,expr是食物的当前值(value)。在这种情况下,我知道,我可以简单地访问变量food,但是在某些情况下,该值可能无法再访问(array.shift等)。除了将expr移出到局部变量然后访问它之外,是否有直接访问caseexpr值的方法?罗亚附注我知道这个具体示例很简单,只是一个示例场景。 最佳答案

  5. ruby - 从外部访问类的实例变量 - 2

    我理解(我认为)Ruby中类变量和类的实例变量之间的区别。我想知道如何从该类外部访问该类的实例变量。从内部(即在类方法中而不是实例方法中),它可以直接访问,但是从外部,有没有办法做MyClass.class.[@$#]variablename?我没有任何具体原因要这样做,只是学习Ruby并想知道是否可行。 最佳答案 classMyClass@my_class_instance_var="foo"class上述yield:>>foo我相信Arkku演示了如何从类外部访问类变量(@@),而不是类实例变量(@)。我从这篇文章中提取了上述内

  6. ruby-on-rails - 使用 HTTP.get_response 检索 Facebook 访问 token 时出现 Rails EOF 错误 - 2

    我试图在我的网站上实现使用Facebook登录功能,但在尝试从Facebook取回访问token时遇到障碍。这是我的代码:ifparams[:error_reason]=="user_denied"thenflash[:error]="TologinwithFacebook,youmustclick'Allow'toletthesiteaccessyourinformation"redirect_to:loginelsifparams[:code]thentoken_uri=URI.parse("https://graph.facebook.com/oauth/access_token

  7. ruby - 如何配置 Ruby Mechanize 代理以通过 Charles Web 代理工作? - 2

    我正在使用Ruby/Mechanize编写一个“自动填写表格”应用程序。它几乎可以工作。我可以使用精彩CharlesWeb代理以查看服务器和我的Firefox浏览器之间的交换。现在我想使用Charles查看服务器和我的应用程序之间的交换。Charles在端口8888上代理。假设服务器位于https://my.host.com。.一件不起作用的事情是:@agent||=Mechanize.newdo|agent|agent.set_proxy("my.host.com",8888)end这会导致Net::HTTP::Persistent::Error:...lib/net/http/pe

  8. ruby - 使用 Class.new 时访问外部范围 - 2

    是否有可能以某种方式访问​​Class.new范围内的a?a=5Class.new{defb;aend}.new.b#NameError:undefinedlocalvariableormethod`a'for#:0x007fa8b15e9af0>#:in`b' 最佳答案 即使@MarekLipka的回答是正确的——改变变量范围总是有风险的。这是可行的,因为每个block都带有创建它的上下文,因此您的局部变量a突然变得不那么局部了——它变成了一个“隐藏的”全局变量:a=5object=Class.new{define_method(

  9. ruby - 使用访问器方法即时创建对象 - 2

    使用散列定义的访问器方法动态创建对象的最简单方法是什么?例如,如果我有一个散列:{foo:"Foo",bar:"Bar"}我想要一个具有访问器方法foo、foo=、bar和bar=的对象,其初始值分别为"Foo"和"Bar"。我可以想到这样做:moduleObjectWithAccessordefself.newh;Struct.new(*h.keys).new(*h.values)endendo=ObjectWithAccessor.new(foo:"Foo",bar:"Bar")o.foo#=>"Foo"但是,我不需要它们的多个实例具有相同的特定键集,而是希望每次都使用可能不同的键

  10. ruby - 404 未找到,但可以从网络浏览器正常访问 - 2

    我在这方面尝试了很多URL,在我遇到这个特定的之前,它们似乎都很好:require'rubygems'require'nokogiri'require'open-uri'doc=Nokogiri::HTML(open("http://www.moxyst.com/fashion/men-clothing/underwear.html"))putsdoc这是结果:/Users/macbookair/.rvm/rubies/ruby-2.0.0-p481/lib/ruby/2.0.0/open-uri.rb:353:in`open_http':404NotFound(OpenURI::HT

随机推荐