in

Java gRPC from Scratch: A Comprehensive Guide

default image

gRPC (Google Remote Procedure Call) is an open source remote procedure call (RPC) system initially developed by Google. It uses HTTP/2 for transport, Protocol Buffers as the interface description language, and provides features such as authentication, bidirectional streaming and flow control, blocking or nonblocking bindings, and cancellation and timeouts. gRPC enables clients and servers to communicate transparently, and makes it easier to build connected systems.

In this comprehensive guide, we will explore gRPC with Java from the ground up. We will cover:

  • What is gRPC and its advantages
  • How gRPC works
  • protobuf messages and services
  • Generating code from .proto files
  • Implementing a basic gRPC Java server
  • Client code generation
  • Streaming RPCs
  • Deadlines, error handling and cancellation
  • Authentication and authorization
  • Load balancing and naming
  • Monitoring and logging
  • Testing gRPC services

By the end, you will have a solid understanding of gRPC in Java and be able to implement gRPC services in your applications.

What is gRPC?

gRPC is a modern, high-performance remote procedure call (RPC) framework that can run anywhere. It enables client and server applications to communicate transparently, and makes it easier to build connected systems.

Here are some key advantages of gRPC:

  • Language agnostic: gRPC tooling supports many languages including Java, Go, Python, Ruby, C#, Node.js, Objective-C, PHP and Dart. This allows seamless integration between polyglot systems.

  • High performance: gRPC is built on top of HTTP/2 which provides significant performance improvements over HTTP/1.x. Some benchmarks show gRPC having 5x lower latency and 6x higher throughput than REST over HTTP 1.1.

  • Bi-directional streaming: Allows both client and server to stream data to each other for high throughput, low latency connectivity.

  • Protobuf contract: Services and messages are defined in protobuf which is efficient to serialize and language-neutral.

  • Generated code: Languages have first-class support for protobuf and gRPC with plugins that generate Stub, request and response classes. Removes boilerplate code.

  • Simpler than REST: No need to design resource hierarchy or methods like POST vs PUT. Just define the service methods and messages in protobuf.

In summary, gRPC provides an efficient, language-neutral mechanism for connecting systems with bi-directional streaming, high throughput and low latency.

How gRPC Works

gRPC works seamlessly across languages and platforms using Protocol Buffers and HTTP/2. Here is a quick overview of how the main components fit together:

  • gRPC clients call gRPC services by specifying the method to call and sending request parameters. Clients use generated stubs which handle serialization and deserialization of messages.

  • gRPC services implement RPC call handlers for the methods defined in the protobuf service definition. The server implementation handles requests and sends back responses.

  • protobufs define the contract between client and server by specifying service methods and message structures. These are used to generate client stubs and server skeletons.

  • HTTP/2 handles all transmission of protobuf messages over the wire efficiently and securely. gRPC builds on HTTP/2 features like multiplexing, bi-directional streams, flow control and TLS encryption.

This overall flow allows developers to focus on the service contract and business logic while gRPC handles all the connection, communication and threading complexity.

Defining Services and Messages with Protobufs

With gRPC, you define the service contract with Protocol Buffers (protobuf). The .proto files:

  • Define the gRPC service with RPC methods.
  • Define the request and response message types used in the gRPC methods.

For example, here is a simple HelloService in proto3 syntax:

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

message HelloRequest {
  string name = 1; 
}

message HelloResponse {
  string message = 1;
}

This defines a HelloService with one RPC method called SayHello. It takes a HelloRequest parameter and returns a HelloResponse.

The HelloRequest and HelloResponse messages structures are also defined. We just have simple string fields here but protobuf supports a variety of data types like integers, enums, nested messages and maps.

Generating gRPC Code

Once you have defined the gRPC service in a .proto file, you can use the protobuf compiler protoc to generate client and server code.

For Java, this produces:

  • A gRPC client stub class to call the service methods from client code.
  • An abstract service base class to override and implement the RPC handlers on the server.
  • The request/response classes for serializing/deserializing the messages.

For example, running protoc on hello.proto would generate:

  • HelloServiceGrpc.java: The gRPC client stub
  • HelloServiceImplBase.java: The abstract server class
  • HelloRequest.java & HelloResponse.java: Message classes

By generating code from the .proto definition, you get type-safe contracts and efficient serialization/deserialization for free!

Implementing a gRPC Java Server

Let‘s look at how to implement a gRPC server in Java. We will use the HelloService example above.

Add gRPC dependencies

To get started, you need to add the Netty gRPC dependencies which include the Netty transport library and gRPC stub code:

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

And add the grpc-protobuf dependency which pulls in the necessary protobuf libraries:

<dependency>
  <groupId>io.grpc</groupId>
  <artifactId>grpc-protobuf</artifactId>
  <version>1.42.1</version>
</dependency>

Implement the gRPC Service

Next, create your gRPC service implementation by extending the generated abstract class:

public class HelloServiceImpl extends HelloServiceGrpc.HelloServiceImplBase {

  @Override
  public void sayHello(HelloRequest req, StreamObserver<HelloResponse> responseObserver) {

    HelloResponse response = HelloResponse.newBuilder()
      .setMessage("Hello " + req.getName())
      .build();

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

}

Override the sayHello method, build a HelloResponse and send it back to the client via the responseObserver. That‘s it!

Start the gRPC Server

Then in your server startup code, you need to create a gRPC server, register your service implementation and start the server:

Server server = ServerBuilder.forPort(8080)
  .addService(new HelloServiceImpl())
  .build();

server.start();

The ServerBuilder handles all the gRPC plumbing and starting the underlying HTTP/2 Netty server.

Just like that you have a gRPC service running on localhost:8080!

Calling gRPC Services from Java Clients

Next let‘s look at how to call our gRPC service from a Java client using the generated stub.

Create the gRPC Channel

First you create a gRPC channel to the server:

ManagedChannel channel = ManagedChannelBuilder
  .forAddress("localhost", 8080)
  .usePlaintext() 
  .build();

This sets up a Netty channel to port 8080 on localhost using plain text communication.

Initialize the Stub

Then initialize the stub which will call the service:

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

This creates a blocking stub that will serialize requests, send them over the channel and return the deserialized response.

There is also a non-blocking stub HelloServiceStub which uses async callbacks instead of blocking.

Call the gRPC Method

Finally, you can call the gRPC method via the stub:

HelloResponse response = stub.sayHello(
  HelloRequest.newBuilder()
    .setName("World")
    .build()
);

System.out.println(response.getMessage());

You build a request, pass it to stub.sayHello() and get the response back to print out.

Just like that you can call your gRPC service from a Java client!

Streaming RPCs

In addition to simple unary RPCs, gRPC also supports streaming semantics for high performance, bi-directional workflows:

  • Server streaming – Client sends one request, server streams multiple responses.
  • Client streaming – Client streams multiple requests, server returns a single response.
  • Bi-directional streaming – Both sides stream messages to each other.

For example, a server streaming RPC method would look like:

service HelloService {
  rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse);
}

LotsOfReplies returns a stream of HelloResponse messages back to the client.

To implement a streaming RPC on the server:

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

  responseObserver.onNext(/* stream responses */);

  responseObserver.onCompleted();
} 

Stream messages to responseObserver and call onCompleted() when done.

The client receives streaming responses via a StreamObserver callback:

stub.lotsOfReplies(request, new StreamObserver<HelloResponse>() {

  @Override
  public void onNext(HelloResponse response) {
    // process streaming response
  }

  // onError, onCompleted

});

Streaming allows processing real-time data flows for high throughput.

Deadlines, Timeouts and Cancellation

gRPC supports deadlines, timeouts and cancellation:

  • Deadlines allow clients to set an end time by which the RPC should complete. Both client and server can check if a deadline has expired.

  • Timeouts set the max time to wait for a response before cancelling a request. Timeouts require server cooperation.

  • Cancellation allows clients to cancel in-flight RPC requests by sending cancellation notifications to the server. Servers can listen for cancellations.

This allows graceful winding down of longer procedures like streaming RPCs.

Here is an example of setting a deadline and cancelling the request from the client:

HelloRequest request = // ...

// Set deadline 
stub = stub.withDeadline(Deadline.after(1, TimeUnit.SECONDS));

// Call service
stub.sayHello(request, responseObserver);

// Cancel request
CancellationTokenSource cts = new CancellationTokenSource();
cts.cancel();
stub.sayHello(request, responseObserver, cts.getToken()); 

The server can check Context.isCancelled() in the handler to react to cancellations.

Securing gRPC with SSL/TLS

gRPC supports SSL/TLS encryption and identity verification out of the box. Just configure TLS when creating the client channel and server:

Server

Server server = NettyServerBuilder.forPort(8080)
  .sslContext(grpcSslContext)
  .addService(new HelloServiceImpl())
  .build();

Client

ManagedChannel channel = NettyChannelBuilder.forAddress("localhost", 8080)
  .sslContext(grpcSslContext)
  .build();

The grpcSslContext is the SSL context loading cert chains and private keys.

This enables secure authentication and encryption without requiring extra implementation!

Load Balancing and Naming

For production deployments, gRPC services typically run on multiple servers. Clients need to load balance across the servers.

There are two main options:

  • Smart Load Balancer – A dedicated load balancer middleman. Uses service discovery to find instances and balances requests across them.

  • DNS Resolver – Client asks DNS server to resolve the gRPC server name. DNS server returns list of IP addresses to round robin through.

The gRPC Java ecosystem provides balanced channel implementations for each approach:

Smart Load Balancer

ManagedChannel channel = ManagedChannelBuilder
  .forTarget("lb://mylb.example.com:8080")
  .build();

Delegates balancing to mylb.example.com load balancer.

DNS Resolver

ManagedChannel channel = ManagedChannelBuilder
  .forTarget("dns:///$myserver:8080")
  .build();

Talks to DNS server to resolve $myserver instances.

This makes it easy to scale gRPC services across nodes!

Monitoring, Tracing and Logging

Observability is crucial for monitoring and debugging production systems. gRPC provides hooks for adding:

  • Metrics: Capture key RPC metrics like rate, latency, errors

  • Tracing: Follow RPCs across process and network boundaries

  • Logging: Log message payloads, headers, trailers etc.

For example, gRPC Java apps can easily integrate Prometheus metrics and OpenTracing distributed tracing using provided interceptors:


// Prometheus metrics 
serverBuilder.intercept(new io.grpc.Instrumentation.ServerRequestRecorder(...));

// OpenTracing 
clientBuilder.intercept(TracingClientInterceptor.newBuilder()...); 

There are also gRPC plugins for logging frameworks like SLF4J and Log4j.

This makes it easy to observe RPCs in complex microservices!

Testing gRPC Services

Since gRPC is based on interfaces and handles serialization/deserialization automatically, it‘s straightforward to test gRPC services.

For unit testing, create a client using InProcessChannelBuilder and call service methods directly:

HelloServiceImpl server = new HelloServiceImpl(); 

// In-process client
HelloServiceGrpc.HelloServiceBlockingStub stub = 
  GrpcCleanupRule.directChannel(server).newBlockingStub(HelloServiceGrpc.getServiceDescriptor());

// Assert response
HelloResponse response = stub.sayHello(request);
assertThat(response.getMessage()).isEqualTo("Hello Test");

For integration tests, start a real gRPC server and make requests to test end-to-end.

Because the contract is explicit in .proto files, mocking is simple as well.

Conclusion

That wraps up our comprehensive overview of gRPC in Java! Here are some key takeaways:

  • gRPC handles communication efficiently via HTTP/2, protobuf and auto-generated code.

  • Define the service contract in .proto files and generate clients/servers.

  • Server implements gRPC handlers by extending the service base class.

  • Clients call gRPC methods via generated blocking/async stubs.

  • Supports unary, server streaming, client streaming and bi-directional streaming RPCs.

  • Handle deadlines, timeouts and cancellations for graceful shutdown.

  • Secure gRPC with SSL/TLS encryption and authentication.

  • Load balance across gRPC servers using smart load balancers or DNS.

  • Integrate monitoring, metrics, tracing and logging for observability.

  • Easily test services with generated interfaces and contract-first approach.

gRPC provides a modern, high-performance and language-neutral approach to connecting polyglot systems efficiently. The combination of HTTP/2, protobufs and code generation makes it an appealing choice vs traditional REST and web services.

I hope this guide provides a solid foundation for applying gRPC in your Java applications. Let me know if you have any other topics you would like covered!

AlexisKestler

Written by Alexis Kestler

A female web designer and programmer - Now is a 36-year IT professional with over 15 years of experience living in NorCal. I enjoy keeping my feet wet in the world of technology through reading, working, and researching topics that pique my interest.