What if I told you there is a world beyond JSON REST apis ? What if I told you, that some cool folks from Google invited crazy efficient serialization protocol and a way of doing remote cals, so that it’s much lighter on CPU, being (easily) 5 times more compact than JSON and is truely cross-platform ? Ah, forgot to mention it integrates nicely with dotnet ecosystem.
gRPC facilites the features of HTTP2 and compactness of ProtoBuff, Google’s binary serialization format. It was desgined with efficiency in mind, hence it’s binary nature. However, they took cross platform compatibility really seriously, providing libraries for over 20 languages, including c#.
The basic idea in ProtoBuff is that you define your data structures and services in so called IDL (Interface Definition Language) which you can then use to generate client/server code in variety of languages. Below you can see example proto file (written in IDL), which acts as a service schema:
syntax = "proto3";
package HelloWorld;
message HelloRequest {
string name = 1;
}
message HelloReply {
string greeting = 1;
}
service GreetingService {
rpc SayHello (HelloRequest) returns (HelloReply) {}
rpc StreamOfGreetings (HelloRequest) returns (stream HelloReply) {}
}
This file describes available services and procedures you can invoke on them. Later on, we’ll use this file to generate C# base classes and DTOs, so that we can implement the server behaviour. For now, let’s see what we have in there:
-
First line declares we’re about to use
proto3
syntax instead of it’s older brotherproto2
. If you are not working with legacy systems, you’d generally like to useproto3
syntax as it contains some new fancy features -
Second line declares
namespace
. This thing maps directly to C# namespace in which the generated code is going to reside -
Then, we declare our messages, which are going to be mapped to C# classes with serializing / deserializing behaviour in them. These are our DTOs
Notice the tag id which comes after property name and think twice before changing them in an already existing schema, as this is pretty important. It uniquely identifies the property in a binary stream, so if you change this value you effectievely break serialization contract! -
Last but not the least, there is our service declaration. It maps to a C# base class with procedures (in our case
SayHello
andStreamOfGreetings
) you can override to implement service behaviour. Also, client code will be generated so that you can actually call the service.
Noticestream
keyword there, inStreamOfGreetings
procedure. This allows you to return stream of values instead of a single one. You can think of this like anObservable
, or a “reactive stream”
Cool, but.. how do I get it to work with C# then?
The most important thing in gRPC and ProtoBuf is to declare your schema in IDL first (see snippet above). It’s used by protoc
compiler to generate code in language of your choice.
Thankfully, there is great integration of this tool with msbuild, through Grpc.Tools
nuget package. It contains msbuild targets, which handle code generation on build what effectively means, that each team you build the project in VS, all the proxy classes are automatically generated for you and get supplied to the compiler. All you need to do is to place the following in your csproj:
<ItemGroup>
<Protobuf Include="**/*.proto" />
</ItemGroup>
This “tells” msbuild to use proto compiler in order to generate appropiate classes.
Also, you’d like to add Grpc
package which contains neccessary bindings to gRPC native core.
Implementing server
To implement the server side, you need to create a class inheriting from respective generated base class, which in our case is GreetingService.GreetingServiceBase
.
Notice the GreetingService
befor the dot - that’s because GreetingServiceBase
is a nested class within GreetingService
. This parent class also contains another interesting class, GreetingServiceClient
, which we’ll talk about soon.
Now, if you peek definition of GreetingServiceBase
you can notice there are virtual methods for each one rpc procedure we defined in our proto file. Let’s override them to implement some behaviour:
class GrpcServer : GreetingService.GreetingServiceBase
{
public override Task<HelloReply> SayHello(
HelloRequest request,
ServerCallContext context)
{
return Task.FromResult(new HelloReply
{
Greeting = $"Hello, {request.Name}"
});
}
public override async Task StreamOfGreetings(
HelloRequest request,
IServerStreamWriter<HelloReply> responseStream,
ServerCallContext context)
{
for (var i = 0; i < 10; i++)
{
await responseStream.WriteAsync(new HelloReply()
{
Greeting = $"[streaming {i}] Hello, {request.Name}"
});
await Task.Delay(1000);
}
}
}
Notice IServerStreamWriter<T>
interface there. When defining a streaming procedure (using IDLs stream
keyword) , you can push values to the values stream using it’s WriteAsync
method, in an asynchronous way.
Now, all that’s left is to bind this implementation to the actual server and start it:
class Program
{
const int Port = 50051;
static void Main(string[] args)
{
var server = new Server
{
Services = { GreetingService.BindService(new GrpcServer()) },
Ports = {
new ServerPort("localhost", Port, ServerCredentials.Insecure)
}
};
server.Start();
Console.WriteLine("Server listening on port " + Port);
Console.WriteLine("Press any key to stop the server...");
Console.ReadKey();
server.ShutdownAsync().Wait();
}
}
That’s all you need to get the minimal gRPC server up and running!
Now, how do I call it?
Having our classes generated, all we need to call rpc procedures is to construct GreetingServiceClient
supplying Channel
, which specifies the endpoint and credentials. Also, for consuming stream returned by StreamOfGreetings
method we’ll use IAsyncStreamReader<T>
interface and it’s MoveNext
method and Current
property:
namespace DummyGrpcClient
{
class Program
{
static void Main(string[] args)
{
var channel = new Channel(
"127.0.0.1:50051",
ChannelCredentials.Insecure
);
var client = new GreetingService.GreetingServiceClient(channel);
Console.WriteLine(
client.SayHello(
new HelloRequest
{
Name = "john"
})
);
var streamingRequest = new HelloRequest() {Name = "Streaming John"};
var ct = CancellationToken.None;
using(var response = client.StreamOfGreetings(streamingRequest))
{
while (response.ResponseStream.MoveNext(ct).Result)
{
Console.WriteLine(
response.ResponseStream.Current.Greeting
);
}
}
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
channel.ShutdownAsync().Wait();
}
}
}
Final notes
I hope I insipired you to dig a litle into gRPC, as I think it’s well worth to give it a look. It’s also worth checking out grpc.io to find out how many platforms are supported. In the next post of this “series” we’re going to actually measure and compare performance of gRPC vs AspDotNet Core JSON api, so stay tuned!
Comments