前端也能懂的RPC(上)

關(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位。

《計算機(jī)網(wǎng)絡(luò)》第6章

對于HTTP包來說,有多少個鍵值對就會有多大的頭。

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


HTTP2.0

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。
簡化的請求流程如下圖:


request

剛開始接觸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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容