gRPC
Key takeaways
GrpcService | gRPC service component. |
GrpcClient | gRPC client component. |
summator.proto | Proto file containing messages and services. |
Protoc | Proto file compiler. |
Introduction
In this tutorial, you will learn how to create a gRPC client and server by using the Pip.Services' gRPC module. We will start with an explanation of how to install this module and a brief description of the example used. Next, we will see how to create a gRPC server and client. Lastly, we will have a section containing the complete code for this project.
Pre-requisites
In order to create a gRPC server and client, we need to install the grpc module first. The command to do this is:
npm install pip-services4-grpc-node --save
A brief overview of the example
Our example consists of two programs: a service and a client, which communicate between them via the gRPC protocol. The process is as follows
- The client creates a request to add two numbers that is translated into a proto request via the gRPC stub and sent to the service.
- The service receives these two numbers, translates the proto request via the gRPC server, and calls a function named sum. This function is available from the Summator program.
- Once the result is on the server, it is translated into a proto response and sent to the client.
- The client receives the result, and translates and prints it.
The figure below summarizes this procedure.
In order to communicate via the gRPC protocol, the client uses a gRPC stub and the service a gRPC server. Both are constructed based on the summator2.proto file, which contains descriptions of the input parameters, and the function used. These descriptions are transformed into two coded files via the protoc compiler.
These coded files are written in the languages of the service and client respectively. Their names are: summator2_pb2 and summator_pb2_grpc. Together, they contain all the necessary elements to create the gRPC server and the gRPC client.
Proto file
The proto file describes the communication contract between the client and the server. It is written in proto3, a language created by Google to describe gRPC communications.
Syntax
Our proto file will contain the following elements:
- Syntax: A command indicating that we are using proto3
- Number1: A message item describing the input to our function. In our case, we will define both values as floats.
- Number2: A message item describing the value returned by our method. In this example, we return the sum of the inputs as a float value.
- Summator: A service item describing our method.
The figure below summarizes this description.
Compilation
A proto file can be compiled into files in different languages, such as Python, C++, Ruby, C#, Go, and Java. This is done by running the protocol buffer compiler protoc on the .proto file.
The compiler generates two files per language with the information on data types, stub and server.
In our case, both client and service are written in the same language. Thus, we generate a common set of files. Our command is:
npx grpc_tools_node_protoc --js_out=import_style=commonjs,binary:./ --grpc_out=. ./summator.proto
Server
We create a server that communicates to clients via the gRPC protocol and connects to a library of functions, which in this example is represented by the Summator file.
The following sections explain these features in detail.
Summator file
In our example, we call a function named sum, which is available via the summator file. This function adds two given numbers and returns the result. Its code is as follows:
export class Calculations {
public static sum(x: number, y: number): number {
let z = x + y;
return z;
}
}
gRPC service
This is the component that communicates with the library of functions and sends the results to clients after receiving their requests.
Pre-requisites
GrpcService
First, we need to import the GrpcService class from the gRPC module. We can do this with the following command:
import { GrpcController } from "pip-services4-grpc-node";
Proto compiler generated classes
We also need to import the two files previously generated by the protoc compiler.
let services = require('../summator_grpc_pb');
let messages = require('../summator_pb');
Library of functions
And the library with the function available from the server.
import { Calculations } from "./calculations";
Component generation
Now that we have imported all the necessary elements, we can create our gRPC service, which is a subclass of the GrpcService class. In it, we need to define the method that we are using via the _register_method() method. The following code shows how to do this.
export class MyGrpcController extends GrpcController {
public constructor() {
super(services.SummatorService);
}
private async sum(call: any): Promise<any> {
let res = Calculations.sum(call.request.getValue1(), call.request.getValue2());
let response = new messages.Number2();
response.setValue(res);
return response;
}
public register(): void {
this.registerMethod(
"sum",
null,
this.sum
);
}
}
Next, we create an instance of the gRPC service and configure it with the connection parameters.
import { ConfigParams, References } from "pip-services4-components-nodes";
let controller = new MyGrpcController();
controller.configure(ConfigParams.fromTuples(
"connection.protocol", "http",
"connection.host", "localhost",
"connection.port", 50055
));
controller.setReferences(new References());
Running the service
Once we have our service ready, we launch it via the open() method.
await controller.open(null);
Our gRPC service is now listening from our computer (port 50051) and waiting for a client to send a request.
Client
The next step is to create a client, which will be used to call the sum() method available from the service, and obtain the corresponding result.
Pre-requisites
GrpcClient
In order to use this component, we need to import it first. The following command shows how to do this:
import { GrpcClient } from "pip-services4-grpc-node";
Proto compiler generated classes
We also need to import the two files previously generated by the protoc compiler.
let services = require('../summator_grpc_pb');
let messages = require('../summator_pb');
Component generation
Once we have imported all the necessary files, we create a subclass of GrpcClient. In it, we define the get_data method, which calls the Sum method and returns the received result. The code is as follows:
export class MyGrpcClient extends GrpcClient {
public constructor() {
super(services.SummatorClient);
}
public async getData(correlationId: string, value1: number, value2: number): Promise<number> {
let request = new messages.Number1();
request.setValue1(value1);
request.setValue2(value2);
let res = await this.call<any>("sum", correlationId, request);
return res.getValue();
}
}
After defining our gRCP client, we create an instance of it and use the configure() method to define the connection parameters. In our example, we connect to a server using our machine via the default port 50051.
import { ConfigParams, References } from "pip-services4-components-node";
let client = new MyGrpcClient();
client.configure(ConfigParams.fromTuples(
"connection.protocol", "http",
"connection.host", "localhost",
"connection.port", 50055
));
client.setReferences(new References());
Finally, we connect to the server via the open() method.
await client.open(null);
Consuming the service
The next step is to call the get_data() method and obtain the result. The following example shows how to use it to add five and three, which returns eight.
let result = await client.getData(null, 3, 5); // Returns 8
Final code
This section presents the complete code for the example, namely the server and client’s code, the proto file, the two compiler-generated files, and the library file.
Server
The code for the server is:
// Pre-requisites
let services = require('../summator_grpc_pb');
let messages = require('../summator_pb');
import { ConfigParams, Descriptor, References } from "pip-services4-components-node";
import { GrpcController } from "pip-services4-grpc-node";
import { Calculations } from "./calculations";
// gRPC controller
export class MyGrpcController extends GrpcController {
public constructor() {
super(services.SummatorService);
}
private async sum(call: any): Promise<any> {
let res = Calculations.sum(call.request.getValue1(), call.request.getValue2());
let response = new messages.Number2();
response.setValue(res);
return response;
}
public register(): void {
this.registerMethod(
"sum",
null,
this.sum
);
}
}
let controller = new MyGrpcController();
controller.configure(ConfigParams.fromTuples(
"connection.protocol", "http",
"connection.host", "localhost",
"connection.port", 50055
));
controller.setReferences(new References());
await controller.open(null);
Client
The code for the client is:
// Pre-requisites
let services = require('../summator_grpc_pb');
let messages = require('../summator_pb');
import { ConfigParams, References } from "pip-services4-components-node";
import { GrpcClient } from "pip-services4-grpc-node";
// gRPC client
export class MyGrpcClient extends GrpcClient {
public constructor() {
super(services.SummatorClient);
}
public async getData(correlationId: string, value1: number, value2: number): Promise<number> {
let request = new messages.Number1();
request.setValue1(value1);
request.setValue2(value2);
let res = await this.call<any>("sum", correlationId, request);
return res.getValue();
}
}
let client = new MyGrpcClient();
client.configure(ConfigParams.fromTuples(
"connection.protocol", "http",
"connection.host", "localhost",
"connection.port", 50055
));
client.setReferences(new References());
await client.open(null);
// Function call and result
let result = await client.getData(null, 3, 5); // Returns 8
Proto file
Our proto file looks like this:
syntax = "proto3";
option go_package = "./main";
message Number1 {
float value1 = 1;
float value2 = 2;
}
message Number2 {
float value = 1;
}
service Summator {
rpc sum(Number1) returns (Number2) {}
}
Proto compiler generated files
The files generated by the protoc compiler are:
summator_pb.js
Code Example
// source: summator.proto
/**
* @fileoverview
* @enhanceable
* @suppress {missingRequire} reports error on implicit type usages.
* @suppress {messageConventions} JS Compiler reports an error if a variable or
* field starts with 'MSG_' and isn't a translatable message.
* @public
*/
// GENERATED CODE -- DO NOT EDIT!
/* eslint-disable */
// @ts-nocheck
var jspb = require('google-protobuf');
var goog = jspb;
var global = Function('return this')();
goog.exportSymbol('proto.Number1', null, global);
goog.exportSymbol('proto.Number2', null, global);
/**
* Generated by JsPbCodeGenerator.
* @param {Array=} opt_data Optional initial data array, typically from a
* server response, or constructed directly in Javascript. The array is used
* in place and becomes part of the constructed object. It is not cloned.
* If no data is provided, the constructed object will be empty, but still
* valid.
* @extends {jspb.Message}
* @constructor
*/
proto.Number1 = function(opt_data) {
jspb.Message.initialize(this, opt_data, 0, -1, null, null);
};
goog.inherits(proto.Number1, jspb.Message);
if (goog.DEBUG && !COMPILED) {
/**
* @public
* @override
*/
proto.Number1.displayName = 'proto.Number1';
}
/**
* Generated by JsPbCodeGenerator.
* @param {Array=} opt_data Optional initial data array, typically from a
* server response, or constructed directly in Javascript. The array is used
* in place and becomes part of the constructed object. It is not cloned.
* If no data is provided, the constructed object will be empty, but still
* valid.
* @extends {jspb.Message}
* @constructor
*/
proto.Number2 = function(opt_data) {
jspb.Message.initialize(this, opt_data, 0, -1, null, null);
};
goog.inherits(proto.Number2, jspb.Message);
if (goog.DEBUG && !COMPILED) {
/**
* @public
* @override
*/
proto.Number2.displayName = 'proto.Number2';
}
if (jspb.Message.GENERATE_TO_OBJECT) {
/**
* Creates an object representation of this proto.
* Field names that are reserved in JavaScript and will be renamed to pb_name.
* Optional fields that are not set will be set to undefined.
* To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
* For the list of reserved names please see:
* net/proto2/compiler/js/internal/generator.cc#kKeyword.
* @param {boolean=} opt_includeInstance Deprecated. whether to include the
* JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @return {!Object}
*/
proto.Number1.prototype.toObject = function(opt_includeInstance) {
return proto.Number1.toObject(opt_includeInstance, this);
};
/**
* Static version of the {@see toObject} method.
* @param {boolean|undefined} includeInstance Deprecated. Whether to include
* the JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @param {!proto.Number1} msg The msg instance to transform.
* @return {!Object}
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.Number1.toObject = function(includeInstance, msg) {
var f, obj = {
value1: jspb.Message.getFloatingPointFieldWithDefault(msg, 1, 0.0),
value2: jspb.Message.getFloatingPointFieldWithDefault(msg, 2, 0.0)
};
if (includeInstance) {
obj.$jspbMessageInstance = msg;
}
return obj;
};
}
/**
* Deserializes binary data (in protobuf wire format).
* @param {jspb.ByteSource} bytes The bytes to deserialize.
* @return {!proto.Number1}
*/
proto.Number1.deserializeBinary = function(bytes) {
var reader = new jspb.BinaryReader(bytes);
var msg = new proto.Number1;
return proto.Number1.deserializeBinaryFromReader(msg, reader);
};
/**
* Deserializes binary data (in protobuf wire format) from the
* given reader into the given message object.
* @param {!proto.Number1} msg The message object to deserialize into.
* @param {!jspb.BinaryReader} reader The BinaryReader to use.
* @return {!proto.Number1}
*/
proto.Number1.deserializeBinaryFromReader = function(msg, reader) {
while (reader.nextField()) {
if (reader.isEndGroup()) {
break;
}
var field = reader.getFieldNumber();
switch (field) {
case 1:
var value = /** @type {number} */ (reader.readFloat());
msg.setValue1(value);
break;
case 2:
var value = /** @type {number} */ (reader.readFloat());
msg.setValue2(value);
break;
default:
reader.skipField();
break;
}
}
return msg;
};
/**
* Serializes the message to binary data (in protobuf wire format).
* @return {!Uint8Array}
*/
proto.Number1.prototype.serializeBinary = function() {
var writer = new jspb.BinaryWriter();
proto.Number1.serializeBinaryToWriter(this, writer);
return writer.getResultBuffer();
};
/**
* Serializes the given message to binary data (in protobuf wire
* format), writing to the given BinaryWriter.
* @param {!proto.Number1} message
* @param {!jspb.BinaryWriter} writer
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.Number1.serializeBinaryToWriter = function(message, writer) {
var f = undefined;
f = message.getValue1();
if (f !== 0.0) {
writer.writeFloat(
1,
f
);
}
f = message.getValue2();
if (f !== 0.0) {
writer.writeFloat(
2,
f
);
}
};
/**
* optional float value1 = 1;
* @return {number}
*/
proto.Number1.prototype.getValue1 = function() {
return /** @type {number} */ (jspb.Message.getFloatingPointFieldWithDefault(this, 1, 0.0));
};
/**
* @param {number} value
* @return {!proto.Number1} returns this
*/
proto.Number1.prototype.setValue1 = function(value) {
return jspb.Message.setProto3FloatField(this, 1, value);
};
/**
* optional float value2 = 2;
* @return {number}
*/
proto.Number1.prototype.getValue2 = function() {
return /** @type {number} */ (jspb.Message.getFloatingPointFieldWithDefault(this, 2, 0.0));
};
/**
* @param {number} value
* @return {!proto.Number1} returns this
*/
proto.Number1.prototype.setValue2 = function(value) {
return jspb.Message.setProto3FloatField(this, 2, value);
};
if (jspb.Message.GENERATE_TO_OBJECT) {
/**
* Creates an object representation of this proto.
* Field names that are reserved in JavaScript and will be renamed to pb_name.
* Optional fields that are not set will be set to undefined.
* To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
* For the list of reserved names please see:
* net/proto2/compiler/js/internal/generator.cc#kKeyword.
* @param {boolean=} opt_includeInstance Deprecated. whether to include the
* JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @return {!Object}
*/
proto.Number2.prototype.toObject = function(opt_includeInstance) {
return proto.Number2.toObject(opt_includeInstance, this);
};
/**
* Static version of the {@see toObject} method.
* @param {boolean|undefined} includeInstance Deprecated. Whether to include
* the JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @param {!proto.Number2} msg The msg instance to transform.
* @return {!Object}
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.Number2.toObject = function(includeInstance, msg) {
var f, obj = {
value: jspb.Message.getFloatingPointFieldWithDefault(msg, 1, 0.0)
};
if (includeInstance) {
obj.$jspbMessageInstance = msg;
}
return obj;
};
}
/**
* Deserializes binary data (in protobuf wire format).
* @param {jspb.ByteSource} bytes The bytes to deserialize.
* @return {!proto.Number2}
*/
proto.Number2.deserializeBinary = function(bytes) {
var reader = new jspb.BinaryReader(bytes);
var msg = new proto.Number2;
return proto.Number2.deserializeBinaryFromReader(msg, reader);
};
/**
* Deserializes binary data (in protobuf wire format) from the
* given reader into the given message object.
* @param {!proto.Number2} msg The message object to deserialize into.
* @param {!jspb.BinaryReader} reader The BinaryReader to use.
* @return {!proto.Number2}
*/
proto.Number2.deserializeBinaryFromReader = function(msg, reader) {
while (reader.nextField()) {
if (reader.isEndGroup()) {
break;
}
var field = reader.getFieldNumber();
switch (field) {
case 1:
var value = /** @type {number} */ (reader.readFloat());
msg.setValue(value);
break;
default:
reader.skipField();
break;
}
}
return msg;
};
/**
* Serializes the message to binary data (in protobuf wire format).
* @return {!Uint8Array}
*/
proto.Number2.prototype.serializeBinary = function() {
var writer = new jspb.BinaryWriter();
proto.Number2.serializeBinaryToWriter(this, writer);
return writer.getResultBuffer();
};
/**
* Serializes the given message to binary data (in protobuf wire
* format), writing to the given BinaryWriter.
* @param {!proto.Number2} message
* @param {!jspb.BinaryWriter} writer
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.Number2.serializeBinaryToWriter = function(message, writer) {
var f = undefined;
f = message.getValue();
if (f !== 0.0) {
writer.writeFloat(
1,
f
);
}
};
/**
* optional float value = 1;
* @return {number}
*/
proto.Number2.prototype.getValue = function() {
return /** @type {number} */ (jspb.Message.getFloatingPointFieldWithDefault(this, 1, 0.0));
};
/**
* @param {number} value
* @return {!proto.Number2} returns this
*/
proto.Number2.prototype.setValue = function(value) {
return jspb.Message.setProto3FloatField(this, 1, value);
};
goog.object.extend(exports, proto);
summator_grpc_pb.js
Code Example
// GENERATED CODE -- DO NOT EDIT!
'use strict';
var grpc = require('grpc');
var summator_pb = require('./summator_pb.js');
function serialize_Number1(arg) {
if (!(arg instanceof summator_pb.Number1)) {
throw new Error('Expected argument of type Number1');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_Number1(buffer_arg) {
return summator_pb.Number1.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_Number2(arg) {
if (!(arg instanceof summator_pb.Number2)) {
throw new Error('Expected argument of type Number2');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_Number2(buffer_arg) {
return summator_pb.Number2.deserializeBinary(new Uint8Array(buffer_arg));
}
var SummatorService = exports.SummatorService = {
sum: {
path: '/Summator/sum',
requestStream: false,
responseStream: false,
requestType: summator_pb.Number1,
responseType: summator_pb.Number2,
requestSerialize: serialize_Number1,
requestDeserialize: deserialize_Number1,
responseSerialize: serialize_Number2,
responseDeserialize: deserialize_Number2,
},
};
exports.SummatorClient = grpc.makeGenericClientConstructor(SummatorService);
Library
In this tutorial’s example, we used the summator file as a library of methods. This file is like this:
export class Calculations {
public static sum(x: number, y: number): number {
let z = x + y;
return z;
}
}
Wrapping up
In this tutorial, we have learned how to create a gRPC server and client. We also saw how to create a proto file and generate the two gRPC files via the protoc compiler. Finally, we understood how to run the server and the client and obtain a result from a call to the sum() method.