- gRPC Client Implementation
Helidon gRPC client framework allows you to write gRPC clients to access any gRPC service implementation. 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 and metrics collection and interceptors down to the method level.
It allows you to easily specify custom marshaller for requests and responses if
protobufdoes not satisfy your needs.
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 three 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 second 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. ** The third option is to load the method descriptions from a configuration file. (Not yet implemented).
The next step is to create a gRPC
Channelto use to communicate with the server.Finally, you 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)
Lets 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()) // (1)
.build();
Channel channel = ManagedChannelBuilder.forAddress("localhost", 1408) // (2)
.usePlaintext().build();
this.client = GrpcServiceClient.create(channel, desc); // (3)
}
/**
* 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> // (4)
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’s proto `ServiceDescriptor. From theServiceDescriptorthe builder detects the service name, the set of method names, and for each method its type (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 mentionedClientServiceDescriptorandChannel. 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 whereever 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); // (1)
String lcase = client.blockingUnary("Lower", input); // (2)
StringMessageStream stream = new StringMessageStream<StringMessage>();
client.blockingUnary("Lower", input); // (3)
}
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 response thus allowing the client to obtain the result asynchronously. - This is simply a wrapper around the above method. This method blocks till the result is available.
- Here we create invoke the
unarymethod by passing theStringMessageStreamwhoseonNextmethod will be called (once) when the result is available.
Invoking a client streaming method on the StringService
Lets invoke the Join method which causes the server to return a single result after the client has streamed the request values to the server. 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, Helidon Client Framework provides a couple of 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(" ")) // (1)
.map(w -> StringMessage.newBuilder().setText(w).build())
.collect(Collectors.toList());
CompletableFuture<StringMessage> result =
grpcClient.clientStreaming("Join", input); // (2)
}
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); // (3)
for (String word : sentence.split(" ")) {
clientStream.onNext(StringMessage.newBuilder().setText(word).build()); // (4)
}
clientStream.onCompleted(); // (5)
}
public static class StringMessageStream<T> { /* code imitted */ }
}- 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)
Lets 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(); // (1)
StringMessageStream<StringMessage> observer = new StringMessageStream<>(); // (2)
grpcClient.serverStreaming("Split", input, observer); // (3)
}
public static class StringMessageStream<T> { /* code imitted */ }
}- 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 lets 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<>(); // (1)
StringMessageStream<StringMessage> clientStream = grpcClient
.bidiStreaming("Echo", observer); // (2)
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()); // (3)
}
clientStream.onCompleted(); // (4)
}
public static class StringMessageStream<T> { /* code imitted */ }
}- 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, lets 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 need to invoke. The Helidon client framework provides a bunch of APIs 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 following code should be very similar (or same) as the previous section!!
public class StringServiceClient {
public static void main(String[] args) {
ClientMethodDescriptor lower = ClientMethodDescriptor
.unary("StringService", "Lower") // (1)
.requestType(StringMessage.class) // (2)
.responseType(StringMessage.class) // (3)
.build(); // (4)
ClientMethodDescriptor join = ClientMethodDescriptor
.clientStreaming("StringService", "Join") // (5)
.requestType(StringMessage.class)
.responseType(StringMessage.class)
.build();
ClientMethodDescriptor split = ClientMethodDescriptor
.serverStreaming("StringService", "Split") // (6)
.requestType(StringMessage.class)
.responseType(StringMessage.class)
.build();
ClientMethodDescriptor echo = ClientMethodDescriptor
.bidirectional("StringService", "Echo") // (7)
.requestType(StringMessage.class)
.responseType(StringMessage.class)
.build();
ClientServiceDescriptor serviceDesc = ClientServiceDescriptor // (8)
.builder(StringService.class)
.unary(lower)
.clientStreaming(join)
.serverStreaming(split)
.bidirectional(echo)
.build();
Channel channel = ManagedChannelBuilder.forAddress("localhost", 1408) // (9)
.usePlaintext().build();
GrpcServiceClient client = GrpcServiceClient.create(channel, serviceDesc); // (10)
}
}- 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 Marshallers 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 service namedStringServiceand add all ourClientMethodDescriptors. - 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 ClientMethodDescriptor s and the ClientServiceDescriptor as described in the previous section, but with one change. Just do not to set the request and response types in the ClientMethodDescriptor. That’s all!! In fact, there is an API in the ClientServiceDescriptor that makes this even simpler. You can simply pass the method name. For example, to create a client streaming method called "JoinString" that uses java serialization simply call the clientStreamin("JoinString").
Lets see an example of creating a client for a service that uses Java serialization.
public static void main(String[] args) throws Exception {
ClientServiceDescriptor descriptor = ClientServiceDescriptor.builder(HelloService.class) // (1)
.clientStreaming("JoinString") // (2)
.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. - 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), Java serialization 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.