Getting started with gRPC on Java

This article is an introduction to gRPC framework which allows to connect services across data centers using high performance Remote Procedure Calls (RPC). To learn the building blocks of this framework, we will show how to create and test your first gRPC application in Java.

Overview of gRPC

Firstly, some basic concepts: in gRPC, a client application can directly call a method on a remote machine as if it were a local object. More in detail:

  • One the server side: the server implements an interface and runs a gRPC server to handle client calls.
  • On the client side: the client uses a stub (referred to as just a client in some languages) that provides the same methods as the server.

As you can guess from the above description, gRPC has some similarities with other frameworks for remote procedure calls such as EJBs.

For example, just like EJBs the gRPC framework is:

  • Free & open: You can use it in free and opensource products
  • Blocking & non-blocking: support both asynchronous and synchronous processing of the sequence of messages exchanged by a client and server.
  • Interoperability & reach: the wire-protocol must be capable of surviving traversal over common networks. (For example, with WildFly you can use HTTP as the transport (instead of remoting) for remote EJB.

On the other hand, the gRPC framework has some peculiar aspects:

  • Payload agnostic: different services need to use different message types and encodings such as protocol buffers, JSON, XML, and Thrift; the protocol and implementations must allow for this.
  • Layered: key components of the stack must be able to evolve independently. A revision to the wire-format should not disrupt application layer bindings
  • Service oriented vs Object oriented: promote the microservices design philosophy of coarse-grained message exchange between systems.
  • Flow-control: Flow control allows for better buffer management as well as providing protection from DOS by an overly active peer.
  • Pluggable: The wire protocol is only part of a functioning API infrastructure. Large distributed systems need security, health-checking, load-balancing and HA, monitoring, tracing, logging, and so on. Implementations should provide extensions points to cover these concers.

Creating a sample application

Creating a server and client with gRPC is very simple and following are the steps:

  • Create the service definition and payload structure in the Protocol Buffer (.proto) file.
  • Generate the gRPC code from the .proto file by compiling it using protoc
  • Implement the server in one of the supported languages.
  • Create the client that invokes the service through the Stub.
  • Run the server and client(s).

gRPC can use Protocol Buffers as both its Interface Definition Language (IDL) and as its underlying message interchange format. Protocol buffers are Google’s language-neutral, platform-neutral, extensible mechanism for serializing structured data.

This is the structure of a typical gRPC application which uses Java and Maven to build it:

    src
    └── main
        ├── java
        │   └── io
        │       └── grpc
        │           └── example
        │               └── filesystem
        │                   ├── ExampleClient.java
        │                   ├── ExampleServerImpl.java
        │                   └── ExampleServer.java
        └── proto
            └── filesystem
                └── example.proto

As you can see, in the src/main/proto you can find the protocol buffer file which defines the interface for your sample FileManager Service:

syntax = "proto3";

option java_multiple_files = true;
option java_package = "com.mastertheboss.filesystem";
option objc_class_prefix = "HLW";

package filesystem;

service FileManager {
  rpc ReadDir (Directory) returns (FileList) {}
}

// incoming request
message Directory {
   string name = 1;
}

// outgoing response
message FileList {
   string list = 1;
}
  • Since we are using Java code in our example, we have set the package for it in the java_package file option.
  • To define a service, we specify a named service “FileManager” in the .proto file. This service will barely return the directory list from a given file system path.
  • Then, we define rpc methods inside our service definition, specifying their request and response types. In this basic example, we are defining a simple RPC where the client sends a request (Directory) to the server using the stub and waits for a response (FileList) to come back, just like a normal function call.
  • Finally, our .proto file also contains protocol buffer message type definitions for all the request and response types used in our service methods.

Bootstraping the gRPC Server

Firstly, we need a Server for listening for and dispatching incoming gRPC calls:

package io.grpc.example.filesystem;

import io.grpc.Server;
import io.grpc.ServerBuilder;
import io.grpc.health.v1.HealthCheckResponse.ServingStatus;
import io.grpc.protobuf.services.ProtoReflectionService;
import io.grpc.services.HealthStatusManager;
import java.io.IOException;
import java.util.concurrent.TimeUnit;


public final class ExampleServer {
  public static void main(String[] args) throws IOException, InterruptedException {
    int port = 50051;
    String hostname = null;
    if (args.length >= 1) {
      try {
        port = Integer.parseInt(args[0]);
      } catch (NumberFormatException ex) {
        System.err.println("Usage: [port [hostname]]");
        System.err.println("");
        System.err.println("  port      The listen port. Defaults to " + port);
        System.err.println("  hostname  The name clients will see in greet responses. ");
        System.err.println("            Defaults to the machine's hostname");
        System.exit(1);
      }
    }
    if (args.length >= 2) {
      hostname = args[1];
    }
    HealthStatusManager health = new HealthStatusManager();
    final Server server = ServerBuilder.forPort(port)
        .addService(new ExampleServerImpl())
        .addService(ProtoReflectionService.newInstance())
        .addService(health.getHealthService())
        .build()
        .start();
    System.out.println("Listening on port " + port);
    Runtime.getRuntime().addShutdownHook(new Thread() {
      @Override
      public void run() {
        // Start graceful shutdown
        server.shutdown();
        try {
          // Wait for RPCs to complete processing
          if (!server.awaitTermination(30, TimeUnit.SECONDS)) {
            // That was plenty of time. Let's cancel the remaining RPCs
            server.shutdownNow();
            // shutdownNow isn't instantaneous, so give a bit of time to clean resources up
            // gracefully. Normally this will be well under a second.
            server.awaitTermination(5, TimeUnit.SECONDS);
          }
        } catch (InterruptedException ex) {
          server.shutdownNow();
        }
      }
    });

    health.setStatus("", ServingStatus.SERVING);
    server.awaitTermination();
  }
}

As you can see from the above code, the Server does the following:

  • Bootstraps the gRPC server the port 50051
  • Attach a list of Services that the server will be listening to. In our case, the ExampleServerImpl contains the actual implementation for the FileManager Service.
  • Handles a graceful shutdown of the service

Coding the Service implementation

Next, we will code the ExampleServerImpl which implements the methods declared by the service and runs a gRPC server to handle client calls. The gRPC infrastructure decodes incoming requests, executes service methods, and encodes service responses.

package io.grpc.example.filesystem;

import com.mastertheboss.filesystem.FileManagerGrpc;
import com.mastertheboss.filesystem.Directory;
import com.mastertheboss.filesystem.FileList;

import io.grpc.stub.StreamObserver;
import java.io.IOException;
import java.net.InetAddress;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.io.File;


public final class ExampleServerImpl extends FileManagerGrpc.FileManagerImplBase {
  private static final Logger logger = Logger.getLogger(ExampleServerImpl.class.getName());

  @Override
  public void readDir(Directory req, StreamObserver<FileList> responseObserver)  {
    String content = null;

      File f = new File(req.getName());
      if (!f.isDirectory())  {
          throw new RuntimeException(req.getName() + " is not a directory.");
      }
      String[] pathnames = f.list();
      StringBuffer sb = new StringBuffer();

        for (String pathname : pathnames) {
            sb.append(pathname);
            sb.append("\n");
        }
      content = sb.toString();

    FileList reply = FileList.newBuilder()
        .setList(content)
        .build();
    responseObserver.onNext(reply);
    responseObserver.onCompleted();
  }

}

As you can see, this class references a set of Java Classes which are not included in our project: com.mastertheboss.filesystem.FileManagerGrpc, com.mastertheboss.filesystem.Directory, com.mastertheboss.filesystem.FileList. This is where tools come into play.

As a matter of fact, gRPC provides protocol buffer compiler plugins that generate client and server-side code. In our case, the creation of these objects is delegated to the protobuf-maven-plugin which in turn invokes grpc-java tool. More details about this part later.

The implementation of the readDir takes two parameters:

  • Directory: the request
  • StreamObserver: a response observer, which is a special interface for the server to call with its response

Coding the Client

On the client side, the client uses a local object ( aka as stub ) that implements the same methods as the service. The client can then just call those methods on the local object, wrapping the parameters for the call in the appropriate protocol buffer message type. Then, the gRPC framework looks after sending the request(s) to the server and returning the server’s protocol buffer response(s):

package io.grpc.example.filesystem;


import io.grpc.Channel;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.StatusRuntimeException;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.mastertheboss.filesystem.FileManagerGrpc;
import com.mastertheboss.filesystem.Directory;
import com.mastertheboss.filesystem.FileList;

import io.grpc.inprocess.InProcessChannelBuilder;
import io.grpc.inprocess.InProcessServerBuilder;

public class ExampleClient {
  private static final Logger logger = Logger.getLogger(ExampleClient.class.getName());

  private final FileManagerGrpc.FileManagerBlockingStub blockingStub;

  public ExampleClient(Channel channel) {

    blockingStub = FileManagerGrpc.newBlockingStub(channel);
  }


  public void getFileList(String dir) {

    Directory request = Directory.newBuilder().setName(dir).build();
    FileList response;
    try {
      response = blockingStub.readDir(request);
    } catch (StatusRuntimeException e) {
      logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
      return;
    }
    logger.info("Response: " + response.getList());
  }


  public static void main(String[] args) throws Exception {
    if (args.length == 0) {
       System.out.println("Proper Usage is: java ExampleClient directory");
       System.exit(0);
    }
    // Access a service running on the local machine on port 50051
    String target = "localhost:50051";

    ManagedChannel channel = ManagedChannelBuilder.forTarget(target)

        .usePlaintext()
        .build();
    try {
      ExampleClient client = new ExampleClient(channel);
      client.getFileList(args[0]);
    } finally {
      channel.shutdownNow().awaitTermination(5, TimeUnit.SECONDS);
    }
  }
}

Overall, this is the simplest use case of an RPC application:

  • Firstly, a synchronous stub is created (FileManagerGrpc)
  • Then, the client creates a communication channel to the server, known as a Channel
  • Next, the client calls a stub method (getFileList) using plain text format
  • Once the server has the client’s request message, it does whatever is necessary to create and populate a response. The response is then returned (if successful) to the client together with status details (status code and optional status message).
  • If the response status is OK, then the client gets the response, which completes the call on the client side

Building the application

To build our application, we need to include the io.grpc libraries in our project. By using the grpc-bom BOM you can easily keep in sync the dependencies you need for your project:

<dependencyManagement>
<dependencies>
  <dependency>
    <groupId>io.grpc</groupId>
    <artifactId>grpc-bom</artifactId>
    <version>${grpc.version}</version>
    <type>pom</type>
    <scope>import</scope>
  </dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
  <groupId>io.grpc</groupId>
  <artifactId>grpc-protobuf</artifactId>
</dependency>
<dependency>
  <groupId>io.grpc</groupId>
  <artifactId>grpc-stub</artifactId>
</dependency>
<dependency>
  <groupId>io.grpc</groupId>
  <artifactId>grpc-services</artifactId>
</dependency>

<dependency>
  <groupId>org.apache.tomcat</groupId>
  <artifactId>annotations-api</artifactId>
  <version>6.0.53</version>
  <scope>provided</scope>
</dependency>

<dependency>
  <groupId>io.grpc</groupId>
  <artifactId>grpc-netty-shaded</artifactId>
  <scope>runtime</scope>
</dependency>
</dependencies>

Finally, we need some tooling to generate the stubs which are needed for the client-server communication. For this purpose, we will add the following plugin in our pom.xml that will execute upon compilation of the classes:

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

Testing the application

To test the application, from a Command Line, build the artifacts and generate the stubs with:

mvn clean verify

You will see in the build logs that protobuf-maven-plugin generated the project stubs:

[INFO] --- protobuf-maven-plugin:0.6.1:compile (default) @ example-filesystem ---
[INFO] Compiling 1 proto file(s) to /home/francesco/git/grpc-java-1.50.2/examples/example-hostname/target/generated-sources/protobuf/java
[INFO] 
[INFO] --- protobuf-maven-plugin:0.6.1:compile-custom (default) @ example-filesystem ---
[INFO] Compiling 1 proto file(s) to /home/francesco/git/grpc-java-1.50.2/examples/example-hostname/target/generated-sources/protobuf/grpc-java

Then start the gRPC server as follows:

mvn exec:java -Dexec.mainClass=io.grpc.example.filesystem.ExampleServer

Finally, run the Client application with:

mvn exec:java -Dexec.mainClass=io.grpc.example.filesystem.ExampleClient -Dexec.args="/tmp"

As a result, you should see that the Client application prints the content of the /tmp directory on the console

Conclusion
This article was a walk through a simple gRPC application covering the basic building blocks and a simple application. In the next article we will learn how to use Quarkus as RPC Server cutting down the time you will need to create your application.

Source code for this article: https://github.com/fmarchioni/mastertheboss/tree/master/various/example-rpc