前端也能懂的RPC(上)

关键词: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位。

《计算机网络》第6章

对于HTTP包来说,有多少个键值对就会有多大的头。

对比一下gRPC(RPC的一种实现)所采用的HTTP2.0协议,包的结构精简了许多,不同的位存储不同含义的信息,使得全包都能使用二进制编码。


HTTP2.0

2、HTTP协议属于无状态协议

无状态协议,客户端无法对请求和响应进行关联,每次请求都需要重新建立连接,响应完成再关闭连接。性能不高。

三、在开发中体会RPC调用

讲了这么多,好像内容跟做项目关系不大。从熟悉的地方说起吧。

1、BFF项目

最近做的前端的BFF项目,node框架是Midway,RPC框架是hsf。
简化的请求流程如下图:


request

刚开始接触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

你可能感兴趣的:(前端也能懂的RPC(上))