preface

Front-end partners of this article:

  • Experience or interest in front-end BFF development
  • Understanding of gRPC and Protobuf protocols

First of all, a brief talk about BFF (back-end for front-end). The concept of BFF may be overheard by everyone, so I don’t want to copy and paste some cliches here. If you don’t know BFF, you can recommend this article to understand it.

Simply put, BFF is an HTTP server for interface aggregation tailoring.

With the popularity of the back-end GO language, many large companies are turning to go to develop microservices. As we all know, Go is Google home, so naturally, the RPC framework gRPC developed by Google home is widely used by go language.

If the front-end BFF layer needs to connect to the gRPC + Protobuf interface provided by the GO back-end rather than the RESTful API familiar to the front-end, then we need to use grPC-Node to initiate gRPC interface calls.

Grpc-node client interceptor grPC-node client interceptor grPC-node client interceptor

What is a GRPC interceptor? Have a purpose?

GRPC interceptors are similar to axios interceptors as we know them, in that they do some of our processing at various stages of a request before it is sent, or before the request is responded to.

For example, add a token parameter to each request and check whether the errMsg field has a value for each request response.

All this uniform logic, written once for every request, is ridiculous, and we usually handle this logic uniformly in interceptors.

grpc-node client interceptor

Before we talk about grPC-Node interceptor, let’s assume a PB protocol file for later understanding the case.

All of the following cases are benchmarked against this simple PB protocol:

package "hello"

service HelloService {
    rpc SayHello(HelloReq) returns (HelloResp) {}
}

message HelloReq {
    string name = 1;
}

message HelloResp {
    string msg = 1;
}

Copy the code

Creating the Client Interceptor

What about the simplest client interceptor?

// Without doing anything, pass through all operations of the interceptor
const interceptor = (options, nextCall: Function) = > {
  return new InterceptingCall(nextCall(options));
}
Copy the code

That’s right, according to the specification:

  • Each Client Interceptor must be a function that is executed once per request to create a new interceptor instance.
  • The function needs to return an InterceptingCall instance
  • The InterceptingCall instance can pass a nextCall() parameter to continue calling the next interceptor, similarlyexpressThe middlewarenext
  • optionsParameter that describes some properties of the current gRPC request
    • options.method_descriptor.pathIs equal to:/<package name >.<service name >For example, here it is/hello.HelloService/SayHello
    • options.method_descriptor.requestSerializeThe: serializes the request parameter object as a function of the buffer, and trims unnecessary data from the request parameter
    • options.method_descriptor.responseDeserialize: Deserializes the response buffer data into a JSON object
    • options.method_descriptor.requestStream: Boolean, whether the request is streaming
    • options.method_descriptor.responseStream: Boolean, whether the response is streaming

Normally, we don’t make any changes to options, because if there are other interceptors following, this will affect the options value of downstream interceptors.

The above interceptor demo is just a brief introduction to the interceptor specification, the demo doesn’t do anything substantive.

So what should we do if we want to do something before asking for a station exit?

This is where Requester comes in

Requester (intercept processing before exit)

In the second parameter of the InterceptingCall, we can pass in a Request object to handle operations before the request is issued.

const interceptor = (options, nextCall: Function) = > {
  const requester = {
    start(){},
    sendMessage(){},
    halfClose(){},
    cancel(){},}return new InterceptingCall(nextCall(options), requester);
}
Copy the code

Requester is simply an object with specified parameters, structured as follows:

// ts defines interface Requester {start? : (metadata: Metadata, listener: Listener, next: Function) => void; sendMessage? : (message: any, next: Function) => void; halfClose? : (next: Function) => void; cancel? : (next: Function) => void; }Copy the code

Requester.start

Intercepting method called before initiating the outbound call.

start? : (metadata: Metadata, listener: Listener, next: Function) => void;Copy the code

parameter

  • Metadata: Requested metadata. You can add or delete metadata
  • Listener: a listener that listens for inbound operations, as described below
  • Next: Execute requester. Start for the next interceptor, similar to Next for Express. Next here can pass two parameters: metadata and listener.
const requester = {
    start(metadata, listener, next) {
        next(metadata, listener)
    }
}
Copy the code

Requester.sendMessage

Interceptor method called before each outbound message.

sendMessage? : (message: any, next: Function) => void;Copy the code
  • Message: Protobuf request body
  • Next: Chain of interceptor calls, where next passes the message argument
Const requester = {sendMessage(message, next) {// For current PB protocol // message === {name: 'XXXX'} next(message)}}Copy the code

Requester.halfClose

Intercepting method called when the outbound flow is closed (after the message has been sent).

halfClose? : (next: Function) => void;Copy the code
  • Next: chain call without passing arguments

Requester.cancel

Intercepting method called when the request is cancelled from the client. Less often used

cancel? : (next: Function) => void;Copy the code

Listener (Intercept processing before inbound)

Since outbound interception operations, there must be inbound interception operations.

The inbound interception method is defined in the listener in the requester.start method mentioned earlier

interface Listener { onReceiveMetadata? : (metadata: Metadata, next: Function) => void; onReceiveMessage? : (message: any, next: Function) => void; onReceiveStatus? : (status: StatusObject, next: Function) => void; }Copy the code

Listener.onReceiveMetadata

The inbound interception method triggered when response metadata is received.

const requester = {
    start(metadata, listener) {
        const newListener = {
            onReceiveMetadata(metadata, next) {
                next(metadata)
            }
        }
    }
}
Copy the code

Listener.onReceiveMessage

The inbound interception method that is triggered when a response message is received.

const newListener = {
    onReceiveMessage(message, next) {
        // For current PB protocol
        // message === {msg: 'hello xxx'}
        next(message)
    }
}
Copy the code

Listener.onReceiveStatus

An inbound interception method that is triggered when a state is received

Const newListener = {onReceiveStatus(status, next) {// Status is {code:0, details:"OK"} next(status)}}Copy the code

GRPC Interceptor execution order

The order in which interceptors are executed is as follows:

  1. Outbound requests are made in the following order:
    1. start
    2. sendMessage
    3. halfClost
  2. Inbound after request, execute order
    1. onReceiveMetadata
    2. onReceiveMessage
    3. onReceiveStatus

Multiple interceptor execution sequence

If we configure multiple interceptors, assuming the configuration order is [interceptorA, interceptorB, interceptorC], the execution order of interceptors will be:

Call -> interceptorC -> grpc.Call -> interceptorC -> interceptorB -> interceptorA The inboundCopy the code

As you can see, the order of execution is similar to the stack, first in, last out, last in, first out.

So looking at this flowchart, you might automatically think that the sequence of interceptors would be:

Interceptor A: 1. start 2. sendMessage 3. halfClost interceptor B: 4. start 5. sendMessage 6. halfClost interceptor C:......Copy the code

But that’s not the case.

As mentioned earlier, each interceptor has a next method, and the execution of the next method is actually the same interceptor phase as the execution of the next interceptor, for example:

// interceptor A start(metadata, listener, next) {next(metadata, listener, next) {next(metadata, listener, next); Listener)} // interceptor B start(metadata, listener, next) {// Next (metadata, listener)}Copy the code

As a result, the order of execution of the interceptors’ methods will be:

Outbound stage: Start (interceptor A) -> start(interceptor B) -> sendMessage -> halfClost(interceptor A) -> halfClost(interceptor B) -> grpc.call -> Inbound phase: OnReceiveMetadata (interceptor B) -> onReceiveMetadata(interceptor A) -> onReceiveMessage(interceptor B) -> onReceiveMessage(interceptor A) -> OnReceiveStatus (Interceptor B) -> onReceiveStatus(interceptor A)Copy the code

Application scenarios

You may not have a good idea of what interceptors do, but let’s take a look at how interceptors are used in practice.

Log of requests and responses

You can log it in the request and response interceptor


const logInterceptor =  (options, nextCall) = > { 
  return new grpc.InterceptingCall(nextCall(options), { 
    start(metadata, listener, next) { 
      next(metadata, { 
        onReceiveMessage(resp, next) { 
          logger.info(` request:${options.method_descriptor.path}Response body:The ${JSON.stringify(resp)}`) next(resp); }}); },sendMessage(message, next) { 
      logger.info('Initiate a request:${options.method_descriptor.path}; Request parameters:The ${JSON.stringify(message)}`) next(message); }}); };const client = new hello_proto.HelloService('localhost:50051', grpc.credentials.createInsecure(), {
    interceptors: [logInterceptor]
  });
Copy the code

The mock data

The biggest benefit of the microservice scenario is business segmentation, but at the BFF layer, if the microservice interface is not complete, it can easily be blocked by the microservice side, just as the front end is blocked by the back end interface.

We can then use the same idea to implement data mocks of the GRPC interface at the interceptor level

const interceptor =  (options, nextCall) = > { 
  let savedListener 
  // Determine whether the mock interface is currently needed, using environment variables or other judgment logic
  const isMockEnv = true
  return new grpc.InterceptingCall(nextCall(options), { 
    start: function (metadata, listener, next) { 
      // Save the listener for subsequent calls in response to the inbound method
      savedListener = listener
      // In a mock environment, there is no need to call the next method to avoid outbound requests to the server
      if(!isMockEnv) {
        next(metadata, listener); 
      }
    }, 
    sendMessage(message, next) { 
      if(isMockEnv) {
        // Construct your own mock data as needed
        const mockData = {
          hello: 'hello interceptor'
        }
        // Call the previously saved listener response method, onReceiveMessage, onReceiveStatus must be called
        savedListener.onReceiveMetadata(new grpc.Metadata());
        savedListener.onReceiveMessage(mockData);
        savedListener.onReceiveStatus({code: grpc.status.OK});
      } else{ next(message); }}}); };Copy the code

The principle is very simple, in fact, the request is not outbound, directly in the outbound preparation phase, call the inbound response method.

Abnormal request fallback

Sometimes the server side may be abnormal, resulting in interface abnormalities. You can judge the status in the inbound stage of interceptor response to avoid application exceptions.

const fallbackInterceptor = (options, nextCall) = > { 
  let savedMessage
  let savedMessageNext
  return new grpc.InterceptingCall(nextCall(options), { 
    
    start: function (metadata, listener, next) { 
      next(metadata, {
        onReceiveMessage(message, next) {
          // Save message and next for now, and wait until the interface response status is determined before responding
          savedMessage = message;
          savedMessageNext = next;
        },
        onReceiveStatus(status, next) {
            if(status.code ! == grpc.status.OK) {// If the interface fails to respond, respond to the default data and avoid XXX undefined
              savedMessageNext({
                errCode: status.code,
                errMsg: status.details,
                result: []});// Set the current interface to normal
              next({
                code: grpc.status.OK,
                details: 'OK'
              });
            } else{ savedMessageNext(savedMessage); next(status); }}}); }}); };Copy the code

The principle is not complicated, probably is to capture abnormal state, response to normal state and preset data.

conclusion

As you can see, the GRPC interceptor concept is nothing special or difficult to understand. It is basically the same as the common interceptor concept, such as axios interceptor, which provides methods to do some custom unified logic processing for the request and response phases.

This article is mainly on GRPC – Node interceptor to do a simple interpretation, I hope this article can give is using GRPC – Node to do BFF layer students some help.




If articles are helpful to you, your star is my biggest support for other articles:

  • Json you don’t know the browser, Module, and main fields priority
  • You can play it this way? Super useful Typescript built-in and custom types
  • Not shocked, you can also look at the front of the Flutter advice guide


Post promotion in the long term: front end, back end (to go), product, UI, testing, Android, IOS, operation and maintenance all want, recruitment details JD look pull check. Salary and benefits: 20K-50KšŸ˜³, 7 PM off šŸ˜, free fruit šŸ˜, free dinner šŸ˜Š, 15 days annual leave (), 14 days paid sick leave. Resume email: [email protected] or add me to wechat: CWY13920