Getting Started with gRPC-Java

1. Introduction

In this codelab, you'll use gRPC-Java to create a client and server that form the foundation of a route-mapping application written in Java.

By the end of the tutorial, you will have a client that connects to a remote server using gRPC to get the name or postal address of what's located at specific coordinates on a map. A fully fledged application might use this client-server design to enumerate or summarize points of interest along a route.

The server's API is defined in a Protocol Buffers file, which will be used to generate boilerplate code for the client and server so that they can communicate with each other, saving you time and effort in implementing that functionality.

This generated code takes care of not only the complexities of the communication between the server and client, but also data serialization and deserialization.

What you'll learn

  • How to use Protocol Buffers to define a service API.
  • How to build a gRPC-based client and server from a Protocol Buffers definition using automated code generation.
  • An understanding of client-server communication with gRPC.

This codelab is aimed at Java developers new to gRPC or seeking a refresher of gRPC, or anyone else interested in building distributed systems. No prior gRPC experience is required.

2. Before you begin

Prerequisites

  • JDK version 8 or higher

Get the code

So that you don't have to start entirely from scratch, this codelab provides a scaffold of the application's source code for you to complete. The following steps will show you how to finish the application, including using the protocol buffer compiler plugins to generate the boilerplate gRPC code.

First, create the codelab working directory and cd into it:

mkdir grpc-java-getting-started && cd grpc-java-getting-started

Download and extract the codelab:

curl -sL https://github.com/grpc-ecosystem/grpc-codelabs/archive/refs/heads/v1.tar.gz \
  | tar xvz --strip-components=4 \
  grpc-codelabs-1/codelabs/grpc-java-getting-started/start_here

Alternatively, you can download the .zip file containing only the codelab directory and manually unzip it.

The completed source code is available on GitHub if you want to skip typing in an implementation.

3. Define the service

Your first step is to define the application's gRPC service, its RPC method, and its request and response message types using Protocol Buffers. Your service will provide:

  • An RPC method called GetFeature that the server implements and the client calls.
  • The message types Point and Feature that are data structures exchanged between the client and server when using the GetFeature method. The client provides map coordinates as a Point in its GetFeature request to the server, and the server replies with a corresponding Feature that describes whatever is located at those coordinates.

This RPC method and its message types will all be defined in the src/main/proto/routeguide/route_guide.proto file of the provided source code.

Protocol Buffers are commonly known as protobufs. For more information on gRPC terminology, see gRPC's Core concepts, architecture, and lifecycle.

Since we're generating Java code in this example, we've specified a java_package file option and a name for the Java class in our .proto:

option java_package = "io.grpc.examples.routeguide";
option java_outer_classname = "RouteGuideProto";

Message types

In the routeguide/route_guide.proto file of the source code, first define the Point message type. A Point represents a latitude-longitude coordinate pair on a map. For this codelab, use integers for the coordinates:

message Point {
  int32 latitude = 1;
  int32 longitude = 2;
}

The numbers 1 and 2 are unique ID numbers for each of the fields in the message structure.

Next, define the Feature message type. A Feature uses a string field for the name or postal address of something at a location specified by a Point:

message Feature {
  // The name or address of the feature.
  string name = 1;

  // The point where the feature is located.
  Point location = 2;
}

Service method

The route_guide.proto file has a service structure named RouteGuide that defines one or more methods provided by the application's service.

Add the rpc method GetFeature inside the RouteGuide definition. As explained earlier, this method will look up the name or address of a location from a given set of coordinates, so have GetFeature return a Feature for a given Point:

service RouteGuide {
  // Definition of the service goes here

  // Obtains the feature at a given position.
  rpc GetFeature(Point) returns (Feature) {}
}

This is a unary RPC method: a simple RPC where the client sends a request to the server and waits for a response to come back, just like a local function call.

4. Generate client and server code

Next we need to generate the gRPC client and server interfaces from our .proto service definition. We do this using the protocol buffer compiler protoc with a special gRPC Java plugin. You need to use the proto3 compiler (which supports both proto2 and proto3 syntax) in order to generate gRPC services.

When using Gradle or Maven, the protoc build plugin can generate the necessary code as part of the build. You can refer to the grpc-java README for how to generate code from your own .proto files.

We have provided a Gradle environment and configuration in the codelab's source code to build this project.

Inside the grpc-java-getting-started directory, run the following command:

$ chmod +x gradlew
$ ./gradlew generateProto

The following classes are generated from our service definition:

  • Feature.java, Point.java, and others that contain all the protocol buffer code to populate, serialize, and retrieve our request and response message types.
  • RouteGuideGrpc.java which contains (along with some other useful code) a base class for RouteGuide servers to implement, RouteGuideGrpc.RouteGuideImplBase, with all the methods defined in the RouteGuide service and stub classes for clients to use.

5. Implement the server

First let's look at how we create a RouteGuide server. There are two parts to making our RouteGuide service do its job:

  • Implementing the service interface generated from our service definition, which does the actual "work" of our service.
  • Running a gRPC server to listen for requests from clients and dispatch them to the right service implementation.

Implement RouteGuide

As you can see, our server has a RouteGuideService class that extends the generated RouteGuideGrpc.RouteGuideImplBase abstract class:

private static class RouteGuideService extends RouteGuideGrpc.RouteGuideImplBase {
...
}

We have provided the following 2 files for initializing your server with features:

./src/main/java/io/grpc/examples/routeguide/RouteGuideUtil.java

./src/main/resources/io/grpc/examples/routeguide/route_guide_db.json

Let's look into a simple RPC implementation in detail.

Unary RPC

RouteGuideService implements all our service methods. In this case it is just GetFeature(), takes a Point message from the client and returns in a Feature message the corresponding location information from a list of known places.

@Override
public void getFeature(Point request, StreamObserver<Feature> responseObserver) {
  responseObserver.onNext(checkFeature(request));
  responseObserver.onCompleted();
}

The getFeature() method takes two parameters:

  • Point: the request.
  • StreamObserver<Feature>: a response observer, which is a special interface for the server to call with its response.

To return our response to the client and complete the call:

  1. We construct and populate a Feature response object to return to the client, as specified in our service definition. In this example, we do this in a separate private checkFeature() method.
  2. We use the response observer's onNext() method to return the Feature.
  3. We use the response observer's onCompleted() method to specify that we've finished dealing with the RPC.

Start the server

Once we've implemented all our service methods, we need to start up a gRPC server so that clients can actually use our service. We include in our boilerplate the creation of the ServerBuilder object:

ServerBuilder.forPort(port), port, RouteGuideUtil.parseFeatures(featureFile)

We build the service in the constructor:

  1. Specify the port we want to use to listen for client requests using the builder's forPort() method (it will use the wildcard address).
  2. Create an instance of our service implementation class RouteGuideService and pass it to the builder's addService() method.
  3. Call build() on the builder to create an RPC server for our service.

The following snippet shows how we create a ServerBuilder object.

/** Create a RouteGuide server listening on {@code port} using {@code featureFile} database. */
public RouteGuideServer(int port, URL featureFile) throws IOException {
    this(Grpc.newServerBuilderForPort(port, InsecureServerCredentials.create()),
        port, RouteGuideUtil.parseFeatures(featureFile));
  }

The following snippet shows how we create a server object for our RouteGuide service.

/** Create a RouteGuide server using serverBuilder as a base and features as data. */
public RouteGuideServer(ServerBuilder<?> serverBuilder, int port, Collection<Feature> features) {
  this.port = port;
  server = serverBuilder.addService(new RouteGuideService(features))
      .build();
}

Implement a start method that calls start on the server we created above.

public void start() throws IOException {
  server.start();
  logger.info("Server started, listening on " + port);
}

Implement a method to wait for the server to complete so it doesn't immediately exit.

/** Await termination on the main thread since the grpc library uses daemon threads. */
private void blockUntilShutdown() throws InterruptedException {
  if (server != null) {
    server.awaitTermination();
  }
}

As you can see, we build and start our server using a ServerBuilder.

In the main method we:

  1. Create a RouteGuideServer instance.
  2. Call start() to activate an RPC server for our service.
  3. Wait for the service to be stopped by calling blockUntilShutdown().
 public static void main(String[] args) throws Exception {
    RouteGuideServer server = new RouteGuideServer(8980);
    server.start();
    server.blockUntilShutdown();
  }

6. Create the client

In this section, we'll look at creating a client for our RouteGuide service.

Instantiate a stub

To call service methods, we first need to create a stub. There are two types of stubs, but we only need to use the blocking one for this codelab. The two types are:

  • a blocking/synchronous stub that makes an RPC call and waits for the server to respond, and will either return a response or raise an exception.
  • a non-blocking/asynchronous stub that makes non-blocking calls to the server, where the response is returned asynchronously. You can make certain types of streaming calls only by using the asynchronous stub.

First we need to create a gRPC channel and then use the channel to create our stub.

We could have used a ManagedChannelBuilder directly to create the channel.

ManagedChannelBuilder.forAddress(host, port).usePlaintext().build

But let's use a utility method that takes a string with hostname:port.

Grpc.newChannelBuilder(target, InsecureChannelCredentials.create()).build();

Now we can use the channel to create our blocking stub. For this codelab, we only have blocking RPCs, so we use the newBlockingStub method provided in the RouteGuideGrpc class we generated from our .proto.

blockingStub = RouteGuideGrpc.newBlockingStub(channel);

Call service methods

Now let's look at how we call our service methods.

Simple RPC

Calling the simple RPC GetFeature is nearly as straightforward as calling a local method.

We create and populate a request protocol buffer object (in our case Point), pass it to the getFeature() method on our blocking stub, and get back a Feature.

If an error occurs, it is encoded as a Status, which we can obtain from the StatusRuntimeException.

Point request = Point.newBuilder().setLatitude(lat).setLongitude(lon).build();

Feature feature;
try {
  feature = blockingStub.getFeature(request);
} catch (StatusRuntimeException e) {
  logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
  return;
}

The boilerplate logs a message containing the contents based on whether or not there was a feature at the specified point.

7. Try it out!

  1. Inside the start_here directory, run the following command:
$ ./gradlew installDist

This will compile your code, package it in a jar and create the scripts that run the example. They will be created in the build/install/start_here/bin/ directory. The scripts are: route-guide-server and route-guide-client.

The server needs to be running before starting the client.

  1. Run the server:
$ ./build/install/start_here/bin/route-guide-server
  1. Run the client:
$ ./build/install/start_here/bin/route-guide-client

You'll see output like this, with timestamps omitted for clarity:

INFO: *** GetFeature: lat=409,146,138 lon=-746,188,906
INFO: Found feature called "Berkshire Valley Management Area Trail, Jefferson, NJ, USA" at 40.915, -74.619
INFO: *** GetFeature: lat=0 lon=0
INFO: Found no feature at 0, 0

8. What's next