gRPC bidirectional streaming example
By: Suraj Gharat
Tags: Grpc, Scala, Net Core 3

This post is a second extension to my previous post, where we saw gRPC basics with a simple non-stream based example. In this post, we will see a bidirectional streaming example. In this type of gRPC call, both server and client can send a sequence of messages using read-write streams. The two streams operate independently and so clients and servers can have different processing rates.

Service specification

Below service.proto describes a service call that expects a stream of DemoNumber as an input and returns another stream of DemoNumber as its output. Please note the “stream” modifier before the input and the return data types. The type DemoNumber is just a wrapper around primitive integer type, since gRPC does not support primitive types as its input or output types. Here the example requirement is to build a service that takes a series of numbers as the input, and accumulates it till the sum crosses 100, and whenever it does, it sends a response back to its client and resets the sum.

service DemoService { rpc GetCountsToMakeHundred(stream DemoNumber) returns (stream DemoNumber) {} } message DemoNumber{ int32 value = 1; }

Server implementation

At Scala based server side, now we need to implement the above service by implementing a method that takes a StreamObserver[DemoNumber] the input, and returns an object of type, again, StreamObserver[DemoNumber] since in our case both input and output are having same data type. The input StreamObserver[DemoNumber] enables the server to send a stream of DemoNumber to its calling client, and the output StreamObserver[DemoNumber] allows the server to handle the stream of DemoNumber that the calling client is sending to it. The StreamObserver[T] is a gRPC generated stub to access streams. We can use it to read and write data from and to a stream.

In order to access the client stream of request data, a server needs to implement above stub interface, StreamObserver[DemoNumber], which provides methods to handle events like onNext, onError and onCompleted which get called when the calling streaming-client sends any request data, faces error in sending request data and when it is done with sending request data respectively.

private def getCountsToMakeHundred(responseObserver: StreamObserver[DemoNumber]): StreamObserver[DemoNumber] = new StreamObserver[DemoNumber] { import DemoServer._ var sum = 0 var count = 0

override def onNext(value: DemoNumber): Unit = { // accumulate sum and number of inputs sum = sum + value.value count = count + 1 logger.info(s"[Bi streaming gRPC call] Received ${value.value}. Current sum is $sum and count is $count") // if the accumulated sum is gte 100 then send reply to client if(sum >= 100){ responseObserver.onNext(DemoNumber(count)) // reset count = 0 sum = 0 } }

override def onError(t: Throwable): Unit = { logger.severe(s"[Bi streaming gRPC call] received error from client and so completing:"+t) // stopping entire server which would not be the case in reality stop() }

override def onCompleted(): Unit = { DemoServer.logger.info(s"[Bi streaming gRPC call] client input is complete and so also completing server output") responseObserver.onCompleted() } }

Client implementation

In .NET Core client app, we get to access the stream through an object on which we can call methods like MoveNext to check if there is any element to read from the stream, and a property to read the current element, just like how we retrieve elements out of an iterator. Similarly, we also get access to another stream to which we can write client input to be sent to the server as a stream element.

In my example, I first create an async task to read server stream data, using call.ResponseStream.MoveNext() and call.ResponseStream.Current as can be seen in the below snippet. Then, using a for loop, I am sending a sequence of inputs to the server as stream elements, by invoking call.RequestStream.WriteAsync(),which is followed by sending input completion signal to server by invoking call.RequestStream.CompleteAsync(). The client program can keep on sending request data, while at the same time it keeps on receiving response data from its server, for infinite time duration.