Introduction to gRPC

1. Introduction

gRPC is a high performance, open source RPC framework initially developed by Google. It helps in eliminating boilerplate code and helps in connecting polyglot services in and across data centers.

2. Overview

The framework is based on a client-server model of remote procedure calls. A client application can directly call methods on a server application as if it was a local object.

This article will use following steps to create a typical client-server application using gRPC:

  1. Define a service in a .proto file

  2. Generate server and client code using the protocol buffer compiler

  3. Create the server application, implementing the generated service interfaces and spawning the gRPC server

  4. Create the client application, making RPC calls using generated stubs

Let’s define a simple HelloService which returns greetings in exchange for the first and the last name.

3. Maven Dependencies

Let’s add grpc-netty, grpc-protobuf and grpc-stub dependencies:

<dependency>
    <groupId>io.grpc</groupId>
    <artifactId>grpc-netty</artifactId>
    <version>1.16.1</version>
</dependency>
<dependency>
    <groupId>io.grpc</groupId>
    <artifactId>grpc-protobuf</artifactId>
    <version>1.16.1</version>
</dependency>
<dependency>
    <groupId>io.grpc</groupId>
    <artifactId>grpc-stub</artifactId>
    <version>1.16.1</version>
</dependency>

4. Defining the Service

We start by defining a service, specifying methods that can be called remotely along with their parameters and return types.

This is done in the .proto file using the protocol buffers. They are also used for describing the structure of the payload messages.

4.1. Basic Configurations

Let us create a HelloService.proto file for our sample HelloService. We start by adding few basic configuration details:

syntax = "proto3";
option java_multiple_files = true;
package org.baeldung.grpc;

The first line tells the compiler what syntax is used in this file. By default, the compiler generates all the Java code in a single Java file. The second line overrides this setting, and everything will be generated in individual files.

Finally, we specify the package we want to use for our generated Java classes.

4.2. Defining the Message Structure

Next, we define the message:

message HelloRequest {
    string firstName = 1;
    string lastName = 2;
}

This defines the request payload. Here each attribute that goes into the message is defined along with its type.

A unique number needs to be assigned to each attribute, called as the tag. This tag is used by the protocol buffer to represent the attribute instead of using the attribute name.

So, unlike JSON where we would pass attribute name firstName every single time, protocol buffer would use the number 1 to represent firstName. Response payload definition is similar to the request.

Note that we can use the same tag across multiple message types:

message HelloResponse {
    string greeting = 1;
}

4.3. Defining the Service Contract

Finally, let’s define the service contract. For our HelloService we define a hello() operation:

service HelloService {
    rpc hello(HelloRequest) returns (HelloResponse);
}

The hello() operation accepts a unary request and returns a unary response. gRPC also supports streaming by prefixing stream keyword to the request and response.

5. Generating the Code

Now we pass the HelloService.proto file to the protocol buffer compiler protoc to generate the Java files. There are multiple ways to trigger this.

5.1. Using Protocol Buffer Compiler

Download the compiler and follow the instructions in the README file.

You can use the following command to generate the code:

protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/HelloService.proto

5.2. Using Maven Plugin

As a developer, you would want the code generation to be tightly integrated with your build system. gRPC provides a protobuf-maven-plugin for the Maven build system:

<build>
  <extensions>
    <extension>
      <groupId>kr.motd.maven</groupId>
      <artifactId>os-maven-plugin</artifactId>
      <version>1.6.1</version>
    </extension>
  </extensions>
  <plugins>
    <plugin>
      <groupId>org.xolstice.maven.plugins</groupId>
      <artifactId>protobuf-maven-plugin</artifactId>
      <version>0.6.1</version>
      <configuration>
        <protocArtifact>
          com.google.protobuf:protoc:3.3.0:exe:${os.detected.classifier}
        </protocArtifact>
        <pluginId>grpc-java</pluginId>
        <pluginArtifact>
          io.grpc:protoc-gen-grpc-java:1.4.0:exe:${os.detected.classifier}
        </pluginArtifact>
      </configuration>
      <executions>
        <execution>
          <goals>
            <goal>compile</goal>
            <goal>compile-custom</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

The os-maven-plugin extension/plugin generates various useful platform-dependent project properties like $\{os.detected.classifier}

6. Creating the Server

Irrespective of which method you use for code generation, following key files will be generated:

  • HelloRequest.java – contains the HelloRequest type definition

  • HelloResponse.java *–* this contains the HelleResponse type definition

  • HelloServiceImplBase.java *–* this contains the abstract class HelloServiceImplBase which provides an implementation of all the operations we defined in the service interface

6.1. Overriding the Service Base Class

The default implementation of the abstract class HelloServiceImplBase is to throw runtime exception io.grpc.StatusRuntimeException saying that the method is unimplemented.

We shall extend this class and override the hello() method mentioned in our service definition:

public class HelloServiceImpl extends HelloServiceImplBase {

    @Override
    public void hello(
      HelloRequest request, StreamObserver<HelloResponse> responseObserver) {

        String greeting = new StringBuilder()
          .append("Hello, ")
          .append(request.getFirstName())
          .append(" ")
          .append(request.getLastName())
          .toString();

        HelloResponse response = HelloResponse.newBuilder()
          .setGreeting(greeting)
          .build();

        responseObserver.onNext(response);
        responseObserver.onCompleted();
    }
}

If we compare the signature of hello() with the one we wrote in the HellService.proto file, we’ll notice that it does not return HelloResponse. Instead, it takes the second argument as StreamObserver<HelloResponse>, which is a response observer, a call back for the server to call with its response.

This way the client gets an option to make a blocking call or a non-blocking call.

gRPC uses builders for creating objects. We use HelloResponse.newBuilder() and set the greeting text to build a HelloResponse object. We set this object to the responseObserver’s onNext() method to send it to the client.

Finally, we need to call onCompleted() to specify that we’ve finished dealing with the RPC, else the connection will be hung, and the client will just wait for more information to come in.

6.2. Running the Grpc Server

Next, we need to start the gRPC server to listen for incoming requests:

public class GrpcServer {
    public static void main(String[] args) {
        Server server = ServerBuilder
          .forPort(8080)
          .addService(new HelloServiceImpl()).build();

        server.start();
        server.awaitTermination();
    }
}

Here, again we use the builder to create a gRPC server on port 8080 and add the HelloServiceImpl service that we defined. start() would start the server. In our example, we will call awaitTermination() to keep the server running in foreground blocking the prompt.

7. Creating the Client

gRPC provides a channel construct which abstracts out the underlying details like connection, connection pooling, load balancing, etc.

We’ll create a channel using ManagedChannelBuilder. Here, we specify the server address and port.

We’ll be using plain text without any encryption:

public class GrpcClient {
    public static void main(String[] args) {
        ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 8080)
          .usePlaintext()
          .build();

        HelloServiceGrpc.HelloServiceBlockingStub stub
          = HelloServiceGrpc.newBlockingStub(channel);

        HelloResponse helloResponse = stub.hello(HelloRequest.newBuilder()
          .setFirstName("Baeldung")
          .setLastName("gRPC")
          .build());

        channel.shutdown();
    }
}

Next, we need to create a stub which we’ll use to make the actual remote call to hello(). The stub is the primary way for clients to interacts with the server. When using auto generate stubs, the stub class will have constructors for wrapping the channel.

Here we’re using a blocking/synchronous stub so that the RPC call waits for the server to respond, and will either return a response or raise an exception. There are two other types of stubs provided by gRPC, which facilitate non-blocking/asynchronous calls.

Finally, time to make the hello() RPC call. Here we pass the HelloRequest. We can use the auto generated setters to set the firstName, lastName attributes of the HelloRequest object.

We get back the HelloResponse object returned from the server.

8. Conclusion

In this tutorial, we saw how we could use gRPC to ease the development of communication between two services by focusing on defining the service and letting the gRPC to handle all the boilerplate code.

As usual, you’ll find the sources over on GitHub.

Leave a Reply

Your email address will not be published.