Running a gRPC service

Let’s assume we have the following gRPC service definition:

syntax = "proto3";

package grpc.hello;

option java_package = "com.example.grpc.hello";

service HelloService {
  rpc Hello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

The Protobuf compiler will produce some Java code under the com.example.grpc.hello package. The most noteworthy one is HelloServiceGrpc.java which defines the base service class we will implement:

public class HelloServiceGrpc {
    ...
    public static abstract class HelloServiceImplBase implements BindableService {
        public void hello(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
            asyncUnimplementedUnaryCall(METHOD_HELLO, responseObserver);
        }
        ...
    }
    ...
}

Our implementation would look like the following:

public class MyHelloService extends HelloServiceGrpc.HelloServiceImplBase {
    @Override
    public void hello(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
        HelloReply reply = HelloReply.newBuilder()
                                     .setMessage("Hello, " + req.getName() + '!')
                                     .build();
        responseObserver.onNext(reply);
        responseObserver.onCompleted();
    }
}

Note

It is recommended to use the build plugin as explained in the gRPC-Java README rather than running protoc manually.

GrpcService

Once you’ve finished the implementation of the service, you need to build a GrpcService using a GrpcServiceBuilder and add it to the ServerBuilder:

import com.linecorp.armeria.server.Server;
import com.linecorp.armeria.server.ServerBuilder;
import com.linecorp.armeria.server.grpc.GrpcServiceBuilder;

ServerBuilder sb = new ServerBuilder();
...
sb.service(new GrpcServiceBuilder().addService(new MyHelloService())
                                   .build());
...
Server server = sb.build();
server.start();

Note

We bound the GrpcService without specifying any path mappings. It is because GrpcService implements ServiceWithPathMappings, which dynamically provides path mappings by itself.

gRPC-Web

GrpcService supports the gRPC-Web protocol, a small modification to the gRPC wire format that can be used from a browser. To enable it for a GrpcService, add the web formats from GrpcSerializationFormats to the GrpcServiceBuilder. It is usually convenient to just pass GrpcSerializationFormats.values().

import com.linecorp.armeria.server.grpc.GrpcSerializationFormats;

ServerBuilder sb = new ServerBuilder();
...
sb.service(new GrpcServiceBuilder().addService(new MyHelloService())
                                   .supportedSerializationFormats(GrpcSerializationFormats.values())
                                   .build());
...
Server server = sb.build();
server.start();

The server will support both native gRPC and gRPC-Web from the same endpoint. Use the unofficial gRPC-Web-Client to access the service from a browser. gRPC-Web does not support RPC methods with streaming requests.

If the origin of the Javascript and API server are different, gRPC-Web-Client first sends preflight requests by the HTTP OPTIONS method, in order to determine whether the actual request is safe to send in terms of CORS. Armeria provides CorsService to handle this requests, so you need to decorate it when you build a GrpcService:

import com.linecorp.armeria.server.cors.CorsServiceBuilder;

ServerBuilder sb = new ServerBuilder();
...

final CorsServiceBuilder corsBuilder =
        CorsServiceBuilder.forOrigin("http://foo.com")
                          .allowRequestMethods(HttpMethod.POST) // Allow POST method.
                          // Allow Content-type and X-GRPC-WEB headers.
                          .allowRequestHeaders(HttpHeaderNames.CONTENT_TYPE,
                                               HttpHeaderNames.of("X-GRPC-WEB"));

sb.service(new GrpcServiceBuilder().addService(new MyHelloService())
                                   .supportedSerializationFormats(GrpcSerializationFormats.values())
                                   .build(), corsBuilder.newDecorator());
...
Server server = sb.build();
server.start();

Please refer to Cross-Origin Resource Sharing (CORS) by MDN for more information.

Unframed requests

GrpcService supports serving unary RPC methods (no streaming request or response) without gRPC wire format framing. This can be useful for gradually migrating an existing HTTP POST based API to gRPC. As GrpcService supports both binary protobuf and Protobuf-JSON, either legacy protobuf or JSON APIs can be used.

ServerBuilder sb = new ServerBuilder();
...
sb.service(new GrpcServiceBuilder().addService(new MyHelloService())
                                   .enableUnframedRequests(true)
                                   .build());
...
Server server = sb.build();
server.start();

This service’s unary methods can be accessed from any HTTP client at e.g., URL /grpc.hello.HelloService/Hello with Content-Type application/protobuf for binary protobuf POST body or application/json for JSON POST body.

Blocking service implementation

Unlike upstream gRPC-java, Armeria does not run service logic in a separate thread pool. If your service implementation requires blocking, either run the individual blocking logic in a thread pool, or just wrap the entire service implementation in RequestContext.current().blockingTaskExecutor().submit

import com.linecorp.armeria.common.RequestContext;
import com.linecorp.armeria.server.ServiceRequestContext;

public class MyHelloService extends HelloServiceGrpc.HelloServiceImplBase {
    @Override
    public void hello(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
        ServiceRequestContext ctx = (ServiceRequestContext) RequestContext.current();
        ctx.blockingTaskExecutor().submit(() -> {
            Thread.sleep(10000);
            HelloReply reply = HelloReply.newBuilder()
                                         .setMessage("Hello, " + req.getName() + '!')
                                         .build();
            responseObserver.onNext(reply);
            responseObserver.onCompleted();
        });
    }
}