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, similarly
express
The middlewarenext
options
Parameter that describes some properties of the current gRPC requestoptions.method_descriptor.path
Is equal to:/<package name >.<service name >
For example, here it is/hello.HelloService/SayHello
options.method_descriptor.requestSerialize
The: serializes the request parameter object as a function of the buffer, and trims unnecessary data from the request parameteroptions.method_descriptor.responseDeserialize
: Deserializes the response buffer data into a JSON objectoptions.method_descriptor.requestStream
: Boolean, whether the request is streamingoptions.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:
- Outbound requests are made in the following order:
- start
- sendMessage
- halfClost
- Inbound after request, execute order
- onReceiveMetadata
- onReceiveMessage
- 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