关键词:RPC、hsf、Midway、nodejs
一、前言
大家都知道,网络是由光纤传输的。各种信息被转换成二进制,通过明暗相间的光信号经过光纤进行传播。
只有二进制才能在网络中传输,不管是使用了什么协议,http还是rpc,最终都需要被序列化为二进制。
二、RPC是什么
前端页面发起HTTP请求与后端进行通信,那么后端进程间用以通信的RPC协议又是什么呢?(本文说的HTTP特指HTTP1.1)
Remote Procedure Call,远程过程调用。
RPC是解决进程间通信的一种方式。
RPC就是把拦截到的方法参数,转成可以在网络中传输的二进制,并保证服务提供方能正确地还原出语义,最终实现像调用本地一样地调用远程的目的。
为什么服务器间的通信使用RPC而不是HTTP
RPC和HTTP同属于应用层协议,为什么会有用途上的差别呢?
网上有好多答案,梳理了一下,大概如下:
1、HTTP的包冗余过多导致体积过大
HTTP是面向文本的,因此在报文中的每一个字段都是一些ASCII码串,body根据Content-Type有不同的编码。这导致HTTP的包有很多冗余的部分,又需要加入很多无用的内容,比如换行符号,回车符等,体积相较于发送同样信息的RPC大了许多。
ASCII它是一种7位编码,但它存放时必须占全一个字节,也即占用8位。
对于HTTP包来说,有多少个键值对就会有多大的头。
对比一下gRPC(RPC的一种实现)所采用的HTTP2.0协议,包的结构精简了许多,不同的位存储不同含义的信息,使得全包都能使用二进制编码。
2、HTTP协议属于无状态协议
无状态协议,客户端无法对请求和响应进行关联,每次请求都需要重新建立连接,响应完成再关闭连接。性能不高。
三、在开发中体会RPC调用
讲了这么多,好像内容跟做项目关系不大。从熟悉的地方说起吧。
1、BFF项目
最近做的前端的BFF项目,node框架是Midway,RPC框架是hsf。
简化的请求流程如下图:
刚开始接触BFF的时候发现有很多名词很陌生,RPC、泛化调用、序列化、动态代理、jar包什么的。
用node语言去编写一个hsf服务,前端如果不了解RPC一些基本知识,写起项目来就像是个打字员- -。这也是为什么想写这篇文章的原因。
那从前端开发的角度上入手,写项目到发布,前端开发都做了些什么呢?
开发到发布流程
a、调用HSF接口获得数据
项目用的Midway,语法十分像Java bean,直接看代码吧。
import { provide, hsf, inject, Context } from '@ali/midway';
import moment = require('moment');
import { HomePageRequest, TibaoQueryBffFacadeProxy, PageQueryCalendarRequest, } from '../../proxy/kbt-industry-operation-common-service-facade';
import { GatewayContextService } from '../../service/gatewayContext';
@provide()
@hsf()
export class PurchaseSalesIndex {
@inject() ctx: Context; // 上下文
@inject() gatewayContextService: GatewayContextService; // 注入网关上下文以获取登录态
@inject() tibaoQueryBffFacadeProxy: TibaoQueryBffFacadeProxy;// 访问的hsf服务代理
public async queryHomePage(params) { // PurchaseSalesIndex服务的queryHomePage方法
const gateWayUserInfoContext = this.gatewayContextService.getKIOUserInfo(params); // 登录态
const reqParams = {
...gateWayUserInfoContext
};
try {
const param = HomePageRequest.from(reqParams); // format入参
const result = await this.ctx.getProxyData(this, 'tibaoQueryBffFacadeProxy.queryHomePage', param); // 调用HSF接口获得依赖数据
// ... ... 数据处理略
return this.ctx.gatewaySuccess(curResult);// 返回结果
}
}
依赖注入
关于语法,稍微解释一下@provide、@inject装饰器就是Midway引以为傲的依赖注入设计了。他们官网写得很清楚,简而言之就是把声明的上下文变量注入到this指向的实例中,这样我们无需关心注入的东西的内部逻辑,就像真的实现了那样去调用。
config proxy
在项目的/src/config/proxy.ts
中配置了依赖的服务提供方jar包的版本号、groupID等信息
{
artifact: {
// 服务提供方的应用名称
appName: 'kbt-industry-operation',
// hsf 服务的二方包 groupId、artifactId以及版本号
groupId: 'com.XXXXX.alsc',
artifactId: 'kbt-industry-operation-common-service-facade',
version: '1.0.0.20220102-SNAPSHOT',
// 需要调用的服务名称列表
hsfServiceInterfaceNameList: [
'com.XXXXX.alsc.iop.common.service.facade.purchasesaleplatform.api.TibaoQueryBffFacade',
'com.XXXXX.alsc.iop.common.service.facade.purchasesaleplatform.api.TibaoManageBffFacade',
'com.XXXXX.alsc.iop.common.service.facade.purchasesaleplatform.api.TibaoSignManageBffFacade',
'com.XXXXX.alsc.iop.common.service.facade.purchasesaleplatform.api.TibaoSignQueryBffFacade',
'com.XXXXX.alsc.iop.common.service.facade.purchasesaleplatform.api.TibaoMerchantQueryBffFacade',
'com.XXXXX.alsc.iop.common.service.facade.purchasesaleplatform.api.TibaoMaterialBffFacade',
'com.XXXXX.alsc.iop.common.service.facade.purchasesaleplatform.api.TibaoCalendarBffFacade',
]
}
}
format入参
可以看到代码里用的HomePageRequest.from(reqParams)
先处理了一下reqParams,这个HomePageRequest是什么呢?
它位置在/src/proxy/XXX-service-facade.ts
中,是由npm run proxy
这个命令生成的
export class HomePageRequest extends Request {
formatHSF(): any {
return { $class: "com.XXXX.alsc.iop.common.service.facade.purchasesaleplatform.request.HomePageRequest", $: { ...((this && Request && Request.from ? (Request.from(this)) : this) && ((this && Request && Request.from ? (Request.from(this)) : this)).formatHSF ? ((this && Request && Request.from ? (Request.from(this)) : this)).formatHSF() : { $class: "com.XXXX.alsc.iop.common.service.facade.base.Request", $: (this && Request && Request.from ? (Request.from(this)) : this) }).$ } };
}
static from(obj: any, isInvoke?: boolean): HomePageRequest {
let instance = new HomePageRequest();
Object.assign(instance, obj && Request && Request.from ? (Request.from(obj, isInvoke)) : obj);
return instance;
}
static getMeta(): any {
return { artifact: "com.XXXX.alsc:kbt-industry-operation-common-service-facade:1.0.0.20220106-SNAPSHOT", canonicalName: "com.XXXX.alsc.iop.common.service.facade.purchasesaleplatform.request.HomePageRequest" };
}
}
- npm run proxy
当执行了这个命令,将会从上面说到的config里,拉取所依赖的服务提供方jar包,jar包描述了服务名称、服务参数等信息。Midway会读取信息自动生成这段代码。
服务消费方(就是写的这个BFF应用)在调用HSF服务之前可以使用XXXrequest.from(param)
来格式化一下数据。
调用HSF服务
const result = await this.ctx.getProxyData(this, 'tibaoQueryBffFacadeProxy.queryHomePage', param);
- getProxyData
async getProxyData(_this, apiPath, ...args) {
const [path, method] = apiPath.split('.');
let result;
if (process.env['NODE_MOCK']) { // mock 接口返回
try {
const mockFile = require(`../mock_proxy/${path}`).default;
result = mockFile[method];
} catch (e) {
result = await _this[path][method](...args);
}
} else {
result = await _this[path][method](...args);
}
return result;
},
啊,这个方法好像就是为了额外提供mock功能才封装的……
忽略mock逻辑,等价于
this.tibaoQueryBffFacadeProxy.queryHomePage(param)
这里体现了RPC的一个妙处,就是在项目中调用HSF接口,
就像调用本地一样调用远程。
这句话很精妙,值得再默读一遍。对于前端来说,好像也没什么了不起的嘛,前端发起请求不也一行代码就能发起吗,为什么到了后端使用RPC调用,就跟盘古开天辟地一样强调呢?这就说来话长了,具体的下面再讲,这里先卖个关子。
- tibaoQueryBffFacadeProxy
也在/src/proxy/XXX-service-facade.ts
里
@provide('tibaoQueryBffFacadeProxy')
export class TibaoQueryBffFacadeProxy extends TibaoQueryBffFacade {
@inject('consumerTibaoQueryBffFacade')
consumerService: ConsumerTibaoQueryBffFacade;
@inject()
ctx: any;
@init()
init() {
this.setHsfInvoke(
(name: any, args: any) => {
return this.consumerService.consumer.invoke.call(this.consumerService.consumer, name, args, { ctx: this.ctx, requestProps: Object.assign({}, this.ctx.requestProps, this.ctx.hsfRequestProps) });
}
);
}
}
@provide('consumerTibaoQueryBffFacade')
@scope(ScopeEnum.Singleton)
export class ConsumerTibaoQueryBffFacade {
@plugin('hsfClient')
hsfClient: any;
@config('proxy')
config: any;
consumer: any;
@init()
init() {
const appName = 'kbt-industry-operation';
let requestConfig = Object.assign({
group: 'HSF',
responseTimeout: 3000,
version: '1.0.0'
}, this.config.clientParams['com.XXXXX.alsc.iop.common.service.facade.purchasesaleplatform.api.TibaoQueryBffFacade']);
const serviceId = 'com.XXXXX.alsc.iop.common.service.facade.purchasesaleplatform.api.TibaoQueryBffFacade:' + requestConfig.version;
const hsfClient = this.hsfClient;
if (!hsfClient) {
return;
}
this.consumer = hsfClient.createConsumer({
id: serviceId,
appName: appName,
appname: appName,
targetAppName: appName,
group: requestConfig.group,
proxyName: 'TibaoQueryBffFacade',
responseTimeout: requestConfig.responseTimeout,
serverHost: requestConfig.serverHost
});
}
}
代码主要功能就是动态代理。
使用hsfClient 插件,和serviceid、appName、group、host……就是之前提到的config里的那些参数,去调用接口。
这个动态代理的作用就是将调用hsf接口的细节隐藏在hsfClient里,看着是
this.tibaoQueryBffFacadeProxy.queryHomePage(param)
其实是
hsfClient.createConsumer.consumer.invoke.call...
hsfClient大致干了啥,亿点点细节之后再讲。
在Midway的努力下,前端们开发bff应用不需要理解什么技术上的东西就能往上堆业务代码了,甚好。
b、为接口们打jar包
写好接口后执行
npm run jars
可以打出jar包。
- Why jar?
还记得吗,上面我提到过,在调用hsf接口之前,调用了一个方法来format
入参,而format
方法是拉了服务端的jar包生成的。
作为hsf服务给别人调用(即作为服务提供方),node应用也需要jar包让服务消费方拉取,以获得一些接口相关的信息。
由于范式调用的采用,其实不是非得使用jar包才能进行RPC请求的发起的。但是有了它,就和字典一样,接口从此拥有了一张名片。
(为什么是jar包,是因为RPC框架在大多数时候都是服务于java接口的,出于习惯和历史原因)
总之因为一些这样那样的关系,虽然是一个node应用,也采用了jar包的方式提供给各种服务消费方,比如其他的hsf应用啦、网关啦等等等。
2、网关配置
接口写完之后发布,怎么让web页面访问呢?
我们需要将接口配置到网关上。
网关是直接拉取的hsf注册中心,得知服务里具体的方法名称等信息,提供给开发配置,配置好之后就能对外开放了。
简略流程图如下:
小结
讲了一通,发现只是参与了开发流程,是不可能以管窥豹滴。
工具人们能轻易地参与开发BFF应用,离不开RPC框架的负重前行(此处有掌声),那亿点点细节具体是什么呢?且听下回分解。
参考:
1、《计算机网络》第七版
2、https://www.zhihu.com/question/41609070
3、https://time.geekbang.org/column/article/199651
4、http://www.midwayjs.org/docs/service