grpc and WildFly - Part I

We are pleased to announce the first release of support for gRPC services in WildFly[1]. gRPC, a Google project, is, as its name suggests, a Remote Procedure Call framework. In some ways, it is a competitor to Jakarta RESTFul Web Services, but it has different semantics and a different performance profile.

gRPC rests on another Google project, protobuf, which is, according to the website, "a language-neutral, platform-neutral extensible mechanism for serializing structured data." It consists of 1) a programming language independent data definition language and 2) a compressed wire protocol for transmitting data. Moreover, protobuf data is transported over HTTP/2, and taken together, gRPC is well positioned for speedy transmission. On the other hand, the most common data format in the Jakarta REST world is the more readable but more verbose JSON, and there is evidence (Evaluating Performance of REST vs. gRPC) that gRPC transmission can be considerably faster than Jakarta REST transmission. Of course, your mileage may vary.

Given that this is a WildFly blog, and given that RESTEasy, as an implementation of Jakarta RESTful Web Services, is a foundational technology in WildFly, we will assume that the reader is familiar with Jakarta REST but not necessarily familiar with gRPC, and so we’ll take a minute to introduce the latter.

What is gRPC?

gRPC is a modern iteration in a long history of RPC frameworks, a notable example of which is CORBA. In particular, as one smart guy said[2], "gRPC is to CORBA what REST is to SOAP", which should be a relief to anyone proposing to learn gRPC.

A gRPC application begins with a language neutral description of data types and procedure calls. Consider, for example, the following, which comes from the helloworld example in the wildfly-grpc-feature-pack project (https://github.com/wildfly-extras/wildfly-grpc-feature-pack):

    syntax = "proto3";

    option java_multiple_files = true;
    option java_package = "org.wildfly.extension.grpc.example.helloworld";
    option java_outer_classname = "HelloWorldProto";
    option objc_class_prefix = "HLW";

    package helloworld;

    // The greeting service definition.
    service Greeter {
        // Sends a greeting
        rpc SayHello (HelloRequest) returns (HelloReply) {}
    }

    // The request message containing the user's name.
    message HelloRequest {
        string name = 1;
    }

    // The response message containing the greetings
    message HelloReply {
        string message = 1;
    }

Here, HelloRequest and HelloReply are message types, and SayHello is a procedure call that sends the former and receives the latter. This file can be compiled into any number of programming languages, including C++, Python, and, of course, Java. Given that the configuration parameter java_multiple_files is set to true, each of HelloRequest and HelloResponse is compiled to a somewhat opaque Java class with roughly 600 lines, not really meant for human consumption.

An additional class generated by the gRPC compiler, GreeterGrpc, has the client and server side infrastructure. For the client there is GreeterBlockingStub, which allows the client to make calls to the server, something like this:

   ManagedChannelchannel = ManagedChannelBuilder
                               .forTarget("localhost:9555")
                               .usePlaintext()
                               .build();
   GreeterGrpc.GreeterBlockingStub blockingStub = GreeterGrpc.newBlockingStub(channel);
   HelloRequest request = HelloRequest.newBuilder().setName("Bill").build();
   HelloReply response = blockingStub.sayHello(request);
   System.out.println(response.getMessage());

For the server side, there is the inner class GreeterGrpc.GreeterImplBase, which, for each procedure call, has a method that throws an exception. Implementing the service, then, is a matter of overriding each such method with real content, as in GreeterServiceImpl

   @Override
   public void sayHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
      String name = request.getName();
      String message = "Hello " + name;
      responseObserver.onNext(HelloReply.newBuilder().setMessage(message).build());
      responseObserver.onCompleted();
   }

By virtue of implementing io.grpc.BindableService, GreeterServiceImpl is a gRPC service, and it is installed as follows:

   io.grpc.Server server = ServerBuilder.forPort(9555)
                               .addService(new GreeterServiceImpl())
                               .build()
                               .start();

gRPC in WildFly

The previous snippet can be used to set up a free standing gRPC server, but we’re here to talk about the new gRPC subsystem in WildFly. It is packaged in the form of a galleon feature pack, and it can be installed in WildFly as follows

   galleon.sh install wildfly:current --dir=wildfly
   galleon.sh install org.wildfly.extras.grpc:wildfly-grpc-feature-pack:0.1.0.Final --layers=grpc --dir=wildfly

An instance of WildFly with the grpc feature pack will recognize any deployment with one or more instances of io.grpc.BindableService, will install them all, and will start listening on a port which defaults to 9555.

The grpc subsystem has over 20 configurable parameters which can be discovered by way of the jboss-cli interface:

[standalone@localhost:9990 /] cd subsystem=grpc
[standalone@localhost:9990 subsystem=grpc] ls
flow-control-window=undefined                                            permit-keep-alive-time=undefined
handshake-timeout=undefined                                              permit-keep-alive-without-calls=undefined
initial-flow-control-window=undefined                                    protocol-provider=undefined
keep-alive-time=undefined                                                server-host=localhost
keep-alive-timeout=undefined                                             server-port=9555
key-manager-name=key-manager-afcdd1f8-d1a7-4137-aa13-c45237e32428        session-cache-size=undefined
max-concurrent-calls-per-connection=undefined                            session-timeout=undefined
max-connection-age=undefined                                             shutdown-timeout=3
max-connection-age-grace=undefined                                       ssl-context-name=undefined
max-connection-idle=undefined                                            start-tls=undefined
max-inbound-message-size=undefined                                       trust-manager-name=key-manager-trust-store-eeeecd12-36f9-4156-92c7-a889383f17a1
max-inbound-metadata-size=undefined

Most of these are technical parameters used by io.grpc.netty.NettyServerBuilder. Some of the more prominent parameters are server-port, key-manager-name, and trust-manager-name.

The latter two parameters are used to configure the SSL properties of the connections between the gRPC clients and the server.

gRPC over SSL connections

SSL connections are configured by way of the elytron subsystem. First, note that if you want a plaintext connection, the key-manager-name property should be set to null. Otherwise, consider the following fragment from a standalone.xml file, which is configured for identities to be verified on both the server and the client:

<subsystem xmlns="urn:wildfly:elytron:17.0" final-providers="combined-providers" disallowed-providers="OracleUcrypto">
    ...
    <tls>
        <key-stores>
            ...
            <key-store name="key-store-afcdd1f8-d1a7-4137-aa13-c45237e32428">
                <credential-reference clear-text="secret"/>
                <implementation type="JKS"/>
                <file required="false" path="server.keystore.jks" relative-to="jboss.server.config.dir"/>
            </key-store>
            <key-store name="trust-store-eeeecd12-36f9-4156-92c7-a889383f17a1">
                <credential-reference clear-text="secret"/>
                <implementation type="JKS"/>
                <file required="false" path="server.truststore.jks" relative-to="jboss.server.config.dir"/>
            </key-store>
        </key-stores>
        <key-managers>
            ...
            <key-manager name="key-manager-afcdd1f8-d1a7-4137-aa13-c45237e32428" key-store="key-store-afcdd1f8-d1a7-4137-aa13-c45237e32428">
                <credential-reference clear-text="secret"/>
            </key-manager>
        </key-managers>
        <trust-managers>
            <trust-manager name="key-manager-trust-store-eeeecd12-36f9-4156-92c7-a889383f17a1" key-store="trust-store-eeeecd12-36f9-4156-92c7-a889383f17a1"/>
        </trust-managers>
    </tls>
</subsystem>
<subsystem xmlns="urn:wildfly:grpc:1.0" key-manager-name="key-manager-afcdd1f8-d1a7-4137-aa13-c45237e32428" trust-manager-name="key-manager-trust-store-eeeecd12-36f9-4156-92c7-a889383f17a1"/>

Note that the grpc parameter key-manager-name is set to "key-manager-afcdd1f8-d1a7-4137-aa13-c45237e32428", which refers to a key-manager configured in elytron. That key-manager refers to a keystore named "key-store-afcdd1f8-d1a7-4137-aa13-c45237e32428, which refers to file "server.keystore.jks" in the standalone/configuration directory (the value of "jboss.server.config.dir"). So, "server.keystore.jks" should be there.

Next, note that the grpc parameter trust-manager-name is set to "key-manager-trust-store-eeeecd12-36f9-4156-92c7-a889383f17a1", which is the name of a trust-manager that refers to keystore "trust-store-eeeecd12-36f9-4156-92c7-a889383f17a1", which refers to file "server.truststore.jks" in standalone/configuration. Again, that file should be present.

So, there is a keystore and a truststore on the server, and there must be a matching truststore and keystore on the client. Those can be used as follows by the client:

    ClassLoader classLoader = GreeterClient.class.getClassLoader();
    InputStream trustStore = classLoader.getResourceAsStream("client.truststore.pem");
    InputStream keyStore = classLoader.getResourceAsStream("client.keystore.pem");
    InputStream key = classLoader.getResourceAsStream("client.key.pem");
    ChannelCredentials creds = TlsChannelCredentials
                                   .newBuilder()
                                   .trustManager(trustStore)
                                   .keyManager(keyStore, key)
                                   .build();
    ManagedChannel channel = Grpc.newChannelBuilderForAddress("localhost", 9555, creds).build();
    GreeterClient client = new GreeterClient(channel);
    client.greet("world");

A more common scenario would be where only the server is required to present credentials to the client, in which case the grpc subsystem would need just a key-manager-name, associated with a keystore, and trust-manager-name is null.

Downloading

The wildfly-grpc-feature-pack jar can be downloaded from

The source code for the subsystem and examples is found here:

A more detailed discussion can be found here:

References


2. Stuart Douglas, email