注于Java领域优质技术,欢迎关注
首先了解什么叫RPC,为什么要RPC,RPC是指远程过程调用,也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。
比如说,一个方法可能是这样定义的:
Employee getEmployeeByName(String fullName)
那么:
(图片来源:https://www.cs.rutgers.edu/~pxk/417/notes/03-rpc.html)
为什么RPC呢?就是无法在一个进程内,甚至一个计算机内通过本地调用的方式完成的需求,比如比如不同的系统间的通讯,甚至不同的组织间的通讯。由于计算能力需要横向扩展,需要在多台机器组成的集群上部署应用,
RPC的协议有很多,比如最早的CORBA,Java RMI,Web Service的RPC风格,Hessian,Thrift,甚至Rest API。
而Netty框架不局限于RPC,更多的是作为一种网络协议的实现框架,比如HTTP,由于RPC需要高效的网络通信,就可能选择以Netty作为基础。除了网络通信,RPC还需要有比较高效的序列化框架,以及一种寻址方式。如果是带会话(状态)的RPC调用,还需要有会话和状态保持的功能。
大体上来说,Netty就是提供一种事件驱动的,责任链式(也可以说是流水线)的网络协议实现方式。网络协议包含很多层次,很多部分组成,如传输层协议,编码解码,压缩解压,身份认证,加密解密,请求的处理逻辑,怎么能够更好的复用,扩展,业界通用的方法就是责任链,
一个请求应答网络交互通常包含两条链,一条链(Upstream)是从传输层,经过一系列步骤,如身份认证,解密,日志,流控,最后到达业务层,一条链(DownStream)是业务层返回后,又经过一系列步骤,如加密等,又回到传输层。
(图片来源:ChannelPipeline (The Netty Project API Reference (3.2.6.Final)))
这样每一层都有一个处理接口,都可以进行不同的操作,比如身份认证,加解密,日志,流控,将不同的处理实现像拼积木那样插接起来就可以实现一个网络协议了(快速开发)。每一层都有自己的实现,上层不需要关注面向网络的操作(可维护)。Netty已经提供了很多实现。
(图片来源:http://docs.jboss.org/netty/3.1/guide/html/architecture.html)
当然Netty还有许多好处,比如对非阻塞IO(NIO)的支持,比如在链上传递时最大程度的减少buffer的copy(高性能)。
面试的时候经常被问到RPC相关的问题,例如:你说说RPC实现原理、让你实现一个RPC框架应该考虑哪些地方、RPC框架基础上发起一个请求是怎样一个流程等等。所以这次我就总结一波RPC的相关知识点,提前说明一下,本篇文章只是为了回答一些面试问题,所以只是解释原理,并不会深入挖掘细节。
RPC(Remote Procedure Call)翻译成中文就是$\color{red}{远程过程调用}$。RPC框架起到的作用就是为了实现,调用远程方法时,能够做到和调用本地方法一样,让开发人员更专注于业务开发,不用去考虑网络编程等细节。
首先我们区分两个角色一个服务提供方,一个是服务调用方。服务调用方其实是通过动态代理、负载均衡、网络调用等机制去服务提供方的机器上去执行对应的方法。服务提供方将方法执行完成后,将执行结果再通过网络传输返回到服务提供方。
大致过程如下:
但是现在的服务都是集群部署,那么服务调用方怎么应该实时的知道服务提供方的集群中的变化,例如服务提供方的IP地址变了,或者是服务重启时怎么能够及时的切换流量呢?
这就需要$\color{red}{注册中心}$起作用了,我们可以把注册中心看作服务端,然后每个服务都看成客户端,每个客户端都需要将自己注册到注册中心,然后一个服务调用方要调用另一个服务时,需要从注册中心获取服务提供方的信息,主要是获取服务提供方的服务器IP地址列表和端口信息。
服务调用方获取到这些信息后缓存到自己本地,并且跟注册中心保持一个长连接当服务提供方有任何变化时,注册中心能够实时的通知给服务调用方,调用方能够及时更新自己本地缓存的信息(也可以采用定时轮询的方式)。
服务调用方获取到服务器IP地址信息后,根据自己的负载均衡策略选择一个IP地址然后发起网络调用的请求。
可以自己使用JDK原生的BIO活NIO来实现一套网络通信模块,但是这里我们建议直接使用强大的网络通信框架Netty。它是基于NIO的网络通信框架,支持高并发,封装完善,而且性能好传输快。
Netty不是我们本文的主要内容,这里就不展开说了。
因为我们知道数据在网络中传输的时候都是以二进制的形式的,所以在调用方将调用的参数进行传递的时候是需要进行序列化的。服务提供方在接收到参数时也是需要进行反序列化的。
调用方既然需要序列化,服务提供方又要进行反序列化,这样双方就要确定好一个协议,调用方传输什么参数,服务提供方就按照这个协议去进行解析,而且在返回结果的时候也是按照这个协议进行结果解析。
那么这个协议应该是怎么样的结构,都是什么样子的呢?
因为这个协议可以自定义,我们为了方便就以JSON的形式给举个例子:
{
"interfaces": "interface=com.jimoer.rpc.test.producer.TestService;method=printTest;parameter=com.jiomer.rpc.test.producer.TestArgs",
"requestId": "3",
"parameter": {
"com.jiomer.rpc.test.producer.TestArgs": {
"age": 20,
"name": "Jimoer"
}
}
}
首先第一个参数interfaces是,我们要让服务提供方知道调用方要调用哪个接口,以及接口中的哪个方法,并且方法的参数是什么类型的。
第二个参数是当前一次请求的一个唯一标识,在多个线程同时请求一个方法时,用这个id来进行区分,以后无论是做链路追踪还是日志管理都可以以此id为依据。
第三个参数就是 实际的调用方法中的参数值。具体是什么类型的,每个属性值都是什么。
下面也是举一个简单的例子来说明一下调用的过程。我们一部分采用代码的形式一部分采用文字的形式来将整个调用过程串起来。
// 定义请求的URL
String tcpURL = "tcp://testProducer/TestServiceImpl";
// 定义接口请求
TestService testService = ProxyFactory.create(TestService.class, tcpURL);
// 组装请求参数
TestArgs testArgs = new TestArgs(20,"Jimoer");
// 通过动态代理执行请求
String result = testService.printTest(testArgs);
通过查看上面的代码我们可以看到整个调用过程最核心的地方在ProxyFactory.create()方法里,这个方法里面主要的过程是,动态代理生成接口的实际代理对象,然后使用Netty的接口发起网络请求。
Proxy.newProxyInstance(getClass().getClassLoader(), interfaces.getClass().getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 第一步:获取调用服务的地址列表
ListregistryInfos = interfacesMethodRegistryList.get(clazz);
if (registryInfos == null) {
throw new RuntimeException("无法找到服务提供者");
}
// 第二步: 通过自身的负载均衡策略选择一个地址
RegistryInfo registryInfo = loadBalancer.choose(registryInfos);
// 第三步:Netty的网络请求处理
ChannelHandlerContext ctx = channels.get(registryInfo);
// 第四步:根据接口类的全路径名和方法生成唯一标识
String identify = InvokeUtils.buildInterfaceMethodIdentify(clazz, method);
String requestId;
// 第五步:通过加锁的方式保证生成的requestId的唯一性
synchronized (ApplicationContext.this) {
requestIdWorker.increment();
requestId = String.valueOf(requestIdWorker.longValue());
}
// 第六步: 组织参数
JSONObject jsonObject = new JSONObject();
jsonObject.put("interfaces", identify);
jsonObject.put("parameter", param);
jsonObject.put("requestId", requestId);
System.out.println("发送给服务端JSON为:" + jsonObject.toJSONString());
// $$ 多条消息之间的分隔符
String msg = jsonObject.toJSONString() + "$$";
ByteBuf byteBuf = Unpooled.buffer(msg.getBytes().length);
byteBuf.writeBytes(msg.getBytes());
// 第七步:这里发起调用
ctx.writeAndFlush(byteBuf);
// 这里会将线程进行阻塞,知道服务提供方将请求处理好之后返回结果,再唤醒。
waitForResult();
return result;
}
});
执行过程大致分为这几步:
上面也说了,服务调用方发起网络请求后,会阻塞住,直到服务提供方返回数据,所以服务提供方处理完调用方法的逻辑后,还是要唤醒阻塞的调用线程的。
服务提供方在处理请求时也是先通过Netty获取到数据,然后再进行反序列化,然后再根据协议获取到需要调用的方法,然后通过反射去进行调用。
Netty的返回入口在下面这部分逻辑里
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
try {
String message = (String) msg;
if (messageCallback != null) {
// 将接收到的消息放到回调方法中
messageCallback.onMessage(message);
}
} finally {
ReferenceCountUtil.release(msg);
}
}
Netty的client接收到响应的消息后,先将结果返回到调用方,处理完成之后再去释放之前的阻塞调用线程。
client.setMessageCallback(message -> {
// 这里收单服务端返回的消息,先压入队列
RpcResponse response = JSONObject.parseObject(message, RpcResponse.class);
System.out.println("收到一个响应:" + response);
String interfaceMethodIdentify = response.getInterfaceMethodIdentify();
String requestId = response.getRequestId();
// 设定唯一标识
String key = interfaceMethodIdentify + "#" + requestId;
Invoker invoker = inProgressInvoker.remove(key);
// 将结果设置到代理对象中
invoker.setResult(response.getResult());
// 加锁再释放之前的阻塞线程。
synchronized (ApplicationContext.this) {
ApplicationContext.this.notifyAll();
}
});
setResult()方法
@Override
public void setResult(String result) {
synchronized (this) {
this.result = JSONObject.parseObject(result, returnType);
notifyAll();
}
}
上面的步骤就是这样,按照之前请求的唯一标识放入到返回的信息中,然后将结果设置到代理对象中,再通过返回结果,然后唤醒之前的调用阻塞线程。
其实整个RPC的请求过程就是如下(不含异步调用):
做一个总结,用大白话把一个RPC请求流程描述出来:
首先无论是调用方还是服务提供方都要注册到注册中心;
这样就完成了一次RPC网络调用,其实后面框架扩展后,还要考虑限流、熔断、服务降级、序列化多样性扩展,服务监控、链路追踪等等功能。这些就要后面再扩展的讲了,这次就先到这了。
原文链接:https://www.cnblogs.com/jimoer/p/15511954.html
景:今年3月开放联盟链“中移链”在区块链服务网络(BSN[1])中完成适配并上线发布,吸引了大批开发者,部分开发者提出了一些共性问题
目的:本篇文章是为了让读者了解如何在中移链(基于EOS)上调用RPC接口组装交易、签名、上链以及查询上链结果
适用对象:适用于BSN开放联盟链--中移链(基于EOS)开发者
•需要现在bsn[2]创建项目和创建一个账户,可以参考:https://bsnbase.com/static/tmpFile/bzsc/openper/7-3-6.html
•可以获取到eos 端点:https://opbningxia.bsngate.com:18602/api/aecb28acfd154cfeb90d0b6a8ecab1e7/rpc •eos账户/合约地址:helailiang14
•为账户购买足够的资源(cpu、net、ram)
•安装EOSIO开发环境:需要安装cleos和keosd, 可以参考:https://developers.eos.io/welcome/latest/getting-started-guide/local-development-environment/installing-eosio-binaries •部署eos合约: hello.cpp
#include <eosio/eosio.hpp>
#include <eosio/transaction.hpp>
using namespace eosio;
// 通过[[eosio::contract]]标注这个类是一个合约
class [[eosio::contract]] hello : public contract
{
public:
using contract::contract;
// 在构造函数进行表对象的实例化, 标准合约构造函数,receiver也就是我们的合约账号(一般情况下),code就是我们的action名称,ds就是数据流
// get_self() 合约所在的账号 get_code() 当前交易请求的action方法名 get_datastream() 当前数据流
hello(name receiver, name code, datastream<const char *> ds) : contract(receiver, code, ds), friend_table(get_self(), get_self().value)
{
}
// 用[[eosio::action]]标注这个方法是一个合约action就行
// 注意:action的名称要求符合name类型的规则
[[eosio::action]] void hi(name user)
{
print("Hello, ", user);
print("get_self,", get_self().value);
// print("get_code,", get_code().value);
uint32_t now = current_time_point().sec_since_epoch();
auto friend_itr = friend_table.find(user.value);
// 数据不存在
if (friend_itr == friend_table.end())
{
// 第一个参数就是内存使用的对象,第二个参数就是添加表对象时的委托方法。
friend_table.emplace(get_self(), [&](auto &f) {
f.friend_name = user;
f.visit_time = now;
});
}
else
{
// 第一个参数是传递需要修改的数据指针,第二个参数是内存使用的对象,第二个参数就是表对象修改时的委托方法
friend_table.modify(friend_itr, get_self(), [&](auto &f) {
f.visit_time = now;
});
}
}
[[eosio::action]] void nevermeet(name user)
{
print("Never see you again, ", user);
auto friend_itr = friend_table.find(user.value);
check(friend_itr != friend_table.end(), "I don't know who you are.");
// 只有一个参数,就是要删除的对象指针
friend_table.erase(friend_itr);
}
[[eosio::action]] void meetagain()
{
uint32_t now = current_time_point().sec_since_epoch();
auto time_idx = friend_table.get_index<"time"_n>();
auto last_meet_itr = time_idx.begin();
check(last_meet_itr != time_idx.end(), "I don't have a friend.");
time_idx.modify(last_meet_itr, get_self(), [&](auto &f) {
f.visit_time = now;
});
}
private:
// 定义一个结构体,然后用[[eosio::table]]标注这个结构体是一个合约表。在结构体里定义一个函数名primary_key,返回uint64_t类型,作为主键的定义
struct [[eosio::table]] my_friend
{
name friend_name;
uint64_t visit_time;
uint64_t primary_key() const { return friend_name.value; }
double by_secondary() const { return -visit_time; }
};
// 定义表名和查询索引 "friends"_n就是定义表名,所以使用了name类型,之后my_friend是表的结构类
typedef eosio::multi_index<"friends"_n, my_friend> friends;
friends friend_table;
};
curl -X POST 'https://opbningxia.bsngate.com:18602/api/{您的开放联盟链项目ID/rpc/v1/chain/abi_json_to_bin' \
-d '{
"code": "helailiang14",
"action": "hi",
"args": {
"user": "helloworld"
}
}'
------------------
return
{"binargs":"00408a97721aa36a"}
curl GET 'https://opbningxia.bsngate.com:18602/api/{您的开放联盟链项目ID/rpc/v1/chain/get_info'
------------------
return
{
"server_version":"11d35f0f",
"chain_id":"9b4c6015f8b73b2d7ee3ebd92d249a1aba06a614e9990dcf54f7cf2e3d5172e1",
"head_block_num":15134328,
"last_irreversible_block_num":15134262,
"last_irreversible_block_id":"00e6ee360b5e7680a526ddea45db1be15c4be2cd2389020688218fe765be6db7",
"head_block_id":"00e6ee7889523875a28284effecdd1199cc960adb14c14c36cd1bd52afed6824",
"head_block_time":"2022-04-27T09:08:08.500",
"head_block_producer":"prod.b",
"virtual_block_cpu_limit":200000000,
"virtual_block_net_limit":1048576000,
"block_cpu_limit":199900,
"block_net_limit":1048576,
"server_version_string":"v3af0a20",
"fork_db_head_block_num":15134328,
"fork_db_head_block_id":"00e6ee7889523875a28284effecdd1199cc960adb14c14c36cd1bd52afed6824",
"server_full_version_string":"v3af0a20",
"last_irreversible_block_time":"2022-04-27T09:07:35.500"
}
获取到head_block_num : 15134328
获取 head_block_id:00e6ee7889523875a28284effecdd1199cc960adb14c14c36cd1bd52afed6824
获取 chain_id: 9b4c6015f8b73b2d7ee3ebd92d249a1aba06a614e9990dcf54f7cf2e3d5172e1
curl -X POST 'https://opbningxia.bsngate.com:18602/api/{您的开放联盟链项目ID/rpc/v1/chain/get_block' \
-d '{
"block_num_or_id": "15130610"
}'
------------------
return
{
"timestamp": "2022-04-27T09:08:08.500",
"producer": "prod.b",
"confirmed": 0,
"previous": "00e6ee77f2655528739622d2c9235026d4f10138b9821e46ea35165cb086d12d",
"transaction_mroot": "0000000000000000000000000000000000000000000000000000000000000000",
"action_mroot": "665584b582b234bf58d3708b31da20e14d266713e3bc6ce79ea3187cc2ffa5a4",
"schedule_version": 2,
"new_producers": null,
"producer_signature": "SIG_K1_KiYCDLMgE6gE1nNqQQL2jEEF3VVd6iaspAePvvJMjKwgg2Yf6GiTYcznrkymAdtZUAUFh28N8r9RzX936cASKDB6JW6ga3",
"transactions": [],
"id": "00e6ee7889523875a28284effecdd1199cc960adb14c14c36cd1bd52afed6824",
"block_num": 15134328,
"ref_block_prefix": 4018438818
}
获取到timestamp : 2022-04-27T09:08:08
使用EOSIO提供的签名工具实现签名:启动keosd后,才会提供签名服务
curl -X POST POST 'http://192.168.1.46:8800/v1/wallet/sign_transaction' \
-d '[
{
"expiration": "2022-04-27T10:08:08",
"ref_block_num": 61048,
"ref_block_prefix": 4018438818,
"max_net_usage_words": 0,
"max_cpu_usage_ms": 0,
"delay_sec": 0,
"context_free_actions": [],
"actions": [
{
"account": "helailiang14",
"name": "hi",
"authorization": [
{
"actor": "helailiang14",
"permission": "active"
}
],
"data": "00408a97721aa36a"
}
],
"transaction_extensions": [],
"signatures": [],
"context_free_data": []
},
[
"EOS6F6PRkSaPyijTDBYskFbsxpGz53JMTFbEhua94fQEyf7pAMc7Y"
],
"9b4c6015f8b73b2d7ee3ebd92d249a1aba06a614e9990dcf54f7cf2e3d5172e1"
]'
------------------
return :
{
"expiration":"2022-04-27T10:08:08",
"ref_block_num":61048,
"ref_block_prefix":4018438818,
"max_net_usage_words":0,
"max_cpu_usage_ms":0,
"delay_sec":0,
"context_free_actions":[
],
"actions":[
{
"account":"helailiang14",
"name":"hi",
"authorization":[
{
"actor":"helailiang14",
"permission":"active"
}
],
"data":"00408a97721aa36a"
}
],
"transaction_extensions":[
],
"signatures":[
"SIG_K1_K2AzV2Pk4SP3PQhcdQ1bYgGZgr7PUUcJkAGowvncFV1ngrZufeCQpveAUBRYvNA5uyxFk2hKiot3Mu7FCW5rqqeoU5SVTo"
],
"context_free_data":[
]
}
•expiration 过期时间。这里将timestamp加上了60分钟.•ref_block_num: 由第二步返回的head_block_id 16进制的 然后字段截取前八位低4位 转10进制, ee78 ==> 61048•ref_block_prefix:由第二步返回的head_block_id 16进制的 然后字段截取16到24 然后两个字节反转(a28284ef反转为ef8482a2) 转10进制, ef8482a2 ==> 4018438818•account 合约名称,即部署合约的账户•name 调用的合约方法。•actor 调用者。签名者•data :第一步生成的bin字符串•permission 使用的权限类型•EOS6F6PRkSaPyijTDBYskFbsxpGz53JMTFbEhua94fQEyf7pAMc7Y :签署此交易的公钥。实际上是由钱包中对应的私钥来签•9b4c6015f8b73b2d7ee3ebd92d249a1aba06a614e9990dcf54f7cf2e3d5172e1: 是第二步获取的chain_id••signatures: 签名结果 SIG_K1_K2AzV2Pk4SP3PQhcdQ1bYgGZgr7PUUcJkAGowvncFV1ngrZufeCQpveAUBRYvNA5uyxFk2hKiot3Mu7FCW5rqqeoU5SVTo
使用 cleos convert pack_transaction将交易报文转换成 packed 格式
cleos convert pack_transaction '{
"expiration":"2022-04-27T10:08:08",
"ref_block_num":61048,
"ref_block_prefix":4018438818,
"max_net_usage_words": 0,
"max_cpu_usage_ms": 0,
"delay_sec": 0,
"context_free_actions": [],
"actions": [
{
"account": "helailiang14",
"name": "hi",
"authorization": [
{
"actor": "helailiang14",
"permission": "active"
}
],
"data": "00408a97721aa36a"
}
],
"transaction_extensions": []
}'
------------------
return:
{
"signatures": [],
"compression": "none",
"packed_context_free_data": "",
"packed_trx": "0816696278eea28284ef000000000140029bc64567a26a000000000000806b0140029bc64567a26a00000000a8ed32320800408a97721aa36a00"
}
packed_trx: 为交易报文的 packed 格式
curl -X POST 'https://opbningxia.bsngate.com:18602/api/{您的开放联盟链项目ID/rpc/v1/chain/push_transaction' \
-d '{
"signatures": [
"SIG_K1_K2AzV2Pk4SP3PQhcdQ1bYgGZgr7PUUcJkAGowvncFV1ngrZufeCQpveAUBRYvNA5uyxFk2hKiot3Mu7FCW5rqqeoU5SVTo"
],
"compression": "none",
"packed_context_free_data": "",
"packed_trx": "0816696278eea28284ef000000000140029bc64567a26a000000000000806b0140029bc64567a26a00000000a8ed32320800408a97721aa36a00"
}'
------------------
return:
{
"transaction_id":"a69d03f6b1bab4bd8908124eef5e59d3e47df4063e697a07487308cde63a9f79",
"processed":{
"id":"a69d03f6b1bab4bd8908124eef5e59d3e47df4063e697a07487308cde63a9f79",
"block_num":15136664,
"block_time":"2022-04-27T09:27:36.500",
"producer_block_id":null,
"receipt":{
"status":"executed",
"cpu_usage_us":272,
"net_usage_words":13
},
"elapsed":272,
"net_usage":104,
"scheduled":false,
"action_traces":[
{
"action_ordinal":1,
"creator_action_ordinal":0,
"closest_unnotified_ancestor_action_ordinal":0,
"receipt":{
"receiver":"helailiang14",
"act_digest":"31ff1ecb2b0b0c89911b74c7930f08ecfefbd24ba59ef30a905d44068d2d8910",
"global_sequence":15199315,
"recv_sequence":2,
"auth_sequence":[
[
"helailiang14",
4
]
],
"code_sequence":1,
"abi_sequence":1
},
"receiver":"helailiang14",
"act":{
"account":"helailiang14",
"name":"hi",
"authorization":[
{
"actor":"helailiang14",
"permission":"active"
}
],
"data":{
"user":"helloworld"
},
"hex_data":"00408a97721aa36a"
},
"context_free":false,
"elapsed":63,
"console":"Hello, helloworldget_self,7683817463629939264",
"trx_id":"a69d03f6b1bab4bd8908124eef5e59d3e47df4063e697a07487308cde63a9f79",
"block_num":15136664,
"block_time":"2022-04-27T09:27:36.500",
"producer_block_id":null,
"account_ram_deltas":[
],
"account_disk_deltas":[
],
"except":null,
"error_code":null,
"return_value_hex_data":"",
"inline_traces":[
]
}
],
"account_ram_delta":null,
"except":null,
"error_code":null
}
}
signatures: 为第4步签名的结果
packed_trx: 为第5步的报文转换结果
curl -X POST 'https://opbningxia.bsngate.com:18602/api/{您的开放联盟链项目ID/rpc/v1/chain/get_table_rows' \
-d'{
"code": "helailiang14",
"table": "friends",
"scope": "helailiang14",
"json": true
}'
------------------
return:
{
"rows":[
{
"friend_name":"helloworld",
"visit_time":1651051656
}
],
"more":false,
"next_key":"",
"next_key_bytes":""
}
数据[helloworld]已经写到table中
[1] BSN: https://bsnbase.com/
[2] bsn: https://bsnbase.com/p/home/Openalliance/projectManagement
*请认真填写需求信息,我们会在24小时内与您取得联系。