Running a gRPC service
Table of contents
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();
}
}
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;
ServerBuilder sb = Server.builder();
...
sb.service(GrpcService.builder()
.addService(new MyHelloService())
.build());
...
Server server = sb.build();
server.start();
Decorating a GrpcService
Unlike a usual Armeria service, GrpcService
implements a special interface called
HttpServiceWithRoutes
. Therefore, it is recommended to decorate a GrpcService
by specifying
decorator functions as extra parameters rather than using Service.decorate()
:
import com.linecorp.armeria.server.logging.LoggingService;
ServerBuilder sb = Server.builder();
...
sb.service(GrpcService.builder()
.addService(new MyHelloService())
.addService(new MySecondHelloService())
.build(),
LoggingService.newDecorator());
...
Server server = sb.build();
server.start();
The above decorator decorates all services. If you want to decorate only a specific service, you need to specify
the decorators when you add the service to the GrpcServiceBuilder
,
or use the @Decorator
.
GrpcService.builder()
.addService(new MyHelloService(),
List.of(new FirstDecorator(), new SecondDecorator())) // 👈👈👈
.build()
// Or, use the @Decorator directly to the service or method.
@Decorator(ThirdDecorator.class) // 👈👈👈
class MyHelloService extends MyHelloServiceBase {
@Decorator(FourthDecorator.class) // 👈👈👈
@Override
public void sayHello(HelloRequest request, StreamObserver<HelloResponse> responseObserver) {
...
}
}
As you might have noticed already, the decorators will be applied in the order of
FirstDecorator
, SecondDecorator
, ThirdDecorator
, and FourthDecorator
.
gRPC-Web
GrpcService
supports the gRPC-Web protocol,
a small modification to the gRPC wire format that can be used from a browser.
Use the gRPC-Web-Client to access the service from a browser.
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.common.grpc.protocol.GrpcHeaderNames;
import com.linecorp.armeria.server.cors.CorsService;
import com.linecorp.armeria.server.cors.CorsServiceBuilder;
ServerBuilder sb = Server.builder();
...
final CorsServiceBuilder corsBuilder =
CorsService.builder("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"))
// Expose trailers of the HTTP response to the client.
.exposeHeaders(GrpcHeaderNames.GRPC_STATUS,
GrpcHeaderNames.GRPC_MESSAGE,
GrpcHeaderNames.ARMERIA_GRPC_THROWABLEPROTO_BIN);
sb.service(GrpcService.builder()
.addService(new MyHelloService())
.build(),
corsBuilder.newDecorator(),
LoggingService.newDecorator());
...
Server server = sb.build();
server.start();
Please refer to Configuring CORS 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 = Server.builder();
...
sb.service(GrpcService.builder()
.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; charset=utf-8
for JSON POST body.
Blocking service implementation
Unlike upstream gRPC-Java, Armeria does not run service logic in a separate thread pool by default. If your
service implementation requires blocking, run the individual blocking logic in a thread pool,
annotate the gRPC services or methods with a @Blocking
annotation, wrap the entire service implementation in
ServiceRequestContext.current().blockingTaskExecutor().submit()
, or set
GrpcServiceBuilder.useBlockingTaskExecutor()
so the above happens automatically for
all service methods and lifecycle callbacks.
Using @Blocking
annotation
// You can apply the @Blocking annotation at the class level
// so all methods are executed by ServiceRequestContext.current().blockingTaskExecutor().
@Blocking // 👈
class MyBlockingGrpcServiceImpl extends MyGrpcServiceImplBase {
public void blockingCall1(...) {
// Perform a long-running task.
...
}
public void blockingCall2(...) {
// Perform a long-running task.
...
}
}
class MyGrpcServiceImpl extends MyGrpcServiceImplBase {
// The method will be executed by ServiceRequestContext.current().blockingTaskExecutor().
@Blocking // 👈
public void blockingCall(...) {
// Perform a long-running task.
...
}
public void nonBlockingCall(...) {
...
}
}
Wrapping a service with ServiceRequestContext.blockingTaskExecutor()
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.current().blockingTaskExecutor().submit(() -> {
// Perform a long-running task.
HelloReply reply = HelloReply.newBuilder()
.setMessage("Hello, " + req.getName() + '!')
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
});
}
}
Using GrpcServiceBuilder.useBlockingTaskExecutor()
ServerBuilder sb = Server.builder();
sb.service(GrpcService.builder()
.addService(new MyHelloService())
// All service methods will be run within
// the blocking executor.
.useBlockingTaskExecutor(true) // 👈
.build());
Exception propagation
It can be very useful to enable Flags.verboseResponses()
in your server by specifying the
-Dcom.linecorp.armeria.verboseResponses=true
system property, which will automatically return
information about an exception thrown in the server to gRPC clients. Armeria clients will automatically
convert it back into an exception for structured logging, etc. This response will include information about
the actual source code in the server - make sure it is safe to send such potentially sensitive information
to all your clients before enabling this flag!
See more details at Client gRPC.
Server Reflection
Armeria supports gRPC server reflection - just add an instance of ProtoReflectionService
to your server.
import io.grpc.protobuf.services.ProtoReflectionService;
ServerBuilder sb = Server.builder();
...
sb.service(GrpcService.builder()
.addService(new MyHelloService())
.addService(ProtoReflectionService.newInstance())
.build());
...
Server server = sb.build();
server.start();
For more information, see the official gRPC Server Reflection tutorial.