grpc-node在bff中的实践(上)

2,918 阅读3分钟

前言

开始阅读之前,先了解一下概念。rpc, gprc, protocol buffer,bff

什么是grpc
  • gRPC 是一个高性能、开源和通用的 RPC 框架,面向移动和 HTTP/2 设计,提供多语言支持。gRPC使用protocol buffers作为其接口定义语言(IDL)和其基础消息交换格式
  • RPC(Remote Procedure Call)远程过程调用。简单来说,就是我在本地调用了一个函数,或者对象的方法,实际上是调用了远程机器上的函数,或者远程对象的方法,但是这个通信过程对于程序员来说是透明的。

grpc调用过程如图1:

grpc-call

什么是bff

简单来时就是为前端不同的设备建立对应的后端。如图:

Using one server-side BFF per user interface

hello grpc

解决概念问题后,先来看看nodejs如何实现grpc server和client调用。如图1

定义 protobuf

hello.proto

syntax = "proto3"; // 语法proto3

package greeter; // 包名

/**
package greeter 包含两个service:Hello和 SelfIntro
message 定义了rpc方法参数和返回值的结构
*/

service Hello {
    rpc SayHello (SayHelloRequest) returns (SayHelloResponse) {}
}

service SelfIntro {
    rpc IntroMyself (SelfIntroRequest) returns (SelfIntroResponse) {}
}


message SayHelloRequest {
    string name = 1;
}

message SayHelloResponse {
    string message = 1;
}


message SelfIntroRequest {
    
}

message SelfIntroResponse {
    string job = 1;
}

grpc使用protobuf有两种方式。 一种是使用Protobuf.js在运行时动态生成代码,另一种是使用protoc编译器生成静态的代码(生成对应的结构和方法)。 本篇实例采用前者。

加载protobuf
const grpc = require("grpc")
const protoLoader = require("@grpc/proto-loader")
const packageDescripter = protoLoader.loadSync(
    __dirname+ '/../hello.proto',
    {
        keepCase: true
    }
)

const gretterPackage = grpc.loadPackageDefinition(packageDescripter).greeter
server实现
// ... load proto

/**
 * 实现rpc方法 SayHello,IntroMyself
 * 在50051端口启动服务
 */

function SayHello(call, callback) {
    callback(null, {message: 'Hello ' + call.request.name})
}

function IntroMyself(call, callback) {
    callback(null, {job: 'program enginner'})
}

function main() {
    const server = new grpc.Server()
    server.addService(gretterPackage.Hello.service, {
        SayHello,
    })
    server.addService(gretterPackage.SelfIntro.service, {
        IntroMyself,
    })

    server.bind('0.0.0.0:50051', grpc.ServerCredentials.createInsecure())
    server.start(() => {
        console.log('server runing on prot 50051')
    })
}
客户端(stub)调用
// ... load proto

/**
 * 创建服务端存根(stub)
 * 调用远程方法
 */
function main () {
    // 指定远端为localhost:50051

    const stubHello = new gretterPackage.Hello('localhost:50051', grpc.credentials.createInsecure())
    const name = "joe"

    stubHello.SayHello({name}, (err, response) => {
        if (err) { return console.error(err) }
        console.log('Greeting: ', response.message)
    })


    const stubIntro = new gretterPackage.SelfIntro('localhost:50051', grpc.credentials.createInsecure())

    stubIntro.IntroMyself({}, (err, response) => {
        if (err) { return console.error(err) }
        console.log('SelfIntro: my job is', response.job)
    })
}

到此为止我们已经体验了一个完整的grpc调用过程。通常定义protobuf和server实现由后端完成。node这一层创建客户端,拿到rpc结果,以restful api的方式提供给前端

ts提效

我们通过proto知道远端定义那些service,有哪些prc方法,以及调用参数和返回值类型。由于js是弱类型的语言,在实际应用中,没有办法校验参数的合法性,也不能再写代码的时候提供补全提示等。为此,引入ts解决以上问题。

但是,grpc只是支持nodejs。这就需要通过某种方式安卓proto文件生成对应的ts文件

解决那些问题
  1. 代码提示补全
  2. 参数检查
quick look

types.ts

// This file is auto generated by grpc-code-gen, do not edit!
// tslint:disable
export namespace greeter {
  export interface SayHelloRequest {
    'name'?: string;
  }


  export interface SayHelloResponse {
    'message'?: string;
  }


  export interface SelfIntroRequest {
  }


  export interface SelfIntroResponse {
    'job'?: string;
  }


}

greeter/Hello.ts

export interface IHello {
  $FILE_NAME: string;
  new (address: string, credentials: ChannelCredentials, options?: object): IHello;

  /** @deprecated 请使用: SayHelloV2 */
  SayHello(
    request: types.greeter.SayHelloRequest,
    options?: { timeout?: number; flags?: number; host?: string; }
  ): Promise<types.greeter.SayHelloResponse>;
  /** @deprecated 请使用: SayHelloV2 */
  SayHello(
    request: types.greeter.SayHelloRequest,
    metadata: MetadataMap,
    options?: { timeout?: number; flags?: number; host?: string; }
  ): Promise<types.greeter.SayHelloResponse>;
  
  SayHelloV2(option: {
    request: types.greeter.SayHelloRequest;
    metadata?: MetadataMap;
    options?: { timeout?: number; flags?: number; host?: string; };
  }): Promise<{ response:types.greeter.SayHelloResponse, metadata: Metadata }>;
}
export const hello: IHello = new greeter.Hello(`${serviceConfig.host}:${serviceConfig.port}`, credentials);

clinet.ts

import {hello} from "greeter/Hello.ts"
import * as types from "types"

export sayHello = (req: types.greeter.SayHelloRequest):  Promise<types.greeter.SayHelloResponse> => {
    return hello.SayHello(req)
}

sayHello({name: 'grpc'})