In the previous post we’ve seen how to implement a gRPC service in Scala. While gRPC is a great way to implement remote services, many client/server interactions are still implemented using REST/HTTP nowadays.
So the question is: Is it possible to use gRPC to define and implement services and make them available over REST at the same time?
Well, it turns out it’s possible. The translation from protobuf to JSON format is quite straightforward. The only thing left is to define associated endpoints for each of the gRPC calls. Here again it can be easily done along with the gRPC definition using the HTTP annotations from Google.
This is exactly what GRPC-Gateway provides. While GRPC-Gateway is implemented in Go it can run along with any gRPC implementation as it only relies on the protobuf/gRPC definitions (.proto files). It then spawns up an HTTP proxy that translates and forwards REST (HTTP/JSON) calls to a gRPC server.
Fortunately ScalaPB‘s code generation makes it possible to implement the same functionalities in Scala.
Architecture
The Scala’s GRPC-Gateway is made of 3 components:
- The code generator: Generates source files from the .proto files definition
- The runtime library: Provides the runtime infrastructure (e.g. Netty server)
- The user code: Implements business logic and runs infrastructure (e.g. spawns up server on specific port)
Code generators
Code generators run at compile time to generate application source files. Therefore their code is similar to compiler plugin code and is executed directly by SBT. It means the generator source code must be compatible with the Scala version used by SBT. Currently Scala 2.10 is used by SBT 0.13.
As the GRPC-Gateway code generators relies on annotations define in protobuf files, these files need to be compile against Scala 2.10.
Note that this code won’t be embedded into the application’s package, it is only used to generate sources that will in turn be compiles and packaged with the application.
ScalaPB code generation
ScalaPB provides an easy way to define your own code generators. All you have to do is to extend protocbridge.ProtocCodeGenerator
and implement the def run(request: CodeGeneratorRequest): CodeGeneratorResponse
method.
Then the code generation is performed by means of a FunctionalPrinter
to which you pass the strings of generated code:
val fp = FunctionalPrinter() .add(s"package io.grpc.gateway.MyGatewayHandler") .newline .add("object MyGatewayHandler { /* More code comes here */ }") .newline
All you have to do is then create a File.Builder
and add the content using the FunctionalPrinter
val b = CodeGeneratorResponse.File.newBuilder() b.setName(s"io/grpc/gateway/MyGatewayHandler.scala") b.setContent(fp.result) val file = b.build
Finally we need to turn the File
into a CodeGeneratorResponse
:
val b = CodeGeneratorResponse.newBuilder b.addFile(file) b.build
As the FunctionalPrinter
accepts any String we can use this very same mechanism to generate any type of file. In fact we use it to generate the Swagger YAML specification corresponding to the GRPC definitions.
The HTTP annotations
In order to define the HTTP endpoint associated to a gRPC call, the gRPC method definitions must be annotated with an HTTP option extension:
import "google/api/annotations.proto"; rpc GetFeature(Point) returns (Feature) { // define associated REST endpoint on GRPC Gateway option (google.api.http) = { get: "/api/routeguide/feature" }; }
This example specifies that the GetFeature
method is available on the path /api/routeguide/feature
using the HTTP GET method.
This also introduces a dependency to the annotations.proto
file. This file is provided by Google and must be available in the user project when scalaPB parses the .proto files.
In order to reduce the user burden this file is embedded in the runtime’s jar file so the user only has to add the following dependency to his build.sbt
:
libraryDependencies += "beyondthelines" %% "grpcgatewayruntime" % "0.0.1" % "compile,protobuf"
Note that the protobuf
is important as it tells ScalaPB to look into this jar for .proto files.
One pitfall worth noting is that in order to be able to use extensions the corresponding .proto files must be compiled in Java. In our case it means that the Google annotations .proto files must be compiled as Java sources. Fortunately it’s just a matter of configuring ScalaPB accordingly:
PB.targets in Compile += PB.gens.java -> (sourceManaged in Compile).value
JSON translation
The JSON translation is provided by the scalapb-json4s library which is able to translate any GeneratedMessage
(case classes generated from protobuf definitions) into JSON.
unaryCall(req.method(), req.uri(), body) // calls the gRPC service .map(JsonFormat.toJsonString) // transform into JSON format .map(_.getBytes(StandardCharsets.UTF_8)) // transform into bytes
Similarly transforming an HTTP request body is straightforward:
printer.add( s"val input = Try(JsonFormat.fromJsonString[${method.getInputType.getName}](body))" )
This snippet above is from the GRPC-Gateway generator. It generates the following code:
val input = Try(JsonFormat.fromJsonString[T](body))
which turns a JSON string into an instance of type T
.
The Swagger generator
The swagger generator is quite simple. It iterates over the services and methods definitions and generates corresponding Swagger specification for unary methods calls (no streaming supported) having an HTTP endpoint defined using the HTTP annotation.
It doesn’t generate a Scala file but a YAML file (.yml).
The GRPC-Gateway generator
The structure of the gateway generator follows a similar structure except that this time it does generate a Scala source file.
As the generated code is going to be compiled and included with the application code it doesn’t have to be limited to Scala 2.10 but can use the latest Scala version.
The code generated for the GRPC-Gateway is a simple Netty handler in charge of:
- Building gRPC input parameters from requests body or query string
- Performing corresponding gRPC call using the instances extracted above
- Translating back the responses into JSON
- Forwarding the translated responses to the HTTP Client
In fact only the extraction of the input parameters and the call to the appropriate gRPC method is generated.
The last 2 actions (translating the response to Json and sending the response back) are generic enough so they can be placed into a super class that is extended by the generated code.
printer .add(s"class ${service.getName}Handler(channel: ManagedChannel)(implicit ec: ExecutionContext)") .indent .add( "extends GrpcGatewayHandler(channel)(ec) {", s"""override val name: String = "${service.getName}"""", s"private val stub = ${service.getName}Grpc.stub(channel)" ) .newline .call(generateUnaryCall(service)) .outdent .add("}") .newline
The runtime library
The GRPC Gateway Handler
The runtime provides the Netty’s handler common functionality that is extended by the generated code. This is the GrpcGatewayHandler
. Its role is to extract the HTTP request body and parameters, invoke the subclass method to trigger the call to the GRPC server, then translate the response into JSON and send it back to the HTTP client.
The Swagger Handler
The swagger handler is a simple Netty handler that relies Swagger-UI to serve the generated swagger specification.
It gets the requested files from the classpath (as both the generated swagger specs and the swagger-ui files are packaged inside jars) and serves them back to the client.
The swagger .yml files is available on the /specs
path while the swagger files are available on the /docs
files.
Therefore to open the swagger docs one must point its browser to the following url: http://localhost:8981/docs/index.html?url=/specs/RouteGuide.yml. (Assuming the GRPC gateway runs on localhost:8981 and that the spec file is named RouteGuide.yml
).
The server utilities
Finally the runtime provides a utility class GrpcGatewayServerBuilder
in order to start a gateway server easily.
This is what a user must write in order to start its own gateway:
// channel pointing to the GRPC server val channel = ManagedChannelBuilder .forAddress("localhost", 8980) .usePlaintext(true) .build() val gateway = GrpcGatewayServerBuilder .forPort(port) // add the generated handler .addService(new RouteGuideHandler(channel)) .build() gateway.start()
Conclusion
Here ends our tour of the GRPC gateway implementation. It is merely a Proof-of-concept but it should get you through the main pitfalls of working with protobuf code generation.
It still suffers a few limitations: no streaming is supported (might be worth looking at websocket) or try a different backend. In fact I’ll be glad to see gRPC running on top of Akka-Http now that it officially supports HTTP/2 it surely sounds possible.
And as always the source code is available on github for those who want to have a closer look or contribute.
If you only want to give it a try you may have a look at this example. Enjoy!