Understanding gRPC with Java and Gradle

Ashish Bhimte
Globant
Published in
11 min readJan 12, 2023
Photo by nik radzi on Unsplash

In this article, we’ll understand what is a trending technology, gRPC. We’ll also see how gRPC works, how it communicates, which protocol it uses, what are the different ways that gRPC provides to create an API, and what differences there are between gRPC and REST. We’ll also see some code examples to understand the concept.

Prerequisites: Basic understanding of Java, Gradle, and protocol buffers.

Microservice is the buzzword nowadays. Everyone is trying to use microservices to build simple to complex apps.

Microservices are independently manageable services. Using microservices architecture, we can develop an application that can latterly be upgraded by adding more functions or modules. But this also comes with some challenges. And one of the challenges is maintaining communication between these microservices.

Before 2016 most microservices were interacting using REST APIs, event streams, etc. But after 2016, things got changed, and developers started using gRPC for internal microservices communication. Why has this happened? We’ll discuss that later in this article.

In microservices, API building is hard

In microservices, API building is hard because while writing APIs, we need to think of all the complexities of a distributed system, such as network latency, scalability, load balancing, language interoperability, etc. And again, we need to think of authentication, monitoring, and logging.

Don’t you wish to focus on data and leave the rest to the framework?

This is where gRPC comes into the picture.

gRPC was developed by Google and introduced to the world in 2016. It is free, open source and a part of the Cloud Native Computer Foundation (CNCF), just like Docker and K8S.

gRPC is very famous now as it uses the protobuf and HTTP/2 protocol. The main advantage of gRPC is that it has low latency and supports streaming.

How HTTP/2 makes gRPC special?

HTTP/2 provides a long-lasting TCP connection shared by multiple requests. It supports server push (i.e., the server can push a response to multiple requests directly).

HTTP/2 supports multiplexing. The header and data in HTTP/2 are compressed in binary. It requires an SSL connection and works on less bandwidth with more security.

How does gRPC work?

It uses protocol buffers for communication. Protocol buffers have very small payload sizes, and that's why they are less CPU-intensive.

As in many RPC systems, gRPC is based on the idea of defining services and specifying methods that can be called remotely with their parameters and return types. On the server side, the server implements this interface and runs a gRPC server to handle client calls. On the client side, the client has a stub that provides the same methods as the server.

gRPC communication between server and client
Server and client communication using gRPC

gRPC clients and servers can run and talk to each from different environments. For instance, a gRPC server in Java can support clients in JavaScript, Java, or C#.

gRPC provides four types of service methods

1. Unary

In unary calls, the client sends one request, and the server sends only one response.

rpc sum(SumRequest) returns(SumResponse);

2. Server Streaming

HTTP/2 enables server streaming, which is used when the server sends multiple responses for one request.

Example: Getting a list of records in real-time after an update operation.

rpc prime(PrimeRequest) returns(stream PrimeResponse);

3. Client Streaming

HTTP/2 enables client streaming, In which the client sends multiple requests, and the server gives only one response.

Example: uploading/ updating data.

rpc avg(stream AvgRequest) returns(AvgResponse);

4. Bi-directional Streaming

In bi-directional streaming, the client sends multiple requests, and the server also sends multiple responses. After 1st request, the responses can be in any order you can decide.

rpc max(stream MaxRequest) returns(stream MaxResponse);

Differences between gRPC and REST API

  • gRPC uses protocol buffers that are less in size, whereas REST uses JSON, which can be huge in size.
  • gRPC uses HTTP/2 protocol which is very fast and supports streaming, whereas REST uses HTTP/1.1 and supports only unary calls.
  • gRPC supports the free design, where REST supports only predefined methods like GET, PUT, POST, DELETE, etc.
  • There is no browser support for gRPC where REST has browser support.

Let’s do some code to understand gRPC and its service methods

In this code example, we’ll perform simple arithmetic operations like the sum of two numbers, an average of numbers given, the max between two numbers and finding the prime factors of a number.

To achieve the above, we’ll create a server and use stubs in client implementation to test the code.

Let’s create one Gradle project. I have used IntelliJ IDE.

Open IntelliJ IDE, then click on File. Then click on create a new project and fill in/select the details as I have done.

  • Language — Java
  • Build system — Gradle
  • JDK — prefer Java 17
Create Gradle project

Once you click on create, select New Window, and your project should open in a new window of IntelliJ. Your project structure will look like the one below.

Structure of the grpc-demo project

Now update the build.gradle file and add gRPC-related dependencies we’ll need for our project to work.

runtimeOnly 'io.grpc:grpc-netty-shaded:1.49.2'
implementation 'io.grpc:grpc-protobuf:1.49.2'
implementation 'io.grpc:grpc-stub:1.49.2'

I have also added plugins for protobuf so that while writing .proto it notifies us if there are some errors.

id 'com.google.protobuf' version '0.8.18'

After adding dependencies and plugins, your build.gradle file should look like the below :

plugins {
id 'java'
id 'idea'
id 'com.google.protobuf'
version '0.8.18'
}
group 'org.example'
version '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.21.7"
}
plugins {
grpc {
artifact = 'io.grpc:protoc-gen-grpc-java:1.49.2'
}
}
generateProtoTasks {
all() * .plugins {
grpc {}
}
}
}
sourceSets.main.java.srcDir new File(buildDir, 'generated/source')
dependencies {
runtimeOnly 'io.grpc:grpc-netty-shaded:1.49.2'
implementation 'io.grpc:grpc-protobuf:1.49.2'
implementation 'io.grpc:grpc-stub:1.49.2'
compileOnly
'org.apache.tomcat:annotations-api:6.0.53' // necessary for Java 9+
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
}
test {
useJUnitPlatform()
}

Now add .proto files, which will have messages and service definitions. Add those in the /src/main/proto/calculator package.

sum.proto:

This proto defines SumRequest and SumResponse messages for sum operations. Where SumRequest consists of two number fields and SumResponse has a result field.

syntax="proto3";
package calculator;

option java_package="com.proto.calculator";
option java_multiple_files=true;

message SumRequest{
int32 first_number=1;
int32 second_number=2;
}

message SumResponse{
int32 result=1;
}

prime.proto:

Similar to sum.proto, this proto also contains PrimeRequest, which accepts a number and PrimeResponse, which will contain prime factors. We’ll use this proto for the server streaming example. i.e., for a single number server will give multiple responses returning factors.

syntax="proto3";
package calculator;

option java_package="com.proto.calculator";
option java_multiple_files=true;

message PrimeRequest{
int32 number =1;
}

message PrimeResponse{
int32 prime_factor =1;
}

avg.proto:

This proto has AvgRequest and AvgResponse with one field in each.
We’ll use this proto for a client streaming example where the client will send multiple AvgRequest, and the server will respond with oneAvgResponse.

Note: You can use any numeric data type for request and response. Just for an example, I am using int32.

syntax="proto3";
package calculator;

option java_package="com.proto.calculator";
option java_multiple_files=true;

message AvgRequest{
int32 number =1;
}

message AvgResponse{
double avg = 1;
}

max.proto:

This proto has MaxRequest with one number and MaxResponse with one number field. We’ll use this proto to understand bidirectional streaming.
The client will send numbers one by one, and the server will give multiple responses with a max number from the last two received.

syntax="proto3";
package calculator;

option java_package="com.proto.calculator";
option java_multiple_files=true;

message MaxRequest{
int32 number =1;
}

message MaxResponse{
int32 max=1;
}

sqrt.proto:

This proto has SqrtRequest with one number and SqrtResponse with one number. We’ll use this proto to get a square root of a number. If the number is negative, then the server will return the INVALID_ARGUMENT exception.

syntax="proto3";
package calculator;

option java_package="com.proto.calculator";
option java_multiple_files=true;

message SqrtRequest{
int32 number =1;
}

message SqrtResponse{
double result =1;
}

calculator.proto:

This proto imports all the above .proto files, and we have declared a service with five service methods.

package calculator;
option java_package = "com.proto.calculator";
option java_multiple_files = true;
import "calculator/sum.proto";
import "calculator/prime.proto";
import "calculator/avg.proto";
import "calculator/max.proto";
import "calculator/sqrt.proto";
service CalculatorService {
rpc sum(SumRequest) returns(SumResponse); //Unary API
rpc prime(PrimeRequest) returns(
stream PrimeResponse); // Server Streaming
rpc avg(stream AvgRequest) returns(AvgResponse); //Client Streaming
rpc max(stream MaxRequest) returns(
stream MaxResponse); //Bi-directional streaming
// Return a Status.INVALID if the SqrtRequest is negative
rpc sqrt(SqrtRequest) returns(SqrtResponse);
}

Now in IntelliJ, click on Gradle on the right side and click on generateProto.

Add server implementation inside src/main/java/calculator/server package. Here we’ll define a port number (50052 in our case) on which the server will listen for requests. We’ll also add CalculatorServiceImpl service to listen on this port.

CalculatorServer.java:

package calculator.server;
import io.grpc.Server;
import io.grpc.ServerBuilder;
import java.io.IOException;
public class CalculatorServer {
public static void main(String[] args) throws IOException,
InterruptedException {
int port = 50052;
Server server = ServerBuilder.forPort(port)
.addService(new CalculatorServiceImpl()).build();
server.start();
System.out.println("server started. Listening on port : " +
port);
Runtime.getRuntime().addShutdownHook(new Thread(() - > {
System.out.println("Received shutdown request.");
server.shutdown();
System.out.println("server stopped.");
}));
server.awaitTermination();
}
}

Add a CalculatorServiceImpl.java, which defines the functionality of the server. Here we’ll define how the server will respond to each request that comes from a client for various arithmetic operations.

package calculator.server;
import com.proto.calculator.*;
import io.grpc.Status;
import io.grpc.stub.StreamObserver;
public class CalculatorServiceImpl extends CalculatorServiceGrpc
.CalculatorServiceImplBase {
@Override
public void sum(SumRequest request, StreamObserver < SumResponse >
responseObserver) {
responseObserver.onNext(SumResponse.newBuilder().setResult(
request.getFirstNumber() + request.getSecondNumber()
).build());
responseObserver.onCompleted();
}
@Override
public void prime(PrimeRequest request, StreamObserver <
PrimeResponse > responseObserver) {
int n = request.getNumber();
int k = 2;
while (k <= n) {
if (n % k == 0) {
responseObserver.onNext(
PrimeResponse.newBuilder().setPrimeFactor(k).build()
);
n = n / k;
} else {
k = k + 1;
}
}
responseObserver.onCompleted();
}
@Override
public StreamObserver < AvgRequest > avg(StreamObserver <
AvgResponse > responseObserver) {
return new StreamObserver < AvgRequest > () {
int sum = 0;
double count = 0;
@Override
public void onNext(AvgRequest request) {
sum += request.getNumber();
count += 1;
}
@Override
public void onError(Throwable t) {
responseObserver.onError(t);
}
@Override
public void onCompleted() {
responseObserver.onNext(AvgResponse.newBuilder()
.setAvg(sum / count).build());
responseObserver.onCompleted();
}
};
}
@Override
public StreamObserver < MaxRequest > max(StreamObserver <
MaxResponse > responseObserver) {
return new StreamObserver < MaxRequest > () {
int max = 0;
@Override
public void onNext(MaxRequest value) {
if (value.getNumber() > max) {
max = value.getNumber();
responseObserver.onNext(MaxResponse.newBuilder()
.setMax(max).build());
}
}
@Override
public void onError(Throwable t) {
responseObserver.onError(t);
}
@Override
public void onCompleted() {
responseObserver.onCompleted();
}
};
}
@Override
public void sqrt(SqrtRequest request, StreamObserver <
SqrtResponse > responseObserver) {
int number = request.getNumber();
if (number < 0) {
responseObserver.onError(Status.INVALID_ARGUMENT
.withDescription(
"The number being sent cannot be negative")
.augmentDescription("number : " + number)
.asRuntimeException());
return;
}
responseObserver.onNext(SqrtResponse.newBuilder().setResult(
Math.sqrt(number)).build());
responseObserver.onCompleted();
}
}

Now it's time to create a client in src/main/java/calculator/client package.

This client accepts program arguments to perform each arithmetic operation. E.g. for the sum, provide sum as a program argument while running the client. I have given steps on how we can do that below.

CalculatorClient.java

package calculator.client;
import com.proto.calculator.*;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.stub.StreamObserver;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class CalculatorClient {
public static void main(String[] args) throws InterruptedException {
if (args.length == 0) {
System.out.println("need 1 argument");
return;
}
ManagedChannel channel = ManagedChannelBuilder.forAddress(
"localhost", 50052)
.usePlaintext()
.build();
switch (args[0]) {
case "sum":
doSum(channel);
break;
case "prime":
fetchPrime(channel);
break;
case "avg":
calculateAvg(channel);
break;
case "max":
doMax(channel);
break;
case "sqrt":
doSqrt(channel);
break;
default:
System.out.println("invalid keword");
break;
}
}
private static void doSqrt(ManagedChannel channel) {
System.out.println("entered doSqrt");
CalculatorServiceGrpc.CalculatorServiceBlockingStub stub =
CalculatorServiceGrpc.newBlockingStub(channel);
SqrtResponse response = stub.sqrt(SqrtRequest.newBuilder()
.setNumber(25).build());
System.out.println("sqrt 25 = " + response.getResult());
try {
response = stub.sqrt(SqrtRequest.newBuilder().setNumber(-1)
.build());
System.out.println("sqrt -1 = " + response.getResult());
} catch (RuntimeException e) {
System.out.println("Got an exception for sqrt : ");
e.printStackTrace();
}
}
private static void doMax(
ManagedChannel channel) throws InterruptedException {
System.out.println("entered doMax");
CalculatorServiceGrpc.CalculatorServiceStub stub =
CalculatorServiceGrpc.newStub(channel);
CountDownLatch latch = new CountDownLatch(1);
StreamObserver < MaxRequest > stream = stub.max(
new StreamObserver < MaxResponse > () {
@Override
public void onNext(MaxResponse value) {
System.out.println("max = " + value.getMax());
}
@Override
public void onError(Throwable t) {}
@Override
public void onCompleted() {
latch.countDown();
}
});
Arrays.asList(1, 5, 3, 6, 2, 20).forEach(
number - > {
stream.onNext(MaxRequest.newBuilder().setNumber(number)
.build());
}
);
stream.onCompleted();
latch.await(3, TimeUnit.SECONDS);
}
private static void calculateAvg(
ManagedChannel channel) throws InterruptedException {
System.out.println("Entered calculateAvg()");
CalculatorServiceGrpc.CalculatorServiceStub stub =
CalculatorServiceGrpc.newStub(channel);
List < Integer > numbers = List.of(1, 2, 3, 4);
CountDownLatch latch = new CountDownLatch(1);
StreamObserver < AvgRequest > stream = stub.avg(
new StreamObserver < AvgResponse > () {
@Override
public void onNext(AvgResponse response) {
System.out.println("Average : " + response.getAvg());
}
@Override
public void onError(Throwable t) {}
@Override
public void onCompleted() {
latch.countDown();
}
});
for (Integer number: numbers) {
stream.onNext(AvgRequest.newBuilder().setNumber(number)
.build());
}
stream.onCompleted();
latch.await(3, TimeUnit.SECONDS);
}
private static void fetchPrime(ManagedChannel channel) {
System.out.println("Entered fetchPrime");
CalculatorServiceGrpc.CalculatorServiceBlockingStub stub =
CalculatorServiceGrpc.newBlockingStub(channel);
stub.prime(PrimeRequest.newBuilder().setNumber(120).build())
.forEachRemaining(primeResponse - > {
System.out.println("factor : " + primeResponse
.getPrimeFactor());
});
}
private static void doSum(ManagedChannel channel) {
System.out.println("enter doSum()");
CalculatorServiceGrpc.CalculatorServiceBlockingStub stub =
CalculatorServiceGrpc.newBlockingStub(channel);
SumResponse response = stub.sum(SumRequest.newBuilder()
.setFirstNumber(1).setSecondNumber(3).build());
System.out.println("sum 1+3 = " + response.getResult());
}
}

Now run CalculatorServer.java and you should see below:

server started. Listening on port : 50052

Now run CalculatorClient.java with program arguments. sum, prime, avg, max, and sqrt are the valid arguments and will execute the respective feature. Modify the run configuration for CalculatorClient.java. Provide program arguments as shown below, click on Apply and then click on OK.

Update program arguments for CalculatorClient.java

For sum below are the input and output:
Input provided by stub: 1, 3
Output:

enter doSum()
sum 1+3 = 4

For prime below is the input and output:
Input: 120
Output:

Entered fetchPrime
factor : 2
factor : 2
factor : 2
factor : 3
factor : 5

For max below are the input and output:
Input provided by stub: 1, 5, 3, 6, 2, 20
Output:

entered doMax
max = 1
max = 5
max = 6
max = 20

For avg below are the input and output:
Input provided by stub: 1, 2, 3, 4
Output:

Entered calculateAvg()
Average : 2.5

For sqrt below are the input and output:
Input provided by stub: 25 and then -1
Output:

entered doSqrt
sqrt 25 = 5.0
Got an exception
for sqrt:
io.grpc.StatusRuntimeException: INVALID_ARGUMENT:
The number being sent cannot be negative
number: -1
at io.grpc.stub.ClientCalls.toStatusRuntimeException(ClientCalls.java:
271)
at io.grpc.stub.ClientCalls.getUnchecked(ClientCalls.java: 252)
at io.grpc.stub.ClientCalls.blockingUnaryCall(ClientCalls.java: 165)
at com.proto.calculator
.CalculatorServiceGrpc$CalculatorServiceBlockingStub.sqrt(
CalculatorServiceGrpc.java: 423)
at calculator.client.CalculatorClient.doSqrt(CalculatorClient.java:
44)
at calculator.client.CalculatorClient.main(CalculatorClient.java: 29)

Conclusion

In this article, we saw what gRPC is and the way it communicates. We also saw the different service methods gRPC allows us to write by writing code examples.

References

The below references will help you to get more information.

  • The basics of gRPC can be found here.
  • Basics about microservices can be found here.
  • An introduction to the HTTP/2 protocol can be found here.

--

--