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
andFeature
that are data structures exchanged between the client and server when using theGetFeature
method. The client provides map coordinates as aPoint
in itsGetFeature
request to the server, and the server replies with a correspondingFeature
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 forRouteGuide
servers to implement,RouteGuideGrpc.RouteGuideImplBase
, with all the methods defined in theRouteGuide
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:
- 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 privatecheckFeature()
method. - We use the response observer's
onNext()
method to return theFeature
. - 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:
- Specify the port we want to use to listen for client requests using the builder's
forPort()
method (it will use the wildcard address). - Create an instance of our service implementation class
RouteGuideService
and pass it to the builder'saddService()
method. - 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:
- Create a
RouteGuideServer
instance. - Call
start()
to activate an RPC server for our service. - 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!
- 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.
- Run the server:
$ ./build/install/start_here/bin/route-guide-server
- 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
- Learn how gRPC works in Introduction to gRPC and Core concepts
- Work through the Basics tutorial
- Explore the API reference.