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
protobufdoes not satisfy your needs.
The class GrpcServiceClient acts as the client object for accessing a gRPC service. Creating a GrpcServiceClient involves:
- Creating a
ClientServiceDescriptorwhich describes the methods in the service that this client can invoke. - Creating a gRPC
Channelthrough 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>Usage
Client Implementation Basics
- 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
ClientServiceDescriptorto describe the set of methods of a service that the client may invoke. There are several ways to build and initialize aClientServiceDescriptor.The first option is to initialize
ClientServiceDescriptorusingprotocgenerated artifacts likeBindableServiceorio.grpc.ServiceDescriptor. This option is possible if the gRPC service was built using.protofile. 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 aClientServiceDescriptor.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 theprotocgenerated artifacts are not available to the client.
- The next step is to create a gRPC
Channelto use to communicate with the server. - Finally, we create an instance of
GrpcServiceClientpassing theClientMethodDescriptorand theChannelinstances.
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;
}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");
}
}
}- Initialize the builder by specifying the
StringService’sprotoServiceDescriptor. From theServiceDescriptor, 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
Channelto the service that is running onlocalhost:1408. - Finally, we create our
GrpcServiceClientby using the above mentionedClientServiceDescriptor. andChannel. Thisclientreference will be used to invoke various gRPC methods in ourStringService. - We define a static inner class that implements the
io.grpc.StreamObserverinterface. An instance of this class can be used wherever aio.grpc.StreamObserveris 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 */ }
}- This variant of the
unaryAPI takes the method name and a request object and returns aCompletableFuture<Response>where<Response>is the response type. Here we invoke theLowermethod passing the inputStringMessage. This method returns aCompletableFuture<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
unarymethod by passing theStringMessageStreamwhoseonNextmethod 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 */ }
}- 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 aio.grpc.StreamObserveras 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 */ }
}- We prepare the input
StringMessagethat needs to be split. - We create a
StringMessageStreamwhich will receive the results streamed from the server. - We call the
serverStreaming()passing the input and theStringMessageStreamas arguments. The server sends a stream of words by calling theonNext()method on theStringMessageStreamfor 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 */ }
}- We create a
StringMessageStreamwhich will receive the results streamed from the server. - We call the
bidiStreaming()passing theobserveras argument. The server will send its results through this stream (basically by calling theonNext()on theobserver). 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 theclientStream. - We call the
onCompleted()method on theclientStreamto 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); // (
}
}- Use the
unary()method onClientMethodDescriptorto 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 theLowermethod takesStringMessageas a parameter). - Set the response type of the method to be
StringMessage(since theLowermethod returns aStringMessageas a parameter). - Build the
ClientMethodDescriptor. Note that the return value is aClientMethodDescriptorthat contains the correct marshaller for the request & response types. - Use the
clientStreaming()method onClientMethodDescriptorto create a builder for a gRPC client streaming method. The service name and the method name ("Join") are specified. - Use the
serverStreaming()method onClientMethodDescriptorto create a builder for a gRPC server streaming method. The service name and the method name ("Split") are specified. - Use the
bidirectional()method onClientMethodDescriptorto create a builder for a gRPC Bidi streaming method. The service name and the method name ("Echo") are specified. - Create a
ClientServiceDescriptorfor a service namedStringServiceand add all the definedClientMethodDescriptors. - We create a
Channelto the service that is running onlocalhost:1408. - Finally, we create our
GrpcServiceClientby using the above-mentionedClientServiceDescriptorandChannel.
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);
}- Create a
ClientServiceDescriptorfor theHelloService. - 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:
ClientServiceDescriptor descriptor = ClientServiceDescriptor
.builder(HelloService.class)
.marshallerSupplier(new JsonbMarshaller.Supplier())
.clientStreaming("JoinString")
.build();- 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:
- Creating a
ClientServiceDescriptorwhich describes the methods in the service that this client can invoke. - Creating a gRPC
Channelthrough 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(); - Create a builder for a
ClientServiceDescriptorfor theHelloService. - Specify that the
HelloServicehas a unary method namedSayHello. There are many other methods in this class that allow you to defineClientStreaming,ServerStreamingandBidirectionalmethods. - 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());
}- Create a
ClientServiceDescriptorfor theHelloService. - Specify a custom marshaller using the built-in JSON-B marshaller to serialize/deserialize request and response values.
- Add the
SayHellounary method to theClientServiceDescriptorwhich will use the specified custom marshaller. - Create a gRPC
Channelthat will communicate with the server running in localhost and on port 1408 (using plaintext). - Create the
GrpcServiceClientthat uses the aboveChannelandClientServiceDescriptor.GrpcClientServicerepresents a client that can be used to define the set of methods described by the specifiedClientServiceDescriptor. In our case, theClientServiceDescriptordefines one unary method calledSayHello. - Invoke the
SayHellomethod which returns aCompletionStage<String>. - Print the result.
Additional gRPC Client Examples
A set of gRPC client examples for Helidon SE can be found in the following links: