gRPC

How to create a server and a client, and communicate between them.

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
Not available
Not available
Not available

Not available

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

  1. 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.
  2. 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.
  3. Once the result is on the server, it is translated into a proto response and sent to the client.
  4. The client receives the result, and translates and prints it.

The figure below summarizes this procedure.

figure 1

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:

  1. Syntax: A command indicating that we are using proto3
  2. Number1: A message item describing the input to our function. In our case, we will define both values as floats.
  3. 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.
  4. Summator: A service item describing our method.

The figure below summarizes this description.

figure 2

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

Not available
Not available
Not available

Not available
Not available
Not available
Not available

Not available

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;
    }
}

Not available
Not available
Not available

Not available

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";
Not available
Not available
Not available

Not available
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');

Not available
Not available
Not available

Not available
Library of functions

And the library with the function available from the server.

import { Calculations } from "./calculations";

Not available
Not available
Not available

Not available
Not available
Not available
Not available

Not available
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
        );
    }
}
Not available
Not available
Not available

Not available

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());


Not available
Not available
Not available

Not available
Running the service

Once we have our service ready, we launch it via the open() method.

await controller.open(null);

Not available
Not available
Not available

Not available

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";
Not available
Not available
Not available

Not available
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');

Not available
Not available
Not available

Not available

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();
    }
}
Not available
Not available
Not available

Not available

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());
Not available
Not available
Not available

Not available

Finally, we connect to the server via the open() method.

await client.open(null);

Not available
Not available
Not available

Not available

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

Not available
Not available
Not available

Not available

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);
Not available
Not available
Not available

Not available

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

Not available
Not available
Not available

Not available

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
// 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.cs
Not available
summator.pb.go
Not available
summator.pb.dart
Not available
summator.pbenum.dart
Not available
summator2_pb2.py

Not available
summator_grpc_pb.js
// 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);


SummatorGrpc.cs
Not available
summator_grpc.pb.go
Not available
summator.pbgrpc.dart
Not available
summator.pbjson.dart
Not available
summator2_pb2_grpc.py

Not available

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;
    }
}

Not available
Not available
Not available

Not available

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.