Contents

Overview

Helidon gRPC client provides a framework for creating gRPC client applications. The client framework allows a uniform way to access gRPC services that use either Protobuf or some custom serialization format. The benefits of using Helidon gRPC client Framework include:

  • It provides a number of helper methods that make client implementation significantly simpler.

  • It allows you to configure some of the Helidon value-added features, such as security, metrics collection and interceptors down to the method level.

  • It allows you to easily specify custom marshallers for requests and responses if protobuf does not satisfy your needs.

The class GrpcServiceClient acts as the client object for accessing a gRPC service. Creating a GrpcServiceClient involves:

  1. Creating a ClientServiceDescriptor which describes the methods in the service that this client can invoke.
  2. Creating a gRPC Channel through which the client communicates with the server.

In later sections in this document, you will see how to customize both ClientServiceDescriptor and the Channel.

Maven Coordinates

To enable gRPC Client add the following dependency to your project’s pom.xml (see Managing Dependencies).

<dependency>
    <groupId>io.helidon.grpc</groupId>
    <artifactId>helidon-grpc-client</artifactId>
</dependency>
Copied

Usage

Client Implementation Basics

  1. The first step to create a Helidon gRPC client application is to describe the set of methods in the gRPC service. Helidon gRPC client Framework (simply called the "Client framework" in the remainder of the document) provides a class called ClientServiceDescriptor to describe the set of methods of a service that the client may invoke. There are several ways to build and initialize a ClientServiceDescriptor.
    • The first option is to initialize ClientServiceDescriptor using protoc generated artifacts like BindableService or io.grpc.ServiceDescriptor. This option is possible if the gRPC service was built using .proto file. In this case, the set of gRPC methods, their types and the appropriate marshallers are detected automatically. This is certainly the easiest way to initialize a ClientServiceDescriptor.

    • The other option is to programmatically build the ClientServiceDescriptor. This option should be taken if the service was not built from protobuf files or if the protoc generated artifacts are not available to the client.

  2. The next step is to create a gRPC Channel to use to communicate with the server.
  3. Finally, we create an instance of GrpcServiceClient passing the ClientMethodDescriptor and the Channel instances.

Creating gRPC Clients From protoc Generated Artifacts

As mentioned above, the easiest way to create a ClientServiceDescriptor is to create it from an io.grpc.ServiceDescriptor or from a io.grpc.BindableService. It is fairly trivial to obtain these from a service generated from artifacts generated from protobuf IDL file.

For this section we will assume the following proto file:

syntax = "proto3";
option java_package = "io.helidon.grpc.client.test";

service StringService {
  rpc Upper (StringMessage) returns (StringMessage) {}                  // (Unary)
  rpc Lower (StringMessage) returns (StringMessage) {}                  // (Unary)
  rpc Split (StringMessage) returns (stream StringMessage) {}           // (Server Streaming)
  rpc Join (stream StringMessage) returns (StringMessage) {}            // (Client Streaming)
  rpc Echo (stream StringMessage) returns (stream StringMessage) {}     // (Bidirectional Streaming)
}

message StringMessage {
  string text = 1;
}
Copied

If you run it through protoc, it will generate a class (among other things) called StringService. Assuming that the StringService server is running on port 1408, here is how you can create a Helidon gRPC Client that uses the Client Framework to invoke various types of gRPC methods.

Creating and Initializing a ClientServiceDescriptor for StringService (Generated from protoc)

Let’s build a class called ProtoBasedStringServiceClient that invokes the various types of gRPC methods that our StringService offers.

public class ProtoBasedStringServiceClient {

    private GrpcServiceClient client;

    public ProtoBasedStringServiceClient() {
        ClientServiceDescriptor desc = ClientServiceDescriptor
                .builder(StringService.getServiceDescriptor())  
                .build();

        Channel channel = ManagedChannelBuilder.forAddress("localhost", 1408)  
                .usePlaintext().build();

        this.client = GrpcServiceClient.create(channel, desc);  
    }

    /**
     * Many gRPC methods take a {@link io.grpc.StreamObserver} as an argument. Lets
     * build a helper class that can be used in our example.
     */
    public static class StringMessageStream<T> implements StreamObserver<T> {  
        @Override
        public void onNext(T value) {
            System.out.println("Received : " + value);
        }

        @Override
        public void onError(Throwable t) {
          t.printStracktrace();
        }

        @Override
        public void onCompleted() {
          System.out.println("DONE");
        }
    }
}
Copied
  • Initialize the builder by specifying the StringService’s proto ServiceDescriptor. From the ServiceDescriptor, the builder detects the service name, the set of method names, the type of each method (like Unary, ServerStreaming, etc.), the request and response types (and hence their corresponding Marshallers), etc.
  • We create a Channel to the service that is running on localhost:1408.
  • Finally, we create our GrpcServiceClient by using the above mentioned ClientServiceDescriptor. and Channel. This client reference will be used to invoke various gRPC methods in our StringService.
  • We define a static inner class that implements the io.grpc.StreamObserver interface. An instance of this class can be used wherever a io.grpc.StreamObserver is required (like server streaming, bi-directional streaming methods).

Invoking a Unary Method on the StringService

The Client Framework provides many helper methods to invoke gRPC unary methods.

public class ProtoBasedStringServiceClient {

    private GrpcServiceClient client;

    public ProtoBasedStringServiceClient() { /* code omitted */ }

    public void invokeUnaryMethod() throws Exception {
        StringMessage input = StringMessage.newBuilder().setText("ABC").build();

        CompletableFuture<String> result = client.unary("Lower", input);  

        String lcase = client.blockingUnary("Lower", input);  

        StringMessageStream stream = new StringMessageStream<StringMessage>();
        client.blockingUnary("Lower", stream);  
    }

    public static class StringMessageStream<T> { /* code omitted */ }
}
Copied
  • This variant of the unary API takes the method name and a request object and returns a CompletableFuture<Response> where <Response> is the response type. Here we invoke the Lower method passing the input StringMessage. This method returns a CompletableFuture<StringMessage> as its response thus allowing the client to obtain the result asynchronously.
  • This is simply a wrapper around the above method. This method blocks until the result is available.
  • Here, we invoke the unary method by passing the StringMessageStream whose onNext method will be called (once) when the result is available.

Invoking a Client Streaming Method on the StringService

Let’s invoke the Join method which causes the server to return a single result after the client has streamed the request values to the server. The gRPC API expects the client application to provide an instance of io.grpc.StreamObserver as an argument during the invocation of the client streaming method.

In order to simplify the task of invoking Client Streaming methods, the Helidon Client Framework provides two methods to invoke gRPC client Streaming methods. The first variant takes an Iterable as argument which in turn is converted into a io.grpc.StreamObserver. The second variant takes a io.grpc.StreamObserver as argument. The first variant can be used if the number of values to be streamed in small and known a priori.

public class ProtoBasedStringServiceClient {

    private GrpcServiceClient client;

    public ProtoBasedStringServiceClient() { /* code omitted */ }

    public void invokeClientStreamingWithIterable() throws Exception {

        String sentence = "A simple invocation of a client streaming method";
        Collection<StringMessage> input = Arrays.stream(sentence.split(" "))  
                  .map(w -> StringMessage.newBuilder().setText(w).build())
                  .collect(Collectors.toList());

        CompletableFuture<StringMessage> result =
                  grpcClient.clientStreaming("Join", input);  
    }

    public void invokeClientStreaming() throws Exception {
        String sentence = "A simple invocation of a client streaming method";
        StringMessageStream responseStream = new StringMessageStream<StringMessage>();
        StreamObserver<StringMessage> clientStream =
                  grpcClient.clientStreaming("Join", responseStream);  

        for (String word : sentence.split(" ")) {
            clientStream.onNext(StringMessage.newBuilder().setText(word).build());  
        }
        clientStream.onCompleted();  
    }

    public static class StringMessageStream<T> { /* code is omitted */ }

}
Copied
  • We prepare the collection that contains the values to be streamed.
  • We call the first variant of the clientStreaming() method that takes the method name and the collection of values to be streamed from the client. Note: The above helper method is useful if the values to be streamed is fixed and small in number.
  • If the number of values to be streamed is large (or unknown), then it is better to use this variant of the clientStreaming() method that takes a io.grpc.StreamObserver as an argument. This method returns a client stream through which the client can stream (potentially a large number of) value to the server.
  • Once the client stream is obtained, the client streams the values using the onNext() method on the stream.
  • When all values have been stream, the client invokes the onCompleted() method signal that all values have been streamed from the client.

Invoking a Server Streaming Method on the StringService (Generated from protoc)

Let’s invoke the "Split" method which causes the server to stream the results back.

public class ProtoBasedStringServiceClient {

    private GrpcServiceClient client;

    public ProtoBasedStringServiceClient() { /* code omitted */ }

    public void invokeServerStreaming() throws Exception {
        String sentence = "This sentence will be split into words and sent back to client";
        StringMessage input = StringMessage.newBuilder().setText(sentence).build();  

        StringMessageStream<StringMessage> observer = new StringMessageStream<>();  
        grpcClient.serverStreaming("Split", input, observer);  
    }

    public static class StringMessageStream<T> { /* code is omitted */ }

}
Copied
  • We prepare the input StringMessage that needs to be split.
  • We create a StringMessageStream which will receive the results streamed from the server.
  • We call the serverStreaming() passing the input and the StringMessageStream as arguments. The server sends a stream of words by calling the onNext() method on the StringMessageStream for each word.

Invoking a Bi-Directional Streaming Method on the StringService (Generated from protoc)

Now let’s invoke the Echo method in which both the client and the server have to stream the request and response.

public class ProtoBasedStringServiceClient {

    private GrpcServiceClient client;

    public ProtoBasedStringServiceClient() { /* code omitted */ }

    public void invokeBidiStreaming() throws Exception {

        StringMessageStream<StringMessage> observer = new StringMessageStream<>();  
        StringMessageStream<StringMessage> clientStream = grpcClient
                                .bidiStreaming("Echo", observer);  

        String sentence = "Each word will be echoed back to the client by the server";
        for (String word : sentence.split(" ")) {
            clientStream.onNext(StringMessage.newBuilder().setText(word).build());  
        }
        clientStream.onCompleted();  
    }

    public static class StringMessageStream<T> { /* code is omitted */ }

}
Copied
  • We create a StringMessageStream which will receive the results streamed from the server.
  • We call the bidiStreaming() passing the observer as argument. The server will send its results through this stream (basically by calling the onNext() on the observer). The method returns a (client) stream which should be used by the client to stream values to the server.
  • We stream each word in our sentence to the server by calling the onNext() method on the clientStream.
  • We call the onCompleted() method on the clientStream to signal that the client has streamed all its values.

Programmatically Creating ClientServiceDescriptor for StringService

Assuming that the service is still running on port 1408, let’s see how to create our Client without using the StringService's proto ServiceDescriptor.

Since we are not going to use the StringService's proto ServiceDescriptor, we need to describe the methods that the client needs to invoke. The Helidon client framework provides several methods to easily describe gRPC methods.

For example, to register a unary method, we need to use the unary method and configure it to specify the request and response types.

Other than describing the methods that our client will invoke, the rest of the code should be very similar to (or the same as) the previous section!!

public class StringServiceClient {

    public static void main(String[] args) {
        ClientMethodDescriptor lower = ClientMethodDescriptor
                    .unary("StringService", "Lower")  
                    .requestType(StringMessage.class)  
                    .responseType(StringMessage.class)   
                    .build();  

        ClientMethodDescriptor join = ClientMethodDescriptor
                    .clientStreaming("StringService", "Join")   
                    .requestType(StringMessage.class)
                    .responseType(StringMessage.class)
                    .build();

        ClientMethodDescriptor split = ClientMethodDescriptor
                    .serverStreaming("StringService", "Split")  
                    .requestType(StringMessage.class)
                    .responseType(StringMessage.class)
                    .build();

        ClientMethodDescriptor echo = ClientMethodDescriptor
                    .bidirectional("StringService", "Echo")  
                    .requestType(StringMessage.class)
                    .responseType(StringMessage.class)
                    .build();

        ClientServiceDescriptor serviceDesc = ClientServiceDescriptor  
                    .builder(StringService.class)
                    .unary(lower)
                    .clientStreaming(join)
                    .serverStreaming(split)
                    .bidirectional(echo)
                    .build();


        Channel channel = ManagedChannelBuilder.forAddress("localhost", 1408)  
                .usePlaintext().build();

        GrpcServiceClient client = GrpcServiceClient.create(channel, serviceDesc);  // (

    }

}
Copied
  • Use the unary() method on ClientMethodDescriptor to create a builder for a gRPC unary method. The service name and the method name ("Lower") are specified.
  • Set the request type of the method to be StringMessage (since the Lower method takes StringMessage as a parameter).
  • Set the response type of the method to be StringMessage (since the Lower method returns a StringMessage as a parameter).
  • Build the ClientMethodDescriptor. Note that the return value is a ClientMethodDescriptor that contains the correct marshaller for the request & response types.
  • Use the clientStreaming() method on ClientMethodDescriptor to create a builder for a gRPC client streaming method. The service name and the method name ("Join") are specified.
  • Use the serverStreaming() method on ClientMethodDescriptor to create a builder for a gRPC server streaming method. The service name and the method name ("Split") are specified.
  • Use the bidirectional() method on ClientMethodDescriptor to create a builder for a gRPC Bidi streaming method. The service name and the method name ("Echo") are specified.
  • Create a ClientServiceDescriptor for a service named StringService and add all the defined ClientMethodDescriptors.
  • We create a Channel to the service that is running on localhost:1408.
  • Finally, we create our GrpcServiceClient by using the above-mentioned ClientServiceDescriptor and Channel.

At this point the client object can be used to invoke any of the four types of methods we have seen in the earlier sections.

Creating gRPC Clients for Non-Protobuf Services

If your service is not using protobuf for serialization, then the client framework allows you to programmatically initialize ClientMethodDescriptor and create clients to invoke methods on the service.

All you have to do is create the set of ClientMethodDescriptors and the ClientServiceDescriptor as described in the previous section. Just do not set the request and response types in the ClientMethodDescriptor anymore. Furthermore, there is an API in the ClientServiceDescriptor that makes this even simpler where you can simply pass the method name. For example, to create a client streaming method called "JoinString" that uses some custom marshalling, simply call the clientStreaming("JoinString").

public static void main(String[] args) throws Exception {
    ClientServiceDescriptor descriptor = ClientServiceDescriptor.builder(HelloService.class)  
                                                                .marshallerSupplier(new JsonbMarshaller.Supplier())  
                                                                .clientStreaming("JoinString")  
                                                                .build();

    Channel channel = ManagedChannelBuilder.forAddress("localhost", 1408)
                                           .usePlaintext()
                                           .build();

    GrpcServiceClient client = GrpcServiceClient.create(channel, descriptor);

    String sentence = "A simple invocation of a client streaming method";
    Collection<StringMessage> input = Arrays.stream(sentence.split(" "))
                                        .map(w -> StringMessage.newBuilder().setText(w).build())
                                        .collect(Collectors.toList());

    CompletableFuture<StringMessage> result = grpcClient.clientStreaming("Join", input);
}
Copied
  • Create a ClientServiceDescriptor for the HelloService.
  • Specify a custom marshaller using the built-in JSON-B marshaller to serialize/deserialize requests and responses.
  • Add the "JoinString" client streaming method to the ClientServiceDescriptor.

Since we didn’t set the request or response type (like we did in the previous sections), the custom marshaller will be used for Marshalling and Unmarshalling the request and response values.

Note that whether a ClientServiceDescriptor is built using protobuf artifacts or is built programmatically, the same set of APIs provided by the Client Framework can be used to invoke gRPC methods.

Marshalling

Default Marshalling Support

Helidon gRPC supports Protobuf out of the box. The Protobuf marshaller will be used by default for any request and response classes that extend com.google.protobuf.MessageLite, which is the case for all classes generated from a proto file using protoc compiler.

That means that you don’t need any special handling or configuration in order to support Protobuf serialization of requests and responses.

Custom Marshalling

Helidon makes the use of custom marshallers trivial and provides one custom implementation, JsonbMarshaller, out of the box.

You can also easily implement your own marshaller to support serialization formats that are not supported natively by Helidon, by implementing Marshaller and MarshallerSupplier interfaces. As an example, check out the source code of the built-in marshaller: JsonbMarshaller.java.

Furthermore, Oracle Coherence CE provides a marshaller for a highly optimized, binary, platform independent Portable Object Format (POF). You can find more information about POF in Coherence documentation

Setting the custom marshaller

You can set the custom marshaller supplier via the ClientServiceDescriptor.builder.marshallerSupplier() method:

Sample code for setting the marshaller on the ClientServiceDescriptor
ClientServiceDescriptor descriptor = ClientServiceDescriptor
        .builder(HelloService.class)
        .marshallerSupplier(new JsonbMarshaller.Supplier())  
        .clientStreaming("JoinString")
        .build();
Copied
  • Specify the custom marshaller to use.

Configuration

Configure the gRPC client using the Helidon configuration framework, either programmatically or via a configuration file. As mentioned earlier, creating a GrpcServiceClient involves:

  1. Creating a ClientServiceDescriptor which describes the methods in the service that this client can invoke.
  2. Creating a gRPC Channel through which the client communicates with the server.

Configuring the ClientServiceDescriptor

The only way to configure the ClientServiceDescriptor is in your application code.

ClientServiceDescriptor descriptor = ClientServiceDescriptor
        .builder(HelloService.class)  
        .unary("SayHello")  
        .build();  
Copied
  • Create a builder for a ClientServiceDescriptor for the HelloService.
  • Specify that the HelloService has a unary method named SayHello. There are many other methods in this class that allow you to define ClientStreaming, ServerStreaming and Bidirectional methods.
  • Build the ClientServiceDescriptor.

Configuring the gRPC Channel

gRPC allows various channel configurations (deadlines, retries, interceptors etc.)

Please refer to gRPC documentation: https://grpc.io/grpc-java/javadoc/io/grpc/ManagedChannelBuilder.html.

Examples

Quick Start

First, create and run a minimalist HelloService gRPC server application as described in the gRPC server quick start example.

Assuming that the server is running on port 1408, create a client as follows:

public static void main(String[] args) throws Exception {
    ClientServiceDescriptor descriptor = ClientServiceDescriptor.builder(HelloService.class)  
                                                                .marshallerSupplier(new JsonbMarshaller.Supplier()) 
                                                                .unary("SayHello")  
                                                                .build();

    Channel channel = ManagedChannelBuilder.forAddress("localhost", 1408)  
                                           .usePlaintext()
                                           .build();

    GrpcServiceClient client = GrpcServiceClient.create(channel, descriptor);  

    CompletionStage<String> future = client.unary("SayHello", "Helidon gRPC!!");  
    System.out.println(future.get());  

}
Copied
  • Create a ClientServiceDescriptor for the HelloService.
  • Specify a custom marshaller using the built-in JSON-B marshaller to serialize/deserialize request and response values.
  • Add the SayHello unary method to the ClientServiceDescriptor which will use the specified custom marshaller.
  • Create a gRPC Channel that will communicate with the server running in localhost and on port 1408 (using plaintext).
  • Create the GrpcServiceClient that uses the above Channel and ClientServiceDescriptor. GrpcClientService represents a client that can be used to define the set of methods described by the specified ClientServiceDescriptor. In our case, the ClientServiceDescriptor defines one unary method called SayHello.
  • Invoke the SayHello method which returns a CompletionStage<String>.
  • Print the result.

Additional gRPC Client Examples