gRPC basics with example
By: Suraj Gharat
Tags: Grpc, Scala, Net Core 3

What is gRPC ?

gRPC is an open source, language agnostic RPC framework, which can be used for service to service communication. It is an alternative to ReST. It uses Protocol Buffers as the interface definition language.

Advantages

Contract-first approach helps negotiate API contracts across teams very clearly. It is very suitable for polyglot systems where services are implemented in different technologies or languages. It supports both unidirectional and bidirectional streaming calls. It reduces network usage due to Protobuf binary serialization It uses HTTP/2 for transport, which is lighter and performant.

Drawbacks

Yet browsers do not offer complete support needed for a gRPC client to make HTTP/2 requests. Need additional tools to view the actual gRPC payload due to Protobufs, requests and responses are not human readable. We can not test or invoke a gRPC service using tools like cURL and Postman

Example

We have a gRPC server implemented in Scala, and a client implemented in .NET Core. For simplicity, the server here adds input numbers and returns the sum as a result. In my next post, I’ll try to cover streaming gRPC calls.

Service specification

gRPC service specification is described using proto syntax. It tells you the name of the service and its parameters along with their types. Below is the proto file I used for this example, and its Github link.

syntax = "proto3"; package com.surajgharat.practice.grpc.service; service SumService { rpc Sum(SumInput) returns (SumOutput) {} } message SumInput { int32 n1 = 1; int32 n2 = 2; } message SumOutput { int32 result = 1; }

At the Scala server side, as we compile, the proto compiler generates a SumService scala object in the given package, and that object contains a nested trait that we need to implement as a part of service implementation. It also creates SumInput and SumOutput Scala case classes in the same namespace.

P.S. : We can not use plain primitive types as input and output parameters in gRPC.

Server implementation

I am using ScalaPB, a protocol buffer compiler for Scala, which generates traits, types and stubs needed for implementing gRPC server as per the service specification, and also client side channels and stubs, as we compile the project using sbt compile

Install ScalaPB by adding it as a plugin in the project/plugins.sbt file of your Scala project as below.

addSbtPlugin("com.thesamet" % "sbt-protoc" % "1.0.0-RC2") libraryDependencies += "com.thesamet.scalapb" %% "compilerplugin" % "0.10.8"

Add below two dependencies to your build.sbt

libraryDependencies ++= Seq( "io.grpc" % "grpc-netty" % scalapb.compiler.Version.grpcJavaVersion, "com.thesamet.scalapb" %% "scalapb-runtime-grpc" % scalapb.compiler.Version.scalapbVersion )

Below statement in the build.sbt would enable generation of gRPC code on sbt compile

PB.targets in Compile := Seq( scalapb.gen() -> (sourceManaged in Compile).value / "scalapb" )

ScalaPB is very configurable and so it offers to configure proto file location as below. In my case I saved my sumservice.proto in the main/proto folder of my Scala server project.

PB.protoSources in Compile := Seq(sourceDirectory.value / "main/proto")

Below snippet shows the Scala service implementation, wherein SumServiceGrpc.SumService is the trait generated by ScalaPB proto compiler using our service specification. This is a non-blocking implementation which makes use of Scala Futures and simply adds two numbers to create the final result. See complete server code here.

class SumServiceImpl extends SumServiceGrpc.SumService { // service implementation override def sum(request: SumInput): Future[SumOutput] = Future.successful(SumOutput(request.n1 + request.n2)) }

Below Scala statement builds and starts the above server

ServerBuilder .forPort(50051) .addService(SumServiceGrpc.bindService(new SumServiceImpl, executionContext)) .build .start

Client implementation

For .NET Core client implementation, we need to add Grpc.Tools, Google.Protobuf and Grpc.Net.Client nugets to our project which would generate C# types for service, client and message as we build the project.

Also add below tag in the project file to indicate location of grpc service proto file. Moreover, using the “GrpcServices” attribute, we can indicate whether to generate gRPC types only for client, or server or both or none.

<ItemGroup> <Protobuf Include="Protos\sumservice.proto" GrpcServices="Client" /> </ItemGroup>

Below C# snippet shows how to create a channel and gRPC client. A channel represents a long-lived connection to a gRPC service. When a channel is created, it is configured with options related to calling a service. Please note the SumService and SumServiceClient are the generated type/stub by Grpc.Tools as per our given service proto file.

var channel = GrpcChannel.ForAddress("https://localhost:5001"); var client = new SumService.SumServiceClient(channel);

There is a provision to configure the channel using GrpcChannelOptions, which allows us to configure it with HttpClient or MessageHandler of our own choice. gRPC uses HTTP/2 for transport, and by default HttpClient in .NET Core uses HTTP/1.x, and that is why I configured my channel to use a custom message handler which sets request version to HTTP/2.

Finally the below statement makes the gRPC service call and collects the result asynchronously.

SumOutput sumOutput = await client.SumAsync(new SumInput { N1 = n1, N2 = n2 });

See complete client code here.