美文网首页计算机组成/网络/系统
前端也能懂的RPC(上)

前端也能懂的RPC(上)

作者: 景阳冈大虫在此 | 来源:发表于2022-01-01 16:12 被阅读0次

    关键词: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) && (<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
      当执行了这个命令,将会从上面说到的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(上)

        本文链接:https://www.haomeiwen.com/subject/ahggqrtx.html