關(guān)鍵詞:RPC、hsf、Midway、nodejs
一、前言

大家都知道,網(wǎng)絡(luò)是由光纖傳輸?shù)摹8鞣N信息被轉(zhuǎn)換成二進(jìn)制,通過明暗相間的光信號經(jīng)過光纖進(jìn)行傳播。
只有二進(jìn)制才能在網(wǎng)絡(luò)中傳輸,不管是使用了什么協(xié)議,http還是rpc,最終都需要被序列化為二進(jìn)制。
二、RPC是什么
前端頁面發(fā)起HTTP請求與后端進(jìn)行通信,那么后端進(jìn)程間用以通信的RPC協(xié)議又是什么呢?(本文說的HTTP特指HTTP1.1)

Remote Procedure Call,遠(yuǎn)程過程調(diào)用。
RPC是解決進(jìn)程間通信的一種方式。
RPC就是把攔截到的方法參數(shù),轉(zhuǎn)成可以在網(wǎng)絡(luò)中傳輸?shù)亩M(jìn)制,并保證服務(wù)提供方能正確地還原出語義,最終實現(xiàn)像調(diào)用本地一樣地調(diào)用遠(yuǎn)程的目的。
為什么服務(wù)器間的通信使用RPC而不是HTTP
RPC和HTTP同屬于應(yīng)用層協(xié)議,為什么會有用途上的差別呢?
網(wǎng)上有好多答案,梳理了一下,大概如下:
1、HTTP的包冗余過多導(dǎo)致體積過大
HTTP是面向文本的,因此在報文中的每一個字段都是一些ASCII碼串,body根據(jù)Content-Type有不同的編碼。這導(dǎo)致HTTP的包有很多冗余的部分,又需要加入很多無用的內(nèi)容,比如換行符號,回車符等,體積相較于發(fā)送同樣信息的RPC大了許多。
ASCII它是一種7位編碼,但它存放時必須占全一個字節(jié),也即占用8位。

對于HTTP包來說,有多少個鍵值對就會有多大的頭。
對比一下gRPC(RPC的一種實現(xiàn))所采用的HTTP2.0協(xié)議,包的結(jié)構(gòu)精簡了許多,不同的位存儲不同含義的信息,使得全包都能使用二進(jìn)制編碼。

2、HTTP協(xié)議屬于無狀態(tài)協(xié)議
無狀態(tài)協(xié)議,客戶端無法對請求和響應(yīng)進(jìn)行關(guān)聯(lián),每次請求都需要重新建立連接,響應(yīng)完成再關(guān)閉連接。性能不高。
三、在開發(fā)中體會RPC調(diào)用
講了這么多,好像內(nèi)容跟做項目關(guān)系不大。從熟悉的地方說起吧。
1、BFF項目
最近做的前端的BFF項目,node框架是Midway,RPC框架是hsf。
簡化的請求流程如下圖:

剛開始接觸BFF的時候發(fā)現(xiàn)有很多名詞很陌生,RPC、泛化調(diào)用、序列化、動態(tài)代理、jar包什么的。
用node語言去編寫一個hsf服務(wù),前端如果不了解RPC一些基本知識,寫起項目來就像是個打字員- -。這也是為什么想寫這篇文章的原因。
那從前端開發(fā)的角度上入手,寫項目到發(fā)布,前端開發(fā)都做了些什么呢?
開發(fā)到發(fā)布流程

a、調(diào)用HSF接口獲得數(shù)據(jù)
項目用的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; // 注入網(wǎng)關(guān)上下文以獲取登錄態(tài)
@inject() tibaoQueryBffFacadeProxy: TibaoQueryBffFacadeProxy;// 訪問的hsf服務(wù)代理
public async queryHomePage(params) { // PurchaseSalesIndex服務(wù)的queryHomePage方法
const gateWayUserInfoContext = this.gatewayContextService.getKIOUserInfo(params); // 登錄態(tài)
const reqParams = {
...gateWayUserInfoContext
};
try {
const param = HomePageRequest.from(reqParams); // format入?yún)? const result = await this.ctx.getProxyData(this, 'tibaoQueryBffFacadeProxy.queryHomePage', param); // 調(diào)用HSF接口獲得依賴數(shù)據(jù)
// ... ... 數(shù)據(jù)處理略
return this.ctx.gatewaySuccess(curResult);// 返回結(jié)果
}
}
依賴注入
關(guān)于語法,稍微解釋一下@provide、@inject裝飾器就是Midway引以為傲的依賴注入設(shè)計了。他們官網(wǎng)寫得很清楚,簡而言之就是把聲明的上下文變量注入到this指向的實例中,這樣我們無需關(guān)心注入的東西的內(nèi)部邏輯,就像真的實現(xiàn)了那樣去調(diào)用。
config proxy
在項目的/src/config/proxy.ts中配置了依賴的服務(wù)提供方j(luò)ar包的版本號、groupID等信息
{
artifact: {
// 服務(wù)提供方的應(yīng)用名稱
appName: 'kbt-industry-operation',
// hsf 服務(wù)的二方包 groupId、artifactId以及版本號
groupId: 'com.XXXXX.alsc',
artifactId: 'kbt-industry-operation-common-service-facade',
version: '1.0.0.20220102-SNAPSHOT',
// 需要調(diào)用的服務(wù)名稱列表
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入?yún)?/h5>
可以看到代碼里用的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) && (<any>(this && Request && Request.from ? (Request.from(this)) : this)).formatHSF ? (<any>(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
當(dāng)執(zhí)行了這個命令,將會從上面說到的config里,拉取所依賴的服務(wù)提供方j(luò)ar包,jar包描述了服務(wù)名稱、服務(wù)參數(shù)等信息。Midway會讀取信息自動生成這段代碼。
服務(wù)消費(fèi)方(就是寫的這個BFF應(yīng)用)在調(diào)用HSF服務(wù)之前可以使用XXXrequest.from(param)來格式化一下數(shù)據(jù)。
調(diào)用HSF服務(wù)
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)
這里體現(xiàn)了RPC的一個妙處,就是在項目中調(diào)用HSF接口,
就像調(diào)用本地一樣調(diào)用遠(yuǎn)程。
這句話很精妙,值得再默讀一遍。對于前端來說,好像也沒什么了不起的嘛,前端發(fā)起請求不也一行代碼就能發(fā)起嗎,為什么到了后端使用RPC調(diào)用,就跟盤古開天辟地一樣強(qiáng)調(diào)呢?這就說來話長了,具體的下面再講,這里先賣個關(guān)子。
- 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
});
}
}
代碼主要功能就是動態(tài)代理。
使用hsfClient 插件,和serviceid、appName、group、host……就是之前提到的config里的那些參數(shù),去調(diào)用接口。
這個動態(tài)代理的作用就是將調(diào)用hsf接口的細(xì)節(jié)隱藏在hsfClient里,看著是
this.tibaoQueryBffFacadeProxy.queryHomePage(param)
其實是
hsfClient.createConsumer.consumer.invoke.call...
hsfClient大致干了啥,億點(diǎn)點(diǎn)細(xì)節(jié)之后再講。
在Midway的努力下,前端們開發(fā)bff應(yīng)用不需要理解什么技術(shù)上的東西就能往上堆業(yè)務(wù)代碼了,甚好。
b、為接口們打jar包
寫好接口后執(zhí)行
npm run jars
可以打出jar包。
-
Why jar?
還記得嗎,上面我提到過,在調(diào)用hsf接口之前,調(diào)用了一個方法來format入?yún)?,?code>format方法是拉了服務(wù)端的jar包生成的。
作為hsf服務(wù)給別人調(diào)用(即作為服務(wù)提供方),node應(yīng)用也需要jar包讓服務(wù)消費(fèi)方拉取,以獲得一些接口相關(guān)的信息。
由于范式調(diào)用的采用,其實不是非得使用jar包才能進(jìn)行RPC請求的發(fā)起的。但是有了它,就和字典一樣,接口從此擁有了一張名片。
(為什么是jar包,是因為RPC框架在大多數(shù)時候都是服務(wù)于java接口的,出于習(xí)慣和歷史原因)
總之因為一些這樣那樣的關(guān)系,雖然是一個node應(yīng)用,也采用了jar包的方式提供給各種服務(wù)消費(fèi)方,比如其他的hsf應(yīng)用啦、網(wǎng)關(guān)啦等等等。
2、網(wǎng)關(guān)配置
接口寫完之后發(fā)布,怎么讓web頁面訪問呢?
我們需要將接口配置到網(wǎng)關(guān)上。
網(wǎng)關(guān)是直接拉取的hsf注冊中心,得知服務(wù)里具體的方法名稱等信息,提供給開發(fā)配置,配置好之后就能對外開放了。
簡略流程圖如下:

小結(jié)
講了一通,發(fā)現(xiàn)只是參與了開發(fā)流程,是不可能以管窺豹滴。
工具人們能輕易地參與開發(fā)BFF應(yīng)用,離不開RPC框架的負(fù)重前行(此處有掌聲),那億點(diǎn)點(diǎn)細(xì)節(jié)具體是什么呢?且聽下回分解。
參考:
1、《計算機(jī)網(wǎng)絡(luò)》第七版
2、https://www.zhihu.com/question/41609070
3、https://time.geekbang.org/column/article/199651
4、http://www.midwayjs.org/docs/service