Contents
Overview
The Helidon gRPC server provides a framework for creating gRPC applications. While it allows you to deploy any standard gRPC service that implements io.grpc.BindableService interface, including services generated from the Protobuf IDL files (and even allows you to customize them to a certain extent), using Helidon gRPC framework to implement your services has a number of benefits:
It allows you to define both HTTP and gRPC services using a similar programming model, simplifying the learning curve for developers.
It provides a number of helper methods that make service implementation significantly simpler.
It allows you to configure some of the Helidon value-added features, such as security and metrics collection down to the method level.
It allows you to easily specify custom marshallers for requests and responses if Protobuf does not satisfy your needs.
It provides built-in support for health checks.
Maven Coordinates
To enable gRPC Server add the following dependency to your project’s pom.xml (see Managing Dependencies).
<dependency>
<groupId>io.helidon.grpc</groupId>
<artifactId>helidon-grpc-server</artifactId>
</dependency>If gRPC server security is required as described in the section, add the following dependency to your project’s pom.xml:
<dependency>
<groupId>io.helidon.security.integration</groupId>
<artifactId>helidon-security-integration-grpc</artifactId>
</dependency>Usage
gRPC Server Routing
Unlike the webserver, which allows you to route requests based on path expression and the HTTP verb, the gRPC server always routes requests based on the service and method name. This makes routing configuration somewhat simpler — all you need to do is register your services:
private static GrpcRouting createRouting(Config config) {
return GrpcRouting.builder()
.register(new GreetService(config))
.register(new EchoService())
.register(new MathService())
.build();
}- Register
GreetServiceinstance. - Register
EchoServiceinstance. - Register
MathServiceinstance.
Both "standard" gRPC services that implement io.grpc.BindableService interface (typically implemented by extending the generated server-side stub and overriding its methods), and Helidon gRPC services that implement io.helidon.grpc.server.GrpcService interface can be registered. The difference is that Helidon gRPC services allow you to customize behavior down to the method level, and provide a number of useful helper methods that make service implementation easier, as we’ll see in a moment.
Customizing Service Definitions
When registering a service, regardless of its type, you can customize its descriptor by providing a configuration consumer as a second argument to the register method.
This is particularly useful when registering standard BindableService instances, as it allows you to add certain Helidon-specific behaviors, such as health checks and metrics to them:
private static GrpcRouting createRouting(Config config) {
return GrpcRouting.builder()
.register(new GreetService(config))
.register(new EchoService(), service -> {
service.healthCheck(CustomHealthChecks::echoHealthCheck)
.metered();
})
.build();
}- Add custom health check to the service.
- Specify that all the calls to service methods should be metered.
Specifying Global Interceptors
GrpcRouting also allows you to specify custom interceptors that will be applied to all registered services.
This is useful to configure features such as tracing, security and metrics collection, and we provide built-in interceptors for those purposes that you can simply register with the routing definition:
private static GrpcRouting createRouting(Config config) {
return GrpcRouting.builder()
.intercept(GrpcMetrics.timed())
.register(new GreetService(config))
.register(new EchoService())
.register(new MathService())
.build();
}- Register
GrpcMetricsinterceptor that will collect timers for all methods of all services (but can be overridden at the individual service or even method level).
Service Implementation
At the very basic level, all you need to do in order to implement a Helidon gRPC service is create a class that implements the io.helidon.grpc.server.GrpcService interface and define one or more methods for the service:
class EchoService implements GrpcService {
@Override
public void update(ServiceDescriptor.Rules rules) {
rules.marshallerSupplier(new JsonbMarshaller.Supplier())
.unary("Echo", this::echo);
}
/**
* Echo the message back to the caller.
*
* @param request the echo request containing the message to echo
* @param observer the response observer
*/
public void echo(String request, StreamObserver<String> observer) {
complete(observer, request);
}
}- Specify a custom marshaller to marshall requests and responses.
- Define unary method
Echoand map it to thethis::echohandler. - Create a handler for the
Echomethod. - Send the request string back to the client by completing response observer.
The complete method shown in the example above is just one of many helper methods available in the GrpcService class. See the full list here.
The example above implements a service with a single unary method which will be exposed at the `EchoService/Echo' endpoint. The service explicitly defines a marshaller for requests and responses, so this implies that you will have to implement clients by hand and configure them to use the same marshaller as the server. Obviously, one of the major selling points of gRPC is that it makes it easy to generate clients for a number of languages (as long as you use Protobuf for marshalling), so let’s see how we would implement Protobuf enabled Helidon gRPC service.
Implementing Protobuf Services
In order to implement Protobuf-based service, you would follow the official instructions on the gRPC web site, which boil down to the following:
Define the Service IDL
For this example, we will re-implement the EchoService above as a Protobuf service in echo.proto file.
syntax = "proto3";
option java_package = "org.example.services.echo";
service EchoService {
rpc Echo (EchoRequest) returns (EchoResponse) {}
}
message EchoRequest {
string message = 1;
}
message EchoResponse {
string message = 1;
}Based on this IDL, the gRPC compiler will generate message classes (EchoRequest and EchoResponse), client stubs that can be used to make RPC calls to the server, as well as the base class for the server-side service implementation.
We can ignore the last one, and implement the service using Helidon gRPC framework instead.
Implement the Service
The service implementation will be very similar to our original implementation:
class EchoService implements GrpcService {
@Override
public void update(ServiceDescriptor.Rules rules) {
rules.proto(Echo.getDescriptor())
.unary("Echo", this::echo);
}
/**
* Echo the message back to the caller.
*
* @param request the echo request containing the message to echo
* @param observer the response observer
*/
public void echo(Echo.EchoRequest request, StreamObserver<Echo.EchoResponse> observer) {
String message = request.getMessage();
Echo.EchoResponse response = Echo.EchoResponse.newBuilder().setMessage(message).build();
complete(observer, response);
}
}- Specify the proto descriptor in order to provide necessary type information and enable Protobuf marshalling.
- Define unary method
Echoand map it to thethis::echohandler. - Create a handler for the
Echomethod, using Protobuf message types for request and response. - Extract message string from the request.
- Create the response containing extracted message.
- Send the response back to the client by completing response observer.
Interceptors
Helidon gRPC allows you to configure standard interceptors using io.grpc.ServerInterceptor.
For example, you could implement an interceptor that logs each RPC call:
class LoggingInterceptor implements ServerInterceptor {
private static final Logger LOG = Logger.getLogger(LoggingInterceptor.class.getName());
@Override
public <ReqT, ResT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, ResT> call,
Metadata metadata,
ServerCallHandler<ReqT, ResT> handler) {
LOG.info(() -> "CALL: " + call.getMethodDescriptor());
return handler.startCall(call, metadata);
}
}- Implement the interceptor class using
io.grpc.ServerInterceptor. - Implement the logging logic.
- The intercepted call is started.
Registering Interceptors
You can register interceptors globally, in which case they will be applied to all methods of all services, by simply adding them to the GrpcRouting instance:
private static GrpcRouting createRouting(Config config) {
return GrpcRouting.builder()
.intercept(new LoggingInterceptor())
.register(new GreetService(config))
.register(new EchoService())
.build();
}- Adds
LoggingInterceptorto all methods ofGreetServiceandEchoService.
You can also register an interceptor for a specific service, either by implementing GrpcService.update method:
public class MyService implements GrpcService {
@Override
public void update(ServiceDescriptor.Rules rules) {
rules.intercept(new LoggingInterceptor())
.unary("MyMethod", this::myMethod);
}
private <ReqT, ResT> void myMethod(ReqT request, StreamObserver<ResT> observer) {
// do something
}
}- Adds
LoggingInterceptorto all methods ofMyService.
Or by configuring ServiceDescriptor externally, when creating GrpcRouting, which allows you to add interceptors to plain io.grpc.BindableService services as well:
private static GrpcRouting createRouting(Config config) {
return GrpcRouting.builder()
.register(new GreetService(config), cfg -> cfg.intercept(new LoggingInterceptor()))
.register(new EchoService())
.build();
}- Adds
LoggingInterceptorto all methods ofGreetServiceonly.
Finally, you can also register an interceptor at the method level:
public class MyService implements GrpcService {
@Override
public void update(ServiceDescriptor.Rules rules) {
rules.unary("MyMethod",
this::myMethod,
cfg -> cfg.intercept(new LoggingInterceptor()));
}
private <ReqT, ResT> void myMethod(ReqT request, StreamObserver<ResT> observer) {
// do something
}
}- Adds
LoggingInterceptortoMyService::MyMethodonly.
Service Health Checks
Helidon gRPC services provide built-in support for Helidon Health Checks.
Unless a custom health check is implemented by the service developer, each service deployed to the gRPC server will be provisioned with a default health check, which always returns status of UP.
This allows all services, including the ones that don’t have a meaningful health check, to show up in the health report (or to be queried for health) without service developer having to do anything.
However, services that do need custom health checks can easily define one, directly within GrpcService implementation:
public class MyService implements GrpcService {
@Override
public void update(ServiceDescriptor.Rules rules) {
rules.unary("MyMethod", this::myMethod)
.healthCheck(this::healthCheck);
}
private HealthC
heckResponse healthCheck() {
boolean fUp = isMyServiceUp();
return HealthCheckResponse
.named(name())
.state(fUp)
.withData("ts", System.currentTimeMillis())
.build();
}
private <ReqT, ResT> void myMethod(ReqT request, StreamObserver<ResT> observer) {
// do something
}
}- Configure a custom health check for the service.
- Determine the service status.
- Use service name as a health check name for consistency.
- Set the determined service status.
- Optionally provide additional metadata.
You can also define custom health checks for an existing service, including plain io.grpc.BindableService implementations, using a service configurer inside the GrpcRouting definition:
private static GrpcRouting createRouting() {
return GrpcRouting.builder()
.register(new EchoService(), cfg -> cfg.healthCheck(MyCustomHealthChecks::echoHealthCheck))
.build();
}- Configure custom health check for an existing or legacy service.
Exposing Health Checks
All gRPC service health checks are managed by the Helidon gRPC server, and are automatically exposed to the gRPC clients using a custom implementation of the standard gRPC HealthService API.
However, they can also be exposed to REST clients via the standard Helidon/Microprofile /health endpoint:
GrpcServer grpcServer = GrpcServer.create(grpcServerConfig(), createRouting(config));
grpcServer.start();
HealthSupport health = HealthSupport.builder()
.add(grpcServer.healthChecks())
.build();
Routing routing = Routing.builder()
.register(health)
.build();
WebServer.create(webServerConfig(), routing).start(); - Create the
GrpcServerinstance. - Start the gRPC server which will deploy all the services and register default and custom health checks.
- Add gRPC server managed health checks to
HealthSupportinstance. - Add
HealthSupportto the web server routing definition. - Create and start the web server.
All gRPC health checks will now be available via the /health REST endpoint, in addition to the standard gRPC HealthService
Service Metrics
The Helidon gRPC server has built-in support for metrics capture, which allows service developers to easily enable application-level metrics for their services.
Enabling Metrics Capture
By default, the gRPC server only captures two vendor-level metrics: grpc.request.count and grpc.request.meter. These metrics provide an aggregate view of requests across all services, and serve as an indication of the overall server load.
However, users can enable more fine-grained metrics by simply configuring a built-in GrpcMetrics interceptor within the routing:
private static GrpcRouting createRouting(Config config) {
return GrpcRouting.builder()
.intercept(GrpcMetrics.timed())
.register(new GreetService(config))
.register(new EchoService())
.build();
}- Capture the metrics for all methods of all services as a
timer.
In the example above we have chosen to create and keep a timer metric type for each method of each service. Alternatively, we could’ve chosen to use a counter, meter or a histogram instead.
Overriding Metrics Capture
While global metrics capture is certainly useful, it is not always sufficient. Keeping a separate timer for each gRPC method may be excessive, so the user could decide to use a lighter-weight metric type, such as a counter or a meter.
However, the user may still want to enable a histogram or a timer for some services, or even only some methods of some services.
This can be easily accomplished by overriding the type of the captured metric at either the service or the method level:
private static GrpcRouting createRouting(Config config) {
return GrpcRouting.builder()
.intercept(GrpcMetrics.counted())
.register(new MyService())
.build();
}
public static class MyService implements GrpcService {
@Override
public void update(ServiceDescriptor.Rules rules) {
rules
.intercept(GrpcMetrics.metered())
.unary("MyMethod", this::myMethod,
cfg -> cfg.intercept(GrpcMetrics.timer()));
}
private <ReqT, ResT> void myMethod(ReqT request, StreamObserver<ResT> observer) {
// do something
}
}- Use
counterfor all methods of all services, unless overridden. - Use
meterfor all methods ofMyService. - Use
timerforMyService::MyMethod.
Exposing Metrics Externally
Collected metrics are stored in the standard Helidon metric registries, such as the vendor and application registries, and can be exposed via the standard /metrics REST API.
Routing routing = Routing.builder()
.register(MetricsSupport.create())
.build();
WebServer.create(webServerConfig(), routing)
.start()- Add the
MetricsSupportinstance to web server routing. - Create and start the Helidon web server.
See Helidon Metrics documentation for more details.
Specifying Metric Metadata
Helidon metrics contain metadata such as tags, a description, units etc. It is possible to add this additional metadata when specifying the metrics.
Adding Tags
To add tags to a metric, a Map of key/value tags can be supplied.
Map<String, String> tagMap = new HashMap<>();
tagMap.put("keyOne", "valueOne");
tagMap.put("keyTwo", "valueTwo");
GrpcRouting routing = GrpcRouting.builder()
.intercept(GrpcMetrics.counted().tags(tagMap))
.register(new MyService())
.build();- The
tags()method is used to add theMapof tags to the metric.
Adding a Description
A meaningful description can be added to a metric.
GrpcRouting routing = GrpcRouting.builder()
.intercept(GrpcMetrics.counted().description("Something useful"))
.register(new MyService())
.build();- The
description()method is used to add the description to the metric.
Adding Metric Units
A units value can be added to a metric.
GrpcRouting routing = GrpcRouting.builder()
.intercept(GrpcMetrics.timed().units(MetricUnits.SECONDS))
.register(new MyService())
.build();- The
units()method is used to specify the metric units, the value of which is one of the constants from theorg.eclipse.microprofile.metrics.MetricUnitsclass.
Overriding the Metric Name
By default, the metric name is the gRPC service name followed by a dot ('.') followed by the method name. It is possible to supply a function that can be used to override the default behaviour.
The function should implement the io.helidon.grpc.metrics.GrpcMetrics.NamingFunction interface.
@FunctionalInterface
public interface NamingFunction {
/**
* Create a metric name.
*
* @param service the service descriptor
* @param methodName the method name
* @param metricType the metric type
* @return the metric name
*/
String createName(ServiceDescriptor service, String methodName, MetricType metricType);
}This is a functional interface so a lambda expression can be used too.
GrpcRouting routing = GrpcRouting.builder()
.intercept(GrpcMetrics.counted()
.nameFunction((svc, method, metric) -> "grpc." + service.name() + '.' + method) - The
NamingFunctionis just a lambda that returns the concatenated service name and method name with the prefixgrpc.. So for a service "Foo" and method "bar", the above example would produce a name "grpc.Foo.bar".
Security
To enable server security, refer to the earlier section about Security maven coordinates for guidance on what dependency to add in the project’s pom.xml.
Bootstrapping
There are two steps to configure security with the gRPC server:
- Create the security instance and register it the with server.
- Protect the gRPC services of the server with various security features.
// gRPC server's routing
GrpcRouting.builder()
// This is step 1 - register security instance with gRPC server processing
// security - instance of security either from config or from a builder
// securityDefaults - default enforcement for each service that has a security definition
.intercept(GrpcSecurity.create(security).securityDefaults(GrpcSecurity.authenticate()))
// this is step 2 - protect a service
// register and protect this service with authentication (from defaults) and role "user"
.register(greetService, GrpcSecurity.rolesAllowed("user"))
.build();// create the service descriptor
ServiceDescriptor greetService = ServiceDescriptor.builder(new GreetService())
// Add an instance of gRPC security that will apply to all methods of
// the service - in this case require the "user" role
.intercept(GrpcSecurity.rolesAllowed("user"))
// Add an instance of gRPC security that will apply to the "SetGreeting"
// method of the service - in this case require the "admin" role
.intercept("SetGreeting", GrpcSecurity.rolesAllowed("admin"))
.build();
// Create the gRPC server's routing
GrpcRouting.builder()
// This is step 1 - register security instance with gRPC server processing
// security - instance of security either from config or from a builder
// securityDefaults - default enforcement for each service that has a security definition
.intercept(GrpcSecurity.create(security).securityDefaults(GrpcSecurity.authenticate()))
// this is step 2 - add the service descriptor
.register(greetService)
.build();GrpcRouting.builder()
// helper method to load both security and gRPC server security from configuration
.intercept(GrpcSecurity.create(config))
// continue with gRPC server route configuration...
.register(new GreetService())
.build();# This may change in the future - to align with gRPC server configuration,
# once it is supported
security
grpc-server:
# Configuration of integration with gRPC server
defaults:
authenticate: true
# Configuration security for individual services
services:
- name: "GreetService"
defaults:
roles-allowed: ["user"]
# Configuration security for individual methods of the service
methods:
- name: "SetGreeting"
roles-allowed: ["admin"]Client security
When using the Helidon SE gRPC client, API security can be configured for a gRPC service or at the individual method level. The client API has a custom CallCredentials implementation that integrates with the Helidon security APIs.
Security security = Security.builder()
.addProvider(HttpBasicAuthProvider.create(config.get("http-basic-auth")))
.build();
GrpcClientSecurity clientSecurity = GrpcClientSecurity.builder(security.createContext("test.client"))
.property(EndpointConfig.PROPERTY_OUTBOUND_ID, user)
.property(EndpointConfig.PROPERTY_OUTBOUND_SECRET, password)
.build();
ClientServiceDescriptor descriptor = ClientServiceDescriptor
.builder(StringService.class)
.unary("Lower")
.callCredentials(clientSecurity)
.build();
GrpcServiceClient client = GrpcServiceClient.create(channel, descriptor);
String response = client.blockingUnary("Lower", "ABCD"); - Create the Helidon
Securityinstance which, in this case, will use the basic auth provider. - Create the
GrpcClientSecuritygRPCCallCredentialsadding the user and password property expected by the basic auth provider. - Create the gRPC
ClientServiceDescriptorfor theStringServicegRPC service. - Set the
GrpcClientSecurityinstance as the call credentials for all methods of the service. - Create a
GrpcServiceClientthat will allow methods to be called on the service. - Call the "Lower" method which will use the configured basic auth credentials.
GrpcClientSecurity clientSecurity = GrpcClientSecurity.builder(security.createContext("test.client"))
.property(EndpointConfig.PROPERTY_OUTBOUND_ID, user)
.property(EndpointConfig.PROPERTY_OUTBOUND_SECRET, password)
.build();
ClientServiceDescriptor descriptor = ClientServiceDescriptor
.builder(StringService.class)
.unary("Lower")
.unary("Upper", rules -> rules.callCredentials(clientSecurity))
.build();- Create the
GrpcClientSecuritycall credentials in the same way as above. - Create the
ClientServiceDescriptor, this time with two unary methods, "Lower" and "Upper". - The "Upper" method is configured to use the
GrpcClientSecuritycall credentials, the "Lower" method will be called without any credentials.
Outbound security
Outbound security covers three scenarios:
Calling a secure gRPC service from inside a gRPC service method handler.
Calling a secure gRPC service from inside a web server method handler.
Calling a secure web endpoint from inside a gRPC service method handler.
Within each scenario, credentials can be propagated if the gRPC/http method handler is executing within a security context or credentials can be overridden to provide a different set of credentials to use for calling the outbound endpoint.
// Obtain the SecurityContext from the current gRPC call Context
SecurityContext securityContext = GrpcSecurity.SECURITY_CONTEXT.get();
// Create a gRPC CallCredentials that will use the current request's
// security context to configure outbound credentials
GrpcClientSecurity clientSecurity = GrpcClientSecurity.create(securityContext);
// Create the gRPC stub using the CallCredentials
EchoServiceGrpc.EchoServiceBlockingStub stub = noCredsEchoStub.withCallCredentials(clientSecurity);private static void propagateCredentialsWebRequest(ServerRequest req, ServerResponse res) {
try {
// Create a gRPC CallCredentials that will use the current request's
// security context to configure outbound credentials
GrpcClientSecurity clientSecurity = GrpcClientSecurity.create(req);
// Create the gRPC stub using the CallCredentials
EchoServiceGrpc.EchoServiceBlockingStub stub = noCredsEchoStub.withCallCredentials(clientSecurity);
String message = req.queryParams().first("message").orElse(null);
Echo.EchoResponse echoResponse = stub.echo(Echo.EchoRequest.newBuilder().setMessage(message).build());
res.send(echoResponse.getMessage());
} catch (StatusRuntimeException e) {
res.status(GrpcHelper.toHttpResponseStatus(e)).send();
} catch (Throwable thrown) {
res.status(Http.ResponseStatus.create(500, thrown.getMessage())).send();
}
}// Obtain the SecurityContext from the gRPC call Context
SecurityContext securityContext = GrpcSecurity.SECURITY_CONTEXT.get();
// Use the SecurityContext as normal to make a http request
Response webResponse = client.target(url)
.path("/test")
.request()
.property(ClientSecurity.PROPERTY_CONTEXT, securityContext)
.get();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 implement the update method on your service’s class and set the custom marshaller supplier via the ServiceDescriptor.Rules.marshallerSupplier() method:
public class GreetServiceJava
implements GrpcService {
private String greeting;
public GreetServiceJava(Config config) {
this.greeting = config.get("app.greeting").asString().orElse("Ciao");
}
@Override
public void update(ServiceDescriptor.Rules rules) {
rules.marshallerSupplier(new JsonbMarshaller.Supplier())
.unary("Greet", this::greet)
.unary("SetGreeting", this::setGreeting);
}
// Implement Service methods
}- Specify the custom marshaller to use.
Configuration
Configure the gRPC server using the Helidon configuration framework, either programmatically or via a configuration file.
Configuring the gRPC Server in the Code
The easiest way to configure the gRPC server is in the application code.
GrpcServerConfiguration configuration = GrpcServerConfiguration.builder()
.port(8080)
.build();
GrpcServer grpcServer = GrpcServer.create(configuration, routing);See all configuration options here.
Configuring the gRPC Server in a Configuration File
You can also define the gRPC server configuration in a file.
Configuration Options
| key | type | default value | description |
|---|---|---|---|
name | string | grpc.server | Set the name of the gRPC server. Configuration key: `name` |
native | boolean | false | Specify if native transport should be used. |
port | int | 1408 | Sets server port. If port is Configuration key: `port` |
workers | int | Number of processors available to the JVM | Sets a count of threads in pool used to process HTTP requests. Default value is Configuration key: `workers` |
application.yamlgrpc:
port: 3333Then, in your application code, load the configuration from that file.
application.conf file located on the classpathGrpcServerConfiguration configuration = GrpcServerConfiguration.create(
Config.builder()
.sources(classpath("application.conf"))
.build());
GrpcServer grpcServer = GrpcServer.create(configuration, routing);Examples
Quick Start
Here is the code for a minimalist gRPC application that runs on a default port (1408):
public static void main(String[] args) throws Exception {
GrpcServer grpcServer = GrpcServer
.create(GrpcRouting.builder()
.register(new HelloService())
.build())
.start()
.toCompletableFuture()
.get(10, TimeUnit.SECONDS); // Implement the simplest possible gRPC service.
System.out.println("gRPC server started at: http://localhost:" + grpcServer.port());
}
static class HelloService implements GrpcService {
@Override
public void update(ServiceDescriptor.Rules rules) {
rules.marshallerSupplier(new JsonbMarshaller.Supplier())
.unary("SayHello", ((request, responseObserver) -> complete(responseObserver, "Hello " + request)));
}
}- Register the gRPC service.
- Start the server.
- Wait for the server to start while throwing possible errors as exceptions.
- The server is bound to a default port (1408).
- Implement the simplest possible gRPC service.
- Specify a custom marshaller using the built-in JsonB marshaller to marshall requests and responses.
- Add unary method
HelloService/SayHelloto the service definition.
Additional gRPC Server Examples
A set of gRPC server examples for Helidon SE can be found in the following links: