Tutorial: gRPC server and client

This tutorial will show you how to implement a working gRPC server and client based on a Mu service defintion.

This tutorial is aimed at developers who:

Mu supports both Protobuf and Avro. For the purposes of this tutorial we will assume you are using Protobuf, but it’s possible to follow the tutorial even if you are using Avro.

Service definition

If you have followed one of the previous tutorials, you should already have a service definition that looks like this:

trait Greeter[F[_]] {
  def SayHello(req: HelloRequest): F[HelloResponse]
}

object Greeter {

  // ... lots of generated code

}

Implement the server

This is the interesting part: writing the business logic for your service.

We do this by implementing the Greeter trait. Let’s make a Greeter that says “hello” in a happy voice:

import cats.Applicative
import cats.syntax.applicative.*
import mu.examples.protobuf.greeter.*

class HappyGreeter[F[_]: Applicative] extends Greeter[F] {

  def SayHello(req: HelloRequest): F[HelloResponse] =
    HelloResponse(s"Hello, ${req.name}!", happy = true).pure[F]

}

Note that in this implementation we aren’t performing any effects, so we don’t care what F[_] is as long as we can lift a pure value into it.

Server entrypoint

Now we have a Greeter implementation, let’s expose it as a gRPC server.

We’re going to use cats-effect IO as our concrete IO monad, and we’ll make use IOApp from cats-effect.

import cats.effect.{IO, IOApp, ExitCode, Resource}
import mu.examples.protobuf.greeter.Greeter
import higherkindness.mu.rpc.server.{GrpcServer, AddService}

object Server extends IOApp {

  given Greeter[IO] = new HappyGreeter[IO]  // 1

  def run(args: List[String]): IO[ExitCode] = (for {
    serviceDef <- Greeter.bindService[IO]                                                     // 2
    server     <- Resource.eval(GrpcServer.default[IO](12345, List(AddService(serviceDef))))  // 3
    _          <- GrpcServer.serverResource[IO](server)                                               // 4
  } yield ()).useForever

}

Let’s go through this line by line.

  1. First we instantiate our HappyGreeter, concretized to IO, and make it available implicitly for use by Greeter.bindService.

  2. Next we call Greeter.bindService. This is an auto-generated helper method that converts our Greeter service into a gRPC “service definition”, returning IO[io.grpc.ServerServiceDefinition]. Each Scala method in the service will become a gRPC method of the same name.

  3. We build a description of the whole gRPC server by calling GrpcServer.default. We tell it the port we want to run on (12345), and the list of services we want it to expose. The method is called default because we want to use gRPC’s default HTTP transport layer.

  4. Finally we can call GrpcServer.serverResource, passing it our server description. This actually starts the server.

If you copy the above code into a .scala file in the server module of your project, you should be able to start a server using sbt server/run.

Client

Let’s see how to make a client to communicate with the server.

Here is a tiny demo that makes a request to the SayHello endpoint and prints out the reply to the console.

import cats.effect.{IO, IOApp, Resource, ExitCode}
import mu.examples.protobuf.greeter.{Greeter, HelloRequest}
import higherkindness.mu.rpc.*

object ClientDemo extends IOApp {

  val channelFor: ChannelFor = ChannelForAddress("localhost", 12345)  // 1

  val clientResource: Resource[IO, Greeter[IO]] = Greeter.client[IO](channelFor)  // 2

  def run(args: List[String]): IO[ExitCode] =
    for {
      response   <- clientResource.use(c => c.SayHello(HelloRequest(name = "Chris")))  // 3
      serverMood = if (response.happy) "happy" else "unhappy"
      _          <- IO(println(s"The $serverMood server says '${response.greeting}'"))
    } yield ExitCode.Success

}

Again we’ll go through this line by line.

  1. We create a channel, which tells the client how to connect to the server.

  2. We call Greeter.client, another auto-generated helper method. This returns a cats-effect Resource, which will take care of safely allocating and cleaning up resources every time we want to use a client.

  3. We use the resource, using the resulting client to send a request to the SayHello endpoint and get back a response.

If you copy the above code into a .scala file in the client module of your project, and your server is still running, you should be able to see the client in action using sbt client/run.

[info] running com.example.ClientDemo
The happy server says 'Hello, Chris!'
[success] Total time: 1 s, completed 5 Mar 2020, 15:49:03

Next steps

If you want to write tests for your RPC service, take a look at the Testing an RPC service tutorial.