gRPC is a modern RPC framework developed by Google. It picks up the traditional idea of RPC frameworks – call remote methods as easily as if they were local – while trying to avoid mistakes made by its predecessors and focusing on requirements of microservice-oriented systems. gRPC has been heavily utilized by Google for several years and has seen its first public release (1.0.) in August 2016.
Some of gRPC’s highlights include
- be highly performant by leveraging new features introduced by HTTP/2 and favoring protocol buffers over JSON
- avoid the need to write boiler-plate code by providing a simple IDL (Interface Definition Language) and code generation based on protocol buffers
- support blocking (synchronous) and non-blocking (asynchronous) calls and bi-directional streaming
- enable flow-control at application level to handle unbalanced producer/consumer speeds
- support for polyglot services with official libraries for 10+ languages and even more community provided languages
gRPC workflow in a nutshell
Per default, gRPC uses Protocol Buffers as its IDL (Interface Definition Language) and message format. So the – somewhat simplified – steps involved in creating a simple application with a service and client are:
- Describe your service, client and messages in a .proto file using proto3 syntax
- Use the protoc compiler with the gRPC extension to generate the service, client and message source code in your preferred programming language
- On the server side: Implement the service interfaces/traits (no boiler-plate just business logic)
- On the client side: Use the generated stubs to call the service
Languages supported out-of-the-box
Currently gRPC officially supports the following languages: C++, Java, Python, Go, Ruby, C#, NodeJS, Android Java, Objective-C and PHP
…and what about Scala?
ScalaPB to the rescue!
ScalaPB (Scala Protocol Buffers) is a protocol buffer compiler plugin for Scala, i.e. it creates Scala source code from .proto files. Luckily, there’s also a nice little extension that allows us to generate the Scala sources needed to implement gRPC services and clients.
Enough talking, let’s see some code
The snippets shown in this blog post only contain the relevant/important pieces of the complete application. You can find a repository containing the full source code of the provided examples at https://github.com/pbvie/hello-grpc .
Setup
As mentioned above, we’ll be using ScalaPB to generate Scala sources from our service and message definitions written using protocol buffers.
ScalaPB provides an sbt plugin to integrate the compilation of proto files into the usual Scala development workflow. Since we’re going to use gRPC, we’ll also add the required plugin to generate gRPC related sources from protocol buffers.
To add both plugins we need to edit the following files:
plugins.sbt
1addSbtPlugin("com.thesamet" % "sbt-protoc" % "0.99.3") 2libraryDependencies += "com.trueaccord.scalapb" %% "compilerplugin" % "0.5.46"
build.sbt
1// import ScalaPB 2import com.trueaccord.scalapb.compiler.Version.scalapbVersion 3 4// add these ScalaPB settings to your current settings 5PB.protoSources.in(Compile) := Seq(sourceDirectory.in(Compile).value / "proto"), 6PB.targets.in(Compile) := Seq(scalapb.gen() -> sourceManaged.in(Compile).value), 7libraryDependencies ++= Seq( 8 "com.trueaccord.scalapb" %% "scalapb-runtime" % scalapbVersion % "protobuf", 9 "com.trueaccord.scalapb" %% "scalapb-runtime-grpc" % scalapbVersion, 10 "io.grpc" % "grpc-netty" % "1.0.1" 11)
Please see https://github.com/pbvie/hello-grpc/blob/master/project/plugins.sbt and https://github.com/pbvie/hello-grpc/blob/master/build.sbt for the complete setup.
After adding the plugins, running sbt compile will automatically generate and compile Scala sources from .proto files located in src/main/proto/. To only generate the Scala sources (without compiling them) you can run sbt protoc-generate.
Single request/response
The simplest scenario of client/server communication consists of the client sending a single request to the service and receiving a single response in return.
We’ll implement a simple SumService that returns the sum of two numbers passed to it in a request.
Service and message definition
1service Sum {
2 rpc CalcSum (SumRequest) returns (SumResponse) {}
3}
4
5message SumRequest {
6 sint32 a = 1;
7 sint32 b = 2;
8}
9
10message SumResponse {
11 sint32 result = 1;
12}
The first section defines our service. In this case we’re defining a simple RPC method with a single request of type SumRequest and a single response of type SumResponse. Below we define the content of the request and response: Our request will contain two integers called a and b and our response will contain a single integer called result.
Now switch to your terminal (inside your project directory) and execute:
1sbt compile
ScalaPB should have generated the following files:
- target/scala-2.12/src_managed/main/io/ontherocks/hellogrpc/sum/SumGrpc.scala
- target/scala-2.12/src_managed/main/io/ontherocks/hellogrpc/sum/SumProto.scala
- target/scala-2.12/src_managed/main/io/ontherocks/hellogrpc/sum/SumRequest.scala
- target/scala-2.12/src_managed/main/io/ontherocks/hellogrpc/sum/SumResponse.scala
The most interesting file here is SumGrpc.scala since it contains the traits and classes needed to implemented our service and client. If you open the generated file you will find, among others, the following definitions:
1trait Sum // the trait we'll use to implement our service
2trait SumBlockingClient // the trait implemented by the client stubs
3class SumBlockingStub // a blocking (synchronous) client stub
4class SumStub // a non-blocking (asynchronous) client stub
Service implementation
Now that we have our generated code in place, let’s use it to implement our simple SumService.
1class SumService extends SumGrpc.Sum {
2 def calcSum(request: SumRequest): Future[SumResponse] = {
3 val result = SumResponse(request.a + request.b)
4 Future.successful(result)
5 }
6}
Thanks to gRPC we don’t have to concern ourselves with any boiler-plate code and can concentrate on implementing the – in this case rather simple – business logic.
Client implementation
In the last step we add our client calling the service. gRPC has generated two different kinds of client stubs for us: A blocking/synchronous client as well as a non-blocking/asynchronous one. Below we’ll use both to perform the same service call.
Both clients can use the same channel and request, so we only need to define them once:
1val channel = ManagedChannelBuilder.forAddress(Host, Port).usePlaintext(true).build 2val request = SumRequest(3, 4)
Blocking/Synchronous client
1val blockingSumClient: SumBlockingStub = SumGrpc.blockingStub(channel) 2val blockingSumResponse: SumResponse = blockingSumClient.calcSum(request)
Non-blocking/Asynchronous client
1val asyncSumClient: SumStub = SumGrpc.stub(channel) 2val asyncSumResponse: Future[SumResponse] = asyncSumClient.calcSum(request)
In the first (blocking) example we’re getting back a SumResponse directly, which is only possible by waiting for the response, hence blocking the current thread. In the second (non-blocking) examle we’re getting back a Future[SumReponse] that will eventually contain our response without blocking the current thread.
Server-side streaming
Streaming works similar to single request/response services, so we’ll only cover the parts that are different. For a complete example please see the provided GitHub repository .
We’ll implement a little ClockService that returns a stream of the current time (in ms) every second for the next 10 seconds.
Service and message definition
Again, we start by defining our service and the exchanged messages in a .proto file:
1service Clock {
2 rpc GetTime(TimeRequest) returns (stream TimeResponse) {}
3}
4
5message TimeRequest {}
6
7message TimeResponse {
8 int64 currentTime = 1;
9}
The important difference to our previous example is that we define the response of our service as a stream of TimeResponse messages.
Service implementation
After ScalaPB generates the necessary sources for us, we can implement our service:
1class ClockService extends ClockGrpc.Clock {
2 def getTime(request: TimeRequest, responseObserver: StreamObserver[TimeResponse]): Unit = {
3 val scheduler = Executors.newSingleThreadScheduledExecutor()
4 val tick = new Runnable {
5 val counter = new AtomicInteger(10)
6 def run() =
7 if (counter.getAndDecrement() >= 0) {
8 val currentTime = System.currentTimeMillis()
9 responseObserver.onNext(TimeResponse(currentTime))
10 } else {
11 scheduler.shutdown()
12 responseObserver.onCompleted()
13 }
14 }
15 scheduler.scheduleAtFixedRate(tick, 0l, 1000l, TimeUnit.MILLISECONDS)
16 }
17}
Instead of directly returning a Future, that will eventually contain our response, we now pass our responses to the StreamObserver.onNext method. When we’re done sending messages we call the StreamObserver.onComplete method to notify the client that the stream has completed.
Client Implementation
Blocking/Synchronous client
1val blockingClockClient: ClockBlockingStub = ClockGrpc.blockingStub(channel) 2val blockingClockResponse: Iterator[TimeResponse] = blockingClockClient.getTime(request) 3for (t <- blockingClockResponse) { 4 println(s"[blocking client] received: $t") 5} 6// remaining code will only be executed AFTER the stream has completed (in this case blocking our thread for 10 seconds!)
Non-blocking/Asynchronous client
1val asyncClockClient: ClockStub = ClockGrpc.stub(channel)
2val timeResponseObserver = new StreamObserver[TimeResponse] {
3 def onNext(value: TimeResponse) = println(s"[async client] received: $value")
4 def onError(t: Throwable) = println(s"[async client] error: $t")
5 def onCompleted() = println("[async client] stream completed")
6}
7
8asyncClockClient.getTime(request, timeResponseObserver)
9
10// remaining code will be executed immediatelly without waiting for the stream to be completed
While both clients will produce roughly the same output when run, the first (blocking) client will block the thread until the whole stream has completed. The async client registers an observer/callback that will handle the streamed messages as they arrive without blocking the current thread.
What’s next
Congratulations! You have written your first services using gRPC using two of the most common styles of communication: single request/response and server-side streaming. Furthermore, gRPC supports the definition of client-side streaming as well as bi-directional streaming. Both work similar to the provided server-side example and should be easy to implement with the help of the official documentation (see next section).
Follow-up material and links
- Official gRPC website: http://www.grpc.io/
- gRPC motivation and design principles: http://www.grpc.io/blog/principles
- ScalaPB: https://scalapb.github.io/
- gRPC Gitter channel: https://gitter.im/grpc/grpc
- Polyglot – gRPC command line client: https://github.com/grpc-ecosystem/polyglot
- Complementary gRPC projects: https://github.com/grpc-ecosystem
Your job at codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
More articles in this subject area
Discover exciting further topics and let the codecentric world inspire you.
Gemeinsam bessere Projekte umsetzen.
Wir helfen deinem Unternehmen.
Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.
Hilf uns, noch besser zu werden.
Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.
Blog author
Petra Bierleutgeb
Do you still have questions? Just send me a message.
Do you still have questions? Just send me a message.