Skip to content

Commit c44c890

Browse files
spericasbarchetta
authored andcommitted
Adds documentation for gRPC Client in SE. (helidon-io#9440)
Signed-off-by: Santiago Pericas-Geertsen <[email protected]>
1 parent 1270f2e commit c44c890

File tree

2 files changed

+356
-4
lines changed

2 files changed

+356
-4
lines changed

docs/src/main/asciidoc/se/grpc/client.adoc

+167-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
///////////////////////////////////////////////////////////////////////////////
22

3-
Copyright (c) 2019, 2023 Oracle and/or its affiliates.
3+
Copyright (c) 2019, 2024 Oracle and/or its affiliates.
44

55
Licensed under the Apache License, Version 2.0 (the "License");
66
you may not use this file except in compliance with the License.
@@ -27,10 +27,173 @@ include::{rootdir}/includes/se.adoc[]
2727
== Contents
2828
2929
- <<Overview, Overview>>
30+
- <<Maven Coordinates, Maven Coordinates>>
31+
- <<Usage, Usage>>
32+
** <<Generated Stubs, Generated Stubs>>
33+
** <<Service Descriptors, Service Descriptors>>
34+
** <<Client URI Suppliers, Client URI Suppliers>>
35+
** <<gRPC Interceptors, gRPC Interceptors>>
36+
- <<Configuration, Configuration>>
3037
3138
== Overview
3239
33-
gRPC client is temporarily removed from Helidon, please follow issue
34-
https://github.com/helidon-io/helidon/issues/5418
40+
The Helidon gRPC client API is part of the WebClient API, but with specific support to
41+
invoke remote procedures and to register handlers for responses. All four types of gRPC
42+
calls are supported: unary, bi-directional, client stream and server stream. A
43+
Helidon gRPC client can be configured either using generated stubs (the most popular
44+
option) or using manually crafted service descriptors.
45+
46+
include::{rootdir}/includes/dependencies.adoc[]
47+
48+
[source,xml]
49+
----
50+
<dependency>
51+
<groupId>io.helidon.webclient</groupId>
52+
<artifactId>helidon-webclient-grpc</artifactId>
53+
</dependency>
54+
----
55+
56+
== Usage
57+
58+
=== Generated Stubs
59+
60+
A Helidon gRPC client can be configured from generated protobuf stubs. In what follows,
61+
we shall use the following proto file and the corresponding stubs generated using
62+
the `protoc` command:
63+
64+
[source, proto]
65+
----
66+
syntax = "proto3";
67+
option java_package = "my.package";
68+
69+
service StringService {
70+
rpc Upper (StringMessage) returns (StringMessage) {}
71+
rpc Split (StringMessage) returns (stream StringMessage) {}
72+
}
73+
74+
message StringMessage {
75+
string text = 1;
76+
}
77+
----
78+
79+
The gRPC protocol runs on top of HTTP/2, and as such requires TLS configuration to
80+
establish a connection. Thus, the first step is to configure TLS as shown next:
81+
82+
[source,java]
83+
----
84+
include::{sourcedir}/se/grpc/ClientSnippets.java[tag=snippet_1, indent=0]
85+
----
86+
87+
After creating a `Tls` instance, a `WebClient` can be created as follows:
88+
89+
[source,java]
90+
----
91+
include::{sourcedir}/se/grpc/ClientSnippets.java[tag=snippet_2, indent=0]
92+
----
93+
94+
So far, this is all the same as for accessing any protected REST endpoint; the
95+
next step is to obtain a gRPC client stub using our newly created client.
96+
This can be accomplished by _switching_ the client protocol to gRPC, and
97+
using its channel to create a stub:
98+
99+
[source,java]
100+
----
101+
include::{sourcedir}/se/grpc/ClientSnippets.java[tag=snippet_3, indent=0]
102+
----
103+
104+
Once a stub is created, it can be used to invoke any of its declared
105+
methods, such as `upper` to uppercase a string:
106+
107+
[source,java]
108+
----
109+
include::{sourcedir}/se/grpc/ClientSnippets.java[tag=snippet_4, indent=0]
110+
----
111+
112+
When it comes to invoking a method that can return more than one value,
113+
there are two options: it can block (we are using virtual theads after all!)
114+
and return back an `Iterator` or you can provide a `StreamObserver` as it
115+
is more commonly done when using gRPC. Let's consider the case of the
116+
`split` method that breaks up a sentence into individual words, and
117+
can thus return multiple string messages.
118+
119+
Using an iterator as a result:
120+
[source,java]
121+
----
122+
include::{sourcedir}/se/grpc/ClientSnippets.java[tag=snippet_5, indent=0]
123+
----
124+
125+
Passing a stream observer and collecting all the messages into a `Future`
126+
that returns an iterator:
127+
[source,java]
128+
----
129+
include::{sourcedir}/se/grpc/ClientSnippets.java[tag=snippet_6, indent=0]
130+
----
131+
132+
=== Service Descriptors
133+
134+
Service descriptors are an alternative to using generated stubs and the
135+
`protoc` compiler. A service descriptor provides service meta-data to the
136+
WebClient for the purpose of carrying out invocations. The descriptor
137+
includes, the service name, and a description of each service method,
138+
including its type, what it accepts and what it returns.
139+
140+
The following is a descriptor for a service that includes the methods
141+
called in the previous section using a stub:
142+
[source,java]
143+
----
144+
include::{sourcedir}/se/grpc/ClientSnippets.java[tag=snippet_7, indent=0]
145+
----
146+
147+
Configuring a `WebClient` with `Tls` is done in the same manner as shown
148+
above for the stub case. Once the gRPC client is created, a service
149+
descriptor can be provided, and a method invoked using the methods
150+
`unary`, `clientStream`, `serverStream` or `bidi`. For example,
151+
152+
[source,java]
153+
----
154+
include::{sourcedir}/se/grpc/ClientSnippets.java[tag=snippet_8, indent=0]
155+
----
156+
157+
=== Client URI Suppliers
158+
159+
A `ClientURISupplier` can be used to dynamically obtain a sequence of `ClientUri`
160+
instances to access when executing a gRPC request. If a client URI supplier is
161+
configured, the Helidon gRPC implementation will attempt to connect to each
162+
endpoint one by one, in the order provided, until a connection is successfully
163+
established. This feature is useful in certain environments in which more than one
164+
identical server is available, but with some potentially unavailable or unreachable.
165+
166+
A few common implementations are provided in `ClientUriSuppliers`. These include
167+
suppliers for strategies such as random, round-robin, among others. Applications
168+
can either use one of the built-in suppliers or create their own.
169+
170+
The following example configures a round-robin supplier using a collection
171+
of known servers:
172+
173+
[source,java]
174+
----
175+
include::{sourcedir}/se/grpc/ClientSnippets.java[tag=snippet_9, indent=0]
176+
----
177+
178+
If both a base URI and a client URI supplier are configured, the latter will
179+
take precendence over the former.
180+
181+
=== gRPC Interceptors
182+
183+
The gRPC API supports the notion of an interceptor on a channel. Interceptors are
184+
useful to implement cross-cutting concerns that apply to many or all invocations.
185+
These may include security, logging, metrics, etc. They can be specified directly
186+
on the channel returned by a `GrpcClient`, effectively _wrapping_ that channel
187+
with a list of interceptors to execute on every invocation.
188+
189+
[source,java]
190+
----
191+
include::{sourcedir}/se/grpc/ClientSnippets.java[tag=snippet_10, indent=0]
192+
----
193+
194+
== Configuration
195+
196+
TLS can be configured externally, just like it is done when using the
197+
WebClient to access an HTTP endpoint. For more information see
198+
https://helidon.io/docs/v4/se/webclient#_configuring_the_webclient[Configuring the WebClient].
35199
36-
If you require gRPC client, either stay with a previous version of Helidon, or allow us to finish fixing the issue above.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/*
2+
* Copyright (c) 2024 Oracle and/or its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.helidon.docs.se.grpc;
17+
18+
import java.util.List;
19+
import java.util.ArrayList;
20+
import java.util.Iterator;
21+
import java.util.concurrent.CompletableFuture;
22+
import io.helidon.common.configurable.Resource;
23+
import io.helidon.common.tls.Tls;
24+
import io.helidon.config.Config;
25+
import io.helidon.webclient.api.WebClient;
26+
import io.helidon.webclient.api.ClientUri;
27+
import io.helidon.webclient.grpc.GrpcClient;
28+
import io.helidon.webserver.WebServer;
29+
import io.helidon.webserver.grpc.GrpcRouting;
30+
import io.helidon.webserver.grpc.GrpcService;
31+
import io.helidon.webclient.grpc.GrpcServiceDescriptor;
32+
import io.helidon.webclient.grpc.GrpcClientMethodDescriptor;
33+
import io.helidon.webclient.grpc.ClientUriSupplier;
34+
import io.helidon.webclient.grpc.ClientUriSuppliers.RoundRobinSupplier;
35+
36+
import com.google.protobuf.Descriptors;
37+
import io.grpc.Channel;
38+
import io.grpc.stub.StreamObserver;
39+
import io.grpc.ClientInterceptor;
40+
41+
import static io.helidon.grpc.core.ResponseHelper.complete;
42+
43+
@SuppressWarnings("ALL")
44+
class ClientSnippets {
45+
46+
class StringServiceGrpc {
47+
48+
class StringServiceBlockingStub {
49+
Strings.StringMessage upper(Strings.StringMessage msg) {
50+
return null;
51+
}
52+
Iterator<Strings.StringMessage> split(Strings.StringMessage msg) {
53+
return null;
54+
}
55+
void split(Strings.StringMessage msg, StreamObserver<?> observer) {
56+
}
57+
}
58+
59+
static StringServiceGrpc.StringServiceBlockingStub newBlockingStub(Channel c) {
60+
return null;
61+
}
62+
}
63+
64+
class Strings {
65+
66+
class StringMessage {
67+
String getText() {
68+
return null;
69+
}
70+
}
71+
}
72+
73+
Strings.StringMessage newMessage(String s) {
74+
return null;
75+
}
76+
77+
ClientUri[] myServers() {
78+
return null;
79+
}
80+
81+
List<ClientInterceptor> myInterceptors() {
82+
return null;
83+
}
84+
85+
void snippets() {
86+
// tag::snippet_1[]
87+
Tls clientTls = Tls.builder()
88+
.trust(trust -> trust
89+
.keystore(store -> store
90+
.passphrase("password")
91+
.trustStore(true)
92+
.keystore(Resource.create("client.p12"))))
93+
.build();
94+
// end::snippet_1[]
95+
96+
// tag::snippet_2[]
97+
WebClient webClient = WebClient.builder()
98+
.tls(clientTls)
99+
.baseUri("https://localhost:8080")
100+
.build();
101+
// end::snippet_2[]
102+
103+
// tag::snippet_3[]
104+
GrpcClient grpcClient = webClient.client(GrpcClient.PROTOCOL);
105+
StringServiceGrpc.StringServiceBlockingStub service =
106+
StringServiceGrpc.newBlockingStub(grpcClient.channel());
107+
// end::snippet_3[]
108+
109+
// tag::snippet_4[]
110+
Strings.StringMessage msg1 = newMessage("hello");
111+
Strings.StringMessage res1 = service.upper(msg1);
112+
String uppercased = res1.getText();
113+
// end::snippet_4[]
114+
115+
// tag::snippet_5[]
116+
Strings.StringMessage msg2 = newMessage("hello world");
117+
Iterator<Strings.StringMessage> res2 = service.split(msg2);
118+
while (res2.hasNext()) {
119+
// ...
120+
}
121+
// end::snippet_5[]
122+
123+
// tag::snippet_6[]
124+
Strings.StringMessage msg3 = newMessage("hello world");
125+
CompletableFuture<Iterator<Strings.StringMessage>> future = new CompletableFuture<>();
126+
service.split(msg3, new StreamObserver<Strings.StringMessage>() {
127+
private final List<Strings.StringMessage> value = new ArrayList<>();
128+
129+
@Override
130+
public void onNext(Strings.StringMessage value) {
131+
this.value.add(value);
132+
}
133+
134+
@Override
135+
public void onError(Throwable t) {
136+
future.completeExceptionally(t);
137+
}
138+
139+
@Override
140+
public void onCompleted() {
141+
future.complete(value.iterator());
142+
}
143+
});
144+
// end::snippet_6[]
145+
146+
// tag::snippet_7[]
147+
GrpcServiceDescriptor serviceDescriptor = GrpcServiceDescriptor.builder()
148+
.serviceName("StringService")
149+
.putMethod("Upper",
150+
GrpcClientMethodDescriptor.unary("StringService", "Upper")
151+
.requestType(Strings.StringMessage.class)
152+
.responseType(Strings.StringMessage.class)
153+
.build())
154+
.putMethod("Split",
155+
GrpcClientMethodDescriptor.serverStreaming("StringService", "Split")
156+
.requestType(Strings.StringMessage.class)
157+
.responseType(Strings.StringMessage.class)
158+
.build())
159+
.build();
160+
// end::snippet_7[]
161+
162+
// tag::snippet_8[]
163+
Strings.StringMessage res = grpcClient.serviceClient(serviceDescriptor)
164+
.unary("Upper", newMessage("hello"));
165+
// end::snippet_8[]
166+
167+
}
168+
169+
void snippets2() {
170+
Tls clientTls = Tls.builder()
171+
.trust(trust -> trust
172+
.keystore(store -> store
173+
.passphrase("password")
174+
.trustStore(true)
175+
.keystore(Resource.create("client.p12"))))
176+
.build();
177+
178+
// tag::snippet_9[]
179+
GrpcClient grpcClient = GrpcClient.builder()
180+
.tls(clientTls)
181+
.clientUriSupplier(RoundRobinSupplier.create(myServers()))
182+
.build();
183+
// end::snippet_9[]
184+
185+
// tag::snippet_10[]
186+
Channel newChannel = grpcClient.channel(myInterceptors());
187+
// end::snippet_10[]
188+
}
189+
}

0 commit comments

Comments
 (0)