Akka gRPC Quickstart with Scala
Akka gRPC is a toolkit for building streaming gRPC servers and clients on top of Akka Streams. This guide will get you started building gRPC based systems with Scala. If you prefer to use Akka gRPC with Java, switch to the Akka gRPC Quickstart with Java guide.
After trying this example the Akka gRPC documentation is a good next step to continue learning more about Akka gRPC.
Downloading the example
The Hello World example for Scala is a zipped project that includes build files for sbt, Maven and Gradle. You can choose any of these build tools. You can run it on Linux, MacOS, or Windows. The only prerequisite is Java 17 or later, and a local installation of the build tool.
- Download the project zip file.
- Extract the zip file to a convenient location:
- On Linux and OSX systems, open a terminal and use the command
unzip akka-grpc-quickstart-scala.zip
. - On Windows, use a tool such as File Explorer to extract the project.
Running the example
To run Hello World:
On OSX/Linux use sbt
to start sbt in the instructions below, on Windows sbt.bat
. On OSX/Linux use ./gradlew
to start Gradle in the instructions below, on Windows ./gradlew.bat
. For Gradle you have to make the build tool executable with chmod u+x ./gradlew
-
In a console, change directories to the top level of the unzipped project.
For example, if you used the default project name, akka-grpc-quickstart, and extracted the project to your root directory, from the root directory, enter:
cd akka-grpc-quickstart
-
Compile the project by entering:
- sbt
-
sbt compile
- Maven
-
mvn compile
- Gradle
-
./gradlew compileScala
sbtMavenGradle downloads project dependencies, generates gRPC classes from protobuf, and compiles.
-
Run the server:
- sbt
-
sbt "runMain com.example.helloworld.GreeterServer"
- Maven
-
mvn compile dependency:properties exec:exec@server
- Gradle
-
./gradlew runServer
sbtMavenGradle runs the
com.example.helloworld.GreeterServer
main class that starts the gRPC server. Theexec:exec@server
execution is defined in the Mavenpom.xml
build definition. TherunServer
task is defined inbuild.gradle
.The output should include something like:
gRPC server bound to: /127.0.0.1:8080
-
Run the client, open another console window and enter:
- sbt
-
sbt "runMain com.example.helloworld.GreeterClient"
- Maven
-
mvn compile dependency:properties exec:exec@client
- Gradle
-
./gradlew runClient
sbtMavenGradle runs the
com.example.helloworld.GreeterClient
main class that starts the gRPC client. Theexec:exec@client
execution is defined in the Mavenpom.xml
build definition. TherunClient
task is defined inbuild.gradle
.The output should include something like:
Performing request: Alice Performing request: Bob HelloReply(Hello, Bob,UnknownFieldSet(Map())) HelloReply(Hello, Alice,UnknownFieldSet(Map()))
Congratulations, you just ran your first Akka gRPC server and client. Now take a look at what happened under the covers.
You can end the programs with ctrl-c
.
What Hello World does
As you saw in the console output, the example outputs several greetings. Let’s take at the code and what happens at runtime.
Server
First, the GreeterServer
main class creates an akka.actor.typed.ActorSystem
, a container in which Actors, Akka Streams and Akka HTTP run. Next, it defines a function from HttpRequest
to Future[HttpResponse]
using the GreeterServiceImpl
. This function handles gRPC requests in the HTTP/2 server and is bound to port 8080 in this example.
source
import akka.actor.typed.ActorSystem
import akka.actor.typed.scaladsl.Behaviors
import akka.http.scaladsl.ConnectionContext
import akka.http.scaladsl.Http
import akka.http.scaladsl.common.SSLContextFactory
import akka.http.scaladsl.model.HttpRequest
import akka.http.scaladsl.model.HttpResponse
import com.typesafe.config.ConfigFactory
import java.nio.file.Paths
import scala.concurrent.ExecutionContext
import scala.concurrent.Future
import scala.concurrent.duration._
import scala.util.Failure
import scala.util.Success
object GreeterServer {
def main(args: Array[String]): Unit = {
// important to enable HTTP/2 in ActorSystem's config
val conf =
ConfigFactory.parseString("akka.http.server.enable-http2 = on").withFallback(ConfigFactory.defaultApplication())
val system = ActorSystem[Nothing](Behaviors.empty[Nothing], "GreeterServer", conf)
new GreeterServer(system).run()
}
}
class GreeterServer(system: ActorSystem[_]) {
def run(): Future[Http.ServerBinding] = {
implicit val sys = system
implicit val ec: ExecutionContext = system.executionContext
val service: HttpRequest => Future[HttpResponse] =
GreeterServiceHandler(new GreeterServiceImpl(system))
val serverHttpContext = ConnectionContext.httpsServer(
SSLContextFactory.createSSLContextFromPem(
// Note: filesystem paths, not classpath
Paths.get("src/main/resources/certs/server1.pem"),
Paths.get("src/main/resources/certs/server1.key")))
val bound: Future[Http.ServerBinding] = Http()(system)
.newServerAt(interface = "127.0.0.1", port = 8080)
.enableHttps(serverHttpContext)
.bind(service)
.map(_.addToCoordinatedShutdown(hardTerminationDeadline = 10.seconds))
bound.onComplete {
case Success(binding) =>
val address = binding.localAddress
println(s"gRPC server bound to ${address.getHostString}:${address.getPort}")
case Failure(ex) =>
println("Failed to bind gRPC endpoint, terminating system")
ex.printStackTrace()
system.terminate()
}
bound
}
}
GreeterServiceImpl
is our implementation of the gRPC service, but first we must define the interface of the service in the protobuf file src/main/protobuf/helloworld.proto
:
sourcesyntax = "proto3";
option java_multiple_files = true;
option java_package = "com.example.helloworld";
option java_outer_classname = "HelloWorldProto";
// The greeting service definition.
service GreeterService {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}
When compiling the project several things are generated from the proto definition. You can find the generated files in target/scala-2.13/akka-grpc/main/
target/generated-sources/
build/generated/source/proto/main/
if you are curious.
For the server the following classes are generated:
- Message classes, such as
HelloRequest
andHelloReply
GreeterService
interface of the serviceGreeterServiceHandler
utility to create theHttpRequest
toHttpResponse
function from theGreeterServiceImpl
The part that we have to implement on the server side is the GreeterServiceImpl
which implements the generated GreeterService
interface. It is this implementation that is bound to the HTTP
server via the GreeterServiceHandler
and it looks like this:
sourceimport scala.concurrent.Future
import akka.NotUsed
import akka.actor.typed.ActorSystem
import akka.stream.scaladsl.BroadcastHub
import akka.stream.scaladsl.Keep
import akka.stream.scaladsl.MergeHub
import akka.stream.scaladsl.Sink
import akka.stream.scaladsl.Source
class GreeterServiceImpl(system: ActorSystem[_]) extends GreeterService {
private implicit val sys: ActorSystem[_] = system
override def sayHello(request: HelloRequest): Future[HelloReply] = {
Future.successful(HelloReply(s"Hello, ${request.name}"))
}
}
Client
In this example we have the client in the same project as the server. That is common for testing purposes but for real usage you or another team would have a separate project (different service) that is using the client and doesn’t implement the server side of the service. Between such projects you would only share the proto file (by copying it).
From the same proto file that was used on the server side classes are generated for the client:
- Message classes, such as
HelloRequest
andHelloReply
GreeterService
interface of the serviceGreeterServiceClient
that implements the client side of theGreeterService
On the client side we don’t have to implement anything, the GreeterServiceClient
is ready to be used as is.
We need an ActorSystem
and then the GreeterServiceClient
can be created and used like this:
sourceimport scala.concurrent.duration._
import scala.concurrent.{ExecutionContext, Future}
import scala.util.Failure
import scala.util.Success
import akka.Done
import akka.NotUsed
import akka.actor.typed.ActorSystem
import akka.actor.typed.scaladsl.Behaviors
import akka.grpc.GrpcClientSettings
import akka.stream.scaladsl.Source
object GreeterClient {
def main(args: Array[String]): Unit = {
implicit val sys: ActorSystem[Nothing] = ActorSystem[Nothing](Behaviors.empty[Nothing], "GreeterClient")
implicit val ec: ExecutionContext = sys.executionContext
val client = GreeterServiceClient(GrpcClientSettings.fromConfig("helloworld.GreeterService"))
val names =
if (args.isEmpty) List("Alice", "Bob")
else args.toList
names.foreach(singleRequestReply)
def singleRequestReply(name: String): Unit = {
println(s"Performing request: $name")
val reply = client.sayHello(HelloRequest(name))
reply.onComplete {
case Success(msg) =>
println(msg)
case Failure(e) =>
println(s"Error: $e")
}
}
}
}
Note that clients and servers don’t have to be implemented with Akka gRPC. They can be implemented/used with other libraries or languages and interoperate according to the gRPC specification.
Other types of calls
In this first example we saw a gRPC service call for single request returning a Future
reply. The parameter and return type of the calls may also be streams in 3 different combinations:
- client streaming call -
Source
(stream) of requests from the client that returns aFuture
CompletionStage
with a single response, seeitKeepsTalking
in above example - server streaming call - single request that returns a
Source
(stream) of responses, seeitKeepsReplying
in above example - client and server streaming call -
Source
(stream) of requests from the client that returns aSource
(stream) of responses, seestreamHellos
in above example
As next step, let’s try the bidirectional streaming calls.