Getting started with gRPC. Part 1

Motivation and target audience

There are several blog posts on the internet touching one or several shiny features that gRPC framework boasts about. There is also the official documentation from gRPC. It is very easy to get lost in all the jargons, terminology and the code examples. This ends up making readers confused and frustrated with a very limited understanding of what gRPC offers.

The motivation behind writing this blog post is to gently introduce the gRPC framework to interested developers and architects, skipping the more advanced details. This blog starts by setting up an initial context about the gRPC framework, followed by discussing a few of its promising features. We then highlight typical applications that can be build using gRPC and introduce the readers to a simple workflow for a small to medium scale project. Wherever possible, we have provided references to more detailed sources.

This blog focusses only on introducing gRPC as a concept and provide a convenient starting point. The code discussed in this example can be found on github.

Repository

The examples provided in this blog are based on this Github repository.

You should use this blog post to grab the gRPC concepts and the Github repository to validate the concepts you have learned in this blog post. The code-base contains the instructions on how to run the server and the client in README and provides ample comments wherever possible.

Introduction

gRPC is an RPC platform developed by Google which was made open source (Apache 2.0 license) in early 2015. The first public version (1.0.0) was released in August 2016.

The framework consists of two components:

gRPC client-server communication Source

Using these two components, gRPC enables applications to communicate over HTTP/2 tcp connection while sending data in binary encoded format. This is a great way to create microservices and systems that scale very well horizontally. Moreover, the framework is highly performant, avoids the need to write boilerplate code and works seamlessly on the cloud without any hacks. One of the most promising features of gRPC is that it is programing language agnostic. You can start playing around with gRPC with any programming language of your choice.

From gRPC website:

gRPC is a modern, open source remote procedure call (RPC) framework that can run anywhere. It enables client and server applications to communicate transparently and makes it easier to build connected systems.

Content:

  1. What is gRPC and why would we use it? 1.1. Features. 1.2. Supported programming languages. 1.3. Supported data formats. 1.4. Typical applications that can be built with gRPC.

  2. Fitting pieces together. [General workflow for a simple project] 2.1. Defining payload and service definition. 2.2. Generate gRPC code from protobuf definition. 2.3. Implement gRPC server. 2.4. Implement gRPC client. 2.5. Run the server and call it using a client.

  3. Where to go next.
  4. Sources and References.

1. What is gRPC and why would we use it?

If you have worked with REST APIs, what you are generally used to is:

REST API = HTTP1.1 + JSON + REST

where HTTP1.1 is the transport protocol, JSON is message format, and REST is the architectural style (resourceful endpoint).

gRPC handles the same scenario as:

gRPC API = HTTP/2 + Protobuf + RPC

Here the transport protocol is based on HTTP/2, the data format is defined using Protobuf and architectural style is RPC.

Few advantages of this approach are:

References:

1.1. Features.

Apart from providing a robust framework for client-server communication, gRPC provides a few other features which make it an indispensable choice for modern application development.

1.2. Supported programming languages.

Below are the officially supported programming languages:

Other third-party supported languages:

1.3. Supported data formats.

By default, gRPC uses protocol buffers, Google’s mature open source mechanism for serializing structured data. However, since protocol buffers act as serialization layer, it is possible to replace it with other content types or provide your own implementations.

From gRPC website:

gRPC is designed to be extensible to support multiple content types. The initial release contains support for Protobuf and with external support for other content types such as FlatBuffers and Thrift, at varying levels of maturity.

References:

1.4. Typical applications that can be built with gRPC.

The main usage scenario for gRPC are:

2. Fitting pieces together.

A typical workflow for creating gRPC services is defined below. These steps show how protobuf, gRPC and your favorite programming language fit together.

Let us discuss these steps in more detail in the next few sections.

2.1. Define payload and service definition.

In any typical gRPC project, we start with defining our payload in protobuf format. The payload will define our request and response structures and will be stored in .proto files (For eg invoice.proto ). An important thing to note here is that each field in our payload has a data-type, name, and id. Doing this ensures that our payload is strongly typed and the protocol reports an error during compile time if the client uses the wrong datatype while invoking methods on the server. Protobuf uses the term message to define a payload. Below we have defined a message called Invoice with fields id, amount and customerId.

message Invoice {
    string id = 1;
    float amount = 2;
    string customerId = 3;
}

Next, we define our service and the method it exposes. The arguments and return types will always be a message that we have defined earlier. Below we have defined a service called InvoiceService and defined a method (or operation) called Pay. Pay method accepts an input of type Invoice and has a return type as Invoice.

service InvoiceService {
    rpc Pay(Invoice) returns (Invoice);
}

Once we generate code from protobuf, these strongly typed methods will be available for our client and server for implementation. This will ensure that both the client and the server stubs adhere to the contract defined in protobuf.

Before proceeding further, let us take a quick look at what behaviors can our operations possess:

One thing to keep in mind while creating APIs is ensuring their forward and backward compatibilities. It is important that any minor API changed should not break the existing clients. Protobuf provides a few guidelines to seamlessly evolve your APIs. Below mentioned reference will point you in the correct direction if you want to learn more about these concerns:

References below will provide more details on how to use protocol buffers to structure data.

2.2. Generate gRPC code from protobuf.

Now that we have written our payload and service definition, its time to generate code in our preferred programming language from protobuf. We will use Java as a programming language throughout this blog, but the process to generate code in other languages would just be just setting a flag away.

There are 2 major ways to generate code from protobuf:

A good practice is to install protoc compiler and create shell/make scripts to generate the source code in your preferred language. We will cover that approach in one of the upcoming blogs in detail. In this blog, we will generate java code using gradle build system.

References:

2.3. Implementing gRPC Server.

The code generated in the previous section will have two java files, InvoiceServiceGrpc.java and InvoiceOuterClass.java. The former will contain the service definition, while the latter will have payload definition. Implementing the gRPC server will consist of 2 basic steps:

Below snippets show a quick and dirty example of implementing gRPC server.

/**Implementing contract**/
public class InvoiceServiceImpl extends InvoiceServiceGrpc.InvoiceServiceImplBase {
    public InvoiceServiceImpl() {}
    @Override
    public void pay(InvoiceOuterClass.Invoice request, StreamObserver<InvoiceOuterClass.Invoice> responseObserver) {
        // request argument represents typed client request
        // responseObserver is a means to control streaming behavior in gRPC

        // Use a builder to construct a new Protobuf object. Here we provide a custom business logic to increase amount by 1
        InvoiceOuterClass.Invoice response = InvoiceOuterClass.Invoice.newBuilder()
                .setAmount(request.getAmount() + 1)
                .setCustomerId(request.getCustomerId())
                .setId(request.getId())
                .build();

        // Use responseObserver to send a single response back
        responseObserver.onNext(response);

        // When you are done, you must call onCompleted.
        responseObserver.onCompleted();
    }
}
/**Bootstrapping the server and added the service implementation**/
public class Server {
    public static void main(String[] args) throws IOException, InterruptedException {
        io.grpc.Server server = ServerBuilder.forPort(8080)     // listen on port 8080
                .addService(new InvoiceServiceImpl())           // add service implementation
                .build();
        server.start();                                         // start server
        server.awaitTermination();                             
    }
}

2.4. Implementing gRPC Client.

Implementing the gRPC client consists of 2 steps.

Let’s have a look at the below snippet to understand the anatomy of the gRPC client.

/**Implementing a gRPC client**/
public class Client {
    public static void main(String[] args) throws InterruptedException {
        // 1. Creating a gRPC channel
        final ManagedChannel channel = ManagedChannelBuilder
                .forAddress("localhost", 8080)      // Set host and port
                .usePlaintext()                     // This setting should be used in dev. In prod, this should be replaced with TLS/Certificate
                .build();

        // 2. Create a synchronous stub
        final InvoiceServiceGrpc.InvoiceServiceBlockingStub stub = InvoiceServiceGrpc.newBlockingStub(channel);

        // 3. Prepare request
        final InvoiceOuterClass.Invoice request = InvoiceOuterClass.Invoice.newBuilder()
                .setAmount(100.00f)
                .setId("123")
                .setCustomerId("XYZ123")
                .build();

        // 4. Call the pay method on the stub
        final InvoiceOuterClass.Invoice response = stub.pay(request);
        System.out.println(response);

        // Finally close the channel
        channel.shutdown();
    }
}

2.5. Run the server and call it using client.

Now that we have completed the implementation for our client and server, its time to run them and check out results. For the purpose of this blog, we will run the client and server from our IDE and have a look at the output produced by our server.

Start the gRPC server by running the main method in Server.java Start Server

Now run the client by running the main method in Client.java Run Client

You can notice the difference in the value of the amount in the response. On the server, our business logic increments the amount by 1

Let’s check server logs that were generated while processing this request Server Logs

3. Where to go next?

The example presented in this blog is extremely basic and was intended to ease the learning curve for the beginners. It is focussed towards acquainting the readers with basic terminologies in gRPC and presenting a basic workflow to play around and get started with.

At this point we recommend you to clone the repository at Github and work through the instructions provided in README.md file. It prototypes a more advanced Invoice Generation system with step by step explanation on data-structure modeling for gRPC, setting up a java project for gRPC using Gradle and unit testing your gRPC code.

Now that we have a basic understanding of what gRPC is. We will provide more blog posts and examples covering different aspects like message modeling, validation, and error handling in gRPC, protoc compilation techniques, performance optimization etc.

4. Sources and References.