客户端
Json RPC API
RPC
RPC(Remote Procedure Calls )远程过程调用是一种协议,就是从一台机器(客户端)上通过参数传递的方式调用另一台机器(服务器)上的一个函数或方法(可以统称为服务)并得到返回的结果。
RPC协议通常的实现有XML-RPC , JSON-RPC ,gRPC等,它们的通信方式基本相同, 所不同的只是传输数据的格式。
RPC是分布式架构的核心,按响应方式分如下两种:
- 同步调用:客户端调用服务方方法,等待直到服务方返回结果或者超时,再继续自己的操作。
- 异步调用:客户端把消息发送给中间件,不再等待服务端返回,直接继续自己的操作。
一个完整的RPC架构里面包含了四个核心的组件,分别是Client,Client Stub,Server以及Server Stub:
- 客户端(Client),服务的调用方。
- 客户端存根(Client Stub),存放服务端的地址消息,再将客户端的请求参数打包成网络消息,然后通过网络远程发送给服务方。
- 服务端(Server),真正的服务提供者。
- 服务端存根(Server Stub),接收客户端发送过来的消息,将消息解包,并调用本地的方法。
RPC的调用流程如下图所示:
该流程中的具体步骤是:
- 服务调用方(client)(客户端)以本地调用方式调用服务。
- client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;在Java里就是序列化的过程。
- client stub找到服务地址,并将消息通过网络发送到服务端。
- server stub收到消息后进行解码,在Java里就是反序列化的过程。
- server stub根据解码结果调用本地的服务。
- 本地服务执行处理逻辑。
- 本地服务将结果返回给server stub。
- server stub将返回结果打包成消息,主要也是Java里的序列化过程。
- server stub将打包后的消息通过网络并发送至消费方。
- client stub接收到消息,并进行解码, Java里的反序列化。
- 服务调用方(client)得到最终结果。
RPC框架的目标,就是要上面步骤里2~10给封装好,让用户像调用本地服务一样的调用远程服务,实现对客户端(调用方)透明化服务。这个听起来好像不难,但真正落地实现,就要面对以下几个难题:
- 通讯问题 : 主要是通过在客户端和服务器之间建立TCP/UDP连接,远程过程调用的所有交换的数据都在这个连接里传输;TCP连接可以是按需连接,调用结束后就断掉,也可以是长连接,多个远程过程调用共享同一个连接。
- 寻址问题: A服务器上的应用怎么告诉底层的RPC框架,如何连接到B服务器(如主机或IP地址)以及特定的端口,方法的名称是什么,这样才能完成调用。比如基于Web服务协议栈的RPC,就要提供一个endpoint URI。
- 序列化与反序列化 : 当A服务器上的应用发起远程过程调用时,方法的参数需要通过底层的网络协议如TCP传递到B服务器,由于网络协议是基于二进制的;内存中的参数的值要序列化成二进制的形式,也就是序列化(Serialize)或编组(marshall),再发送给B服务器;B服务器接收参数要将参数反序列化;同理,B服务器应用调用自己的方法处理后返回的结果也要序列化给A服务器,A服务器接收也要经过反序列化的过程。
Json RPC
像以太坊等主流区块链实现的RPC,都是基于Json RPC的。目前的版本是V2.0。
- JSON-RPC是一个无状态且轻量级的远程过程调用(RPC)协议实现规范。
- 它主要定义了一些数据结构及其相关的处理规则。
- 它允许运行在基于socket,http等诸多不同消息传输环境的同一进程中。
- 它使用JSON(RFC 4627)作为数据格式,这是它最大的特点。
- JSON支持4种基本类型
- String
- Numbers
- Booleans
- Null
- JSON还支持两种结构化类型:
- 上述数据类型的第一个字母必须大写;客户端与服务端之间交换的成员名字,也是区分大小写的。
- 函数、方法、过程的称谓在该规范里是可互换的。
- 客户端:定义为请求对象的来源及响应对象的处理程序。
- 服务端:定义为响应对象的起源和请求对象的处理程序。
- 一个请求对象包括以下成员:
- jsonrpc:指定JSON-RPC协议版本的字符串,必须准确写为“2.0”;
- method:包含所要调用方法名称的字符串;
- params:调用方法所需要的结构化参数值,该成员参数可以被省略;
- id:已建立客户端的唯一标识id,值必须包含一个字符串、数值或NULL空值;如果不包含该成员则被认定为是一个通知。
下面就是一个请求对象的例子
{"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1}
- 通知:没有包含“id”成员的请求对象为通知, 作为通知的请求对象表明客户端对相应的响应对象并不感兴趣,本身也没有响应对象需要返回给客户端;
- 参数结构:rpc调用如果存在参数则必须为基本类型或结构化类型的参数值,要么为索引数组,要么为关联数组对象。
- 索引:参数必须为数组,并包含与服务端预期顺序一致的参数值;
- 关联名称:参数必须为对象,并包含与服务端相匹配的参数成员名称;没有在预期中的成员名称可能会引起错误。名称必须完全匹配,包括方法的预期参数名以及大小写。
- 响应对象也会是一个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调用的。
- Solana实现了基于HTTP的RPC API:
- 默认端口:8899
- 节点访问:例如http://localhost:8899
- Solana也实现了若干基于WebSocket API:
- 默认端口:8900
- 节点访问:例如ws://localhost:8900
- 下面是已实现的HTTP API
- getAccountInfo:查询账号信息
- getBalance:查询账号余额
- getBlock:查询区块数据
- getBlockHeight:查询区块高度
- getBlockProduction:查询区块生产信息
- getBlockCommitment:查询区块提交信息
- getBlocks:查询区块集
- getBlocksWithLimit:查询指定区间内的区块
- getBlockTime:查询区块时间
- getClusterNodes:查询集群节点
- getEpochInfo:查询周期信息
- getEpochSchedule:查询周期计划
- getFeeCalculatorForBlockhash:查询指定区块的费率计算器
- getFeeRateGovernor:查询费率治理人
- getFees:查询费率
- getFirstAvailableBlock:查询第一个有效区块
- getGenesisHash:查询创世哈希
- getHealth:查询健康状态
- getIdentity:查询实体标识
- getInflationGovernor:查询通胀治理人
- getInflationRate:查询通胀率
- getInflationReward:查询通胀奖励
- getLargestAccounts:查询最大账号
- getLeaderSchedule:查询主导人计划表
- getMaxRetransmitSlot:查询最大重发槽位
- getMaxShredInsertSlot:查询最大插入槽位
- getMinimumBalanceForRentExemption:查询可豁免租金的最小余额
- getMultipleAccounts:查询多个账号
- getProgramAccounts:查询程序账号
- getRecentBlockhash:查询最近的区块哈希
- getRecentPerformanceSamples:查询最近的性能样本
- getSnapshotSlot:获取快照槽位
- getSignaturesForAddress:获取地址签名
- getSignatureStatuses:获取签名状态
- getSlot:查询槽位
- getSlotLeader:查询槽位主导人
- getSlotLeaders:查询槽位主导人集合
- getStakeActivation:查询抵押激活信息
- getSupply:查询供应量
- getTokenAccountBalance:查询通证账号余额
- getTokenAccountsByDelegate:按代表查询通证账号
- getTokenAccountsByOwner:按持有人查询通证账号
- getTokenLargestAccounts:查询通证的最大账号
- getTokenSupply:查询通证供应量
- getTransaction:查询交易
- getTransactionCount:查询交易数量
- getVersion:查询版本
- getVoteAccounts:查询投票账号
- minimumLedgerSlot:最小账本槽位
- requestAirdrop:请求空投
- sendTransaction:发送交易
- simulateTransaction:模拟交易
- 下面则是已实现的WebSocket API
- accountSubscribe:订阅账号事件
- accountUnsubscribe:取消订阅账号事件
- logsSubscribe:订阅日志事件
- logsUnsubscribe:取消订阅日志事件
- programSubscribe:订阅程序事件
- programUnsubscribe:取消订阅程序事件
- signatureSubscribe:订阅签名事件
- signatureUnsubscribe:取消订阅签名事件
- slotSubscribe:订阅槽位事件
- slotUnsubscribe:取消订阅槽位事件
Web3 Javascript API
- Solana为Javascript创建了Web3.js包:@solana/web3.js。
- 下面我们直接来看一段样例代码:
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实时获取事件日志的步骤如下:
- 安装WebSocket相关的库包:
下载包
npm install --save express
npm install websocket
如果下载这监听的话:reconnecting 就会自带心跳机制不需从新加
(缺点只能TypeScript中使用)
npm install --save reconnecting-websocket
- 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);
socket.onopen = function() {
console.log("Socket 已打开");
let rpc = {
jsonrpc: "2.0",
id: 1,
method: "logsSubscribe",
"params": [
{
"mentions": [ "Ef2FSTK4Jk7Yc3AiKoWhDrhdPdATTxPAngRRNSgDTdEY" ]
},
{
"commitment": "finalized"
}
]
}
socket.send(JSON.stringify(rpc));
};
socket.onmessage = function(msg) {
console.log("接受到消息:" + msg.data);
var result = document.getElementById("result").innerHTML;
result = result + "
接收消息:" + msg.data;
document.getElementById("result").innerHTML = result;
};
socket.onclose = function() {};
socket.onerror = function() {
console.log("发生错误!");
}
script>
html>
这个例子,就是会去监听Solana上,是否有关于账户“Ef2FSTK4Jk7Yc3AiKoWhDrhdPdATTxPAngRRNSgDTdEY”的事情,或日志,有的话,它就会接收,并打印到Web的HTML页面中。