This article describes gRPC - Google RPC as a multi-platform, multi-language method for building client/server communications mechanism. The servers are implemented in C#, while the clients are implemented in C#, JavaScript and Python.
Introduction
Benefits of gRPC
Microsoft all but discontinued WCF by not including its code into .NET CORE. The best and most popular remaining solution for remote communications between different processes possibly running on different machines is gRPC or Google Remote Procedure Calls.
gRPC has the following advantages:
- GRPC works on all popular platforms with most of the software languages, including but not limited to
- C#
- Java
- JavaScript/TypeScript
- Python
- Go
- Objective-C
- gRPC is very fast and takes small bandwidth - the data is packed by Protobuffer software in the most rational way.
- gRPC and its underlying Protobuffer are very simple to learn and very natural to use.
- On top of the usual request-reply paradigm, gRPC also uses publish/subscribe paradigm where a subscribed client can receive an asynchronous stream of published messages. Such stream can be easily changed to
IObservable
of Rx and correspondingly all allowed Rx LINQ transformations can be applied.
Proto Files
gRPC and Protobuffer are using simple .proto files to define the services and the messages. Alternatively, in C# and other languages, one can use Code first method of defining gRPC services and messages from attributed language code.
The Code first method is better to be used when one wants to stick to the same language across all gRPC clients and the server. I am more interested in a case where clients written in different languages can access the same server written in C#. In particular, I am interested in C#, Python and JavaScript/TypeScript clients. Because of that, all the samples in this article will be using .proto files to define the messages and services.
Outline of the Article
The article describes two samples - one demonstrating reply/request and the other publish/subscribe paradigms. For each sample, the server is written in C# and the clients are written in three different languages: C#, NodeJS (JavaScript) and in Python.
Code Location
All the sample code is located within NP.Samples under GRPC folder. All folder references below will be with respect to GRPC folder of the repository.
Simple (Reply/Request) gRPC Examples
SimpleGrpcServer.sln solution is located under SimpleRequestReplySample\SimpleGrpcServer folder. The solution consists of five projects:
Protos
- contains the service.proto gRPC protobuffer file SimpleGrpcServer
- C# Grpc server SimpleGrpcClient
- C# Grpc client SimpleNodeJSGrpcClient
- Node JS Grpc client SimplePythonGrpcClient
- Python Grpc client
Protos Project
Protos.csproj project is a .NET project compiling its service.proto file into .NET code for the C# server and client projects. The non-C# projects simply use its service.proto file to generate the client stubs in the corresponding language.
Protos.csproj references three nuget packages - Google.Protobuf
, Grpc
and Grpc.Tools
:
The example I bring up is a very popular example of Greeter Grpc service that can be found, e.g., in Overview for gRPC on .NET and Grpc Quick Start.
The client sends a name, e.g., "Joe Doe
" to the server and the server replies with "Hello Joe Doe
" message.
syntax = "proto3";
package greet;
service Greeter
{
rpc SayHello (HelloRequest) returns (HelloReply);
}
message HelloRequest
{
string name = 1;
}
message HelloReply
{
string msg = 1;
}
The proto file essentially contains messages (request and reply) and the service Greeter
. Note that the messages are very similar to Java or C#, only the names of the fields are followed by numbers - e.g., string name = 1;
. The numbers should be unique for each field within the same message and will be used to store and restore the message fields in the order of the numbers.
In our case, each message has only one field and because of that, any number will do (we had number 1 to signify that this is the first (and only) field within the message).
The service Greeter
contains an rpc
(Remote Procedure Call) called SayHello
that takes message HelloRequest
as its input and returns HelloReply
message as its output. Note that while the interface of the RPC is defined by the proto file, the implementation is still up to the server.
In order for Visual Studio to generate .NET code automatically creating both client and server stubs, the BuildAction
of the service.proto file should be set to "Protobuf Compiler
" (once you reference Grpc.Tools
, this option will appear among build actions):
As we mentioned above, the stubs will automatically be generated only for the C# client and server. The other languages will use their own methods for generating the stubs straight from service.proto file.
Running C# Server and C# Client
To run the server, right-click on SimpleGrpcServer project within Solution Explorer, and choose Debug->Start Without Debugging:
An empty command prompt will open (since server is a console application).
Now run the C# client from the same solution (by right-clicking on SimpleGrpcClient within the Solution Explorer and choosing Debug -> Run Without Debugging).
The client will display "Hello C# Client
" string
returned from the server.
SimpleGrpcClient Code
All C# Client
code is located within Program.cs file of SimpleGrpcClient
project:
using Grpc.Core;
using static Greet.Greeter;
var channel = new Channel("localhost", 5555, ChannelCredentials.Insecure);
var client = new GreeterClient(channel);
var reply =
await client.SayHelloAsync(new Greet.HelloRequest { Name = "C# Client" });
Console.WriteLine(reply.Msg);
SimpleGrpcServer Code
The server code is contained in two files - GreeterImplementation.cs and Program.cs.
GreeterImplementation
- is a class derived from the abstract GreeterBase
generated server stub class. It provides the implementation for the method SayHello(...)
(which is abstract
within the superclass GreeterBase
. The implementation here prepends with "Hello "
string
whatever is contained within request.Name
:
internal class GreeterImplementation : Greeter.GreeterBase
{
public override async Task<HelloReply> SayHello
(
HelloRequest request,
ServerCallContext context)
{
return new HelloReply
{
Msg = $"Hello {request.Name}"
};
}
}
Here is the Program.cs code:
using Greet;
using Grpc.Core;
using SimpleGrpcServerTest;
GreeterImplementation greeterImplementation = new GreeterImplementation();
Server server = new Server
{
Services = { Greeter.BindService(greeterImplementation) }
};
server.Ports.Add(new ServerPort("localhost", 5555, ServerCredentials.Insecure));
server.Start();
Console.ReadLine();
server.ShutdownAsync().Wait();
The most important line is the one that binds the service and GreeterImplementation
:
Server server = new Server
{
Services = { Greeter.BindService(greeterImplementation) }
};
Node JS gRPC Client
To install the needed packages right click on npm under SimpleNodeJSGrpcClient
project within the Solution Explorer and choose "Install npm packages" menu item.
Finally, right-click SimpleNodeJSGrpcClient
project and choose Debug->Start Without Debugging.
The client console should print "Hello Java Script
" string
returned from the server.
Here is the Node JS client's code (with comments):
module;
let grpc = require('@grpc/grpc-js');
let protoLoader = require('@grpc/proto-loader');
const root =
protoLoader.loadSync
(
'../Protos/service.proto',
{
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
});
const greet = grpc.loadPackageDefinition(root).greet;
const client = new greet.Greeter("localhost:5555", grpc.credentials.createInsecure());
client.sayHello({ name: "Java Script" }, function (err, response) {
console.log(response.msg);
});
var done = (function wait() { if (!done) setTimeout(wait, 1000) })();
Python gRPC Client
To prepare Python gRCP client solution, first right click on env under Python Environment under SimplePythonGrpcClient
project and choose "Install from requirements.txt" (needs to be done only once):
This will restore all required Python packages.
Then, you can run it in the same fashion as any other project, just make sure that the server is already running.
The client Python project should result in "Hello Python
" string
printed onto console window.
Here is the Python code (explained in Python comments):
import grpc
import grpc_tools.protoc
grpc_tools.protoc.main([
'grpc_tools.protoc',
'-I{}'.format("../Protos/."),
'--python_out=.',
'--grpc_python_out=.',
'../Protos/service.proto'
])
import service_pb2;
import service_pb2_grpc;
channel = grpc.insecure_channel('localhost:5555')
greeterStub = service_pb2_grpc.GreeterStub(channel)
response = greeterStub.SayHello(service_pb2.HelloRequest(name='Python'))
print(response.msg)
Simple Relay gRPC Examples
Our next sample demos publish/subscribe gRPC architecture. The simple relay server passes the messages published by clients to every client subscribed to it.
The sample's solution StreamingRelayServer.sln is located under StreamingSample/StreamingRelayServer folder.
Start the solution, and you'll see that it consists of the server project - StreamingRelayServer
, the Protos
project containing the protobuf service.proto file and three folders: CSHARP, NodeJS and Python. Each of these folders will contain two clients - publishing client and subscribing client:
Relay Sample Protos
Same as in the previous sample, the service.proto file is only compiled to .NET for the .C# projects - the Python and NodeJS clients use their own mechanism for parsing the file.
service RelayService
{
rpc Publish (Message) returns (PublishConfirmed) {}
rpc Subscribe(SubscribeRequest) returns (stream Message){}
}
message Message
{
string msg = 1;
}
message PublishConfirmed
{
}
message SubscribeRequest
{
}
Note that the rpc Subscribe
returns a stream of Messages
(not a single Message
).
Running the Server and Clients
To start the server, right-click on StreamingRelayServer
project and choose Debug->Start Without Debugging. The clients should be started in exactly the same fashion. In order to observe that something is happenning, you need to start subscribing client(s) first and only then, start publishing client(s).
For example, start the server and then C# CSHARP/SubscribeSample. Then run CSHARP/PublishSample. The subscribing client will print "Published from C# Client
" on the console window.
Remember that before starting Node JS projects for the first time, they'll have to be built (in order to download the JavaScript packages). Also before starting Python projects for the first time, you need to right click on their Python Environments->env and select "Install from requirements.txt" to download and install the Python packages.
C# Publish Client
Publish Client code is contained within Program.cs file of PublishSample
project. Here is the documented code for C# Publish client:
using Grpc.Core;
using Service;
using static Service.RelayService;
Channel channel = new Channel("localhost", 5555, ChannelCredentials.Insecure);
RelayServiceClient client = new RelayServiceClient(channel);
PublishConfirmed confirmation =
await client.PublishAsync(new Message { Msg = "Published from C# Client" });
C# Subscribe Client
The subscribing client code is located within Program.cs file of SubscribeSample
project. It gets the replies
stream from the server and prints messages from that stream:
Channel channel = new Channel("localhost", 5555, ChannelCredentials.Insecure);
RelayServiceClient client = new RelayServiceClient(channel);
using var replies = client.Subscribe(new Service.SubscribeRequest());
while(await replies.ResponseStream.MoveNext())
{
var message = replies.ResponseStream.Current;
Console.WriteLine(message.Msg);
}
The replies
stream potentially can be infinite and will end only when the client or the server terminate the connection. If no new replies are coming from the server, the client waits on await replies.ResponseStream.MoveNext()
.
Server Code
Once we figured out what the server does (by demonstrating its clients), let us take a look at the server's code within StreamingRelayServer
project.
Here is the Program.cs code that starts the server binding it to RelayServiceImplementations
gRPC implementation of RelayServer
and connecting it to the port 5555 on the localhost:
Server server = new Server
{
Services = { RelayService.BindService(new RelayServiceImplementations()) }
};
server.Ports.Add(new ServerPort("localhost", 5555, ServerCredentials.Insecure));
server.Start();
Console.ReadLine();
RelayServiceImplementations
class contains the most interesting code implementing the abstract
methods Publish(...)
and Subscribe(...)
of the gRPC stub generated from RelayService
defined within service.proto file:
internal class RelayServiceImplementations : RelayServiceBase
{
List<Subscription> _subscriptions = new List<Subscription>();
public override async Task<PublishConfirmed> Publish
(
Message request,
ServerCallContext context)
{
foreach (Subscription subscription in _subscriptions)
{
subscription.AddMessage(request.Msg);
}
return new PublishConfirmed();
}
public override async Task Subscribe
(
SubscribeRequest request,
IServerStreamWriter<Message> responseStream,
ServerCallContext context)
{
Subscription subscription = new Subscription();
_subscriptions.Add(subscription);
while (true)
{
try
{
string msg = subscription.TakeMessage(context.CancellationToken);
Message message = new Message { Msg = msg };
await responseStream.WriteAsync(message);
}
catch when(context.CancellationToken.IsCancellationRequested)
{
break;
}
}
_subscriptions.Remove(subscription);
}
}
Publish(...)
method goes over every subscription with _subscriptions
list and adds the newly published message to each of them.
Subscribe(...)
method creates a single subscription and checks it for new messages (inserted by Publish(...)
method). If it finds such message, it removes it and pushes it into the response stream. If it cannot find such message, it waits.
Once the Subscribe
connection is broken, the subscription is removed.
Here is the code for a single subscription
object:
internal class Subscription
{
private BlockingCollection<string> _messages =
new BlockingCollection<string>();
public void AddMessage(string message)
{
_messages.Add(message);
}
public string TakeMessage(CancellationToken cancellationToken)
{
return _messages.Take(cancellationToken);
}
}
BlockingCollection
will block the subscription thread until there are messages in it. Since every subscription (or any client operation) runs in its own thread, the other subscriptions will not be affected.
Publish Node JS Sample
Project PublishNodeJsSample
- contains the relevant code within its app.js file:
let grpc = require('@grpc/grpc-js');
let protoLoader = require('@grpc/proto-loader');
const root = protoLoader.loadSync
(
'../../Protos/service.proto',
{
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
});
const service = grpc.loadPackageDefinition(root).service;
const client = new service.RelayService
("localhost:5555", grpc.credentials.createInsecure());
client.Publish({ msg: "Published from JS Client" }, function (err, response) {
});
The interesting part is client.Publish(...)
code. Note that we are creating a Json object { msg: "Published from JS Client" }
as an input to the Publish(Message msg, ...)
method. Since its Json matches the structure of the Message
object defined within service.proto file, such object will automatically be converted to Message
object on the server.
Here is the reminder of how service.proto Message
looks:
message Message
{
string msg = 1;
}
Subscribe Node JS Sample
The important code for this sample is located within app.js file of SubscribeNodeJsSample
project:
let grpc = require('@grpc/grpc-js');
let protoLoader = require('@grpc/proto-loader');
const root = protoLoader.loadSync
(
'../../Protos/service.proto',
{
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
});
const service = grpc.loadPackageDefinition(root).service;
const client = new service.RelayService
("localhost:5555", grpc.credentials.createInsecure());
var call = client.Subscribe({});
call.on('data', function (response) {
console.log(response.msg);
});
Note that calling Subscribe(...)
gRPC will return a JS delegate that will be called every time a response message arrives from the server.
Publish Python Sample
The code for this sample is located within PublishPythonSample.py file of PublishPythonSample
project:
import grpc
import grpc_tools.protoc
grpc_tools.protoc.main([
'grpc_tools.protoc',
'-I{}'.format("../../Protos/."),
'--python_out=.',
'--grpc_python_out=.',
'../../Protos/service.proto'
])
import service_pb2;
import service_pb2_grpc;
channel = grpc.insecure_channel('localhost:5555')
stub = service_pb2_grpc.RelayServiceStub(channel);
response = stub.Publish(service_pb2.Message(msg='Publish from Python Client'));
Important Note: we are using the following command to create the channel:
channel = grpc.insecure_channel('localhost:5555')
This means that the created channel and the stub
obtained from it will allow only blocking calls to the stub
- stub.Publish(...)
call that we use a little later is a blocking call that waits returning value until the round trip to the server is complete.
If one wants to use async (non-blocking but awaitable) calls, one should create a channel by using:
channel = grpc.aio.insecure_channel('localhost:5555')
Note the difference - .aio.
part of the path to insecure_channel(...)
method is missing in our sample above. But we shall be using this call to async version of insecure_channel(...)
method below where we give an example of a long living subscribing connection.
This looks like a minor detail, but it took me some time to figure it out so hopefully this Note will prevent others from running into the same mistake.
Subscribe Python Sample
Here is the code (from SubscribePythonSample.py file of the same named project):
import asyncio
import grpc
import grpc_tools.protoc
grpc_tools.protoc.main([
'grpc_tools.protoc',
'-I{}'.format("../../Protos/."),
'--python_out=.',
'--grpc_python_out=.',
'../../Protos/service.proto'
])
import service_pb2;
import service_pb2_grpc;
async def run() -> None:
async with grpc.aio.insecure_channel('localhost:5555') as channel:
stub = service_pb2_grpc.RelayServiceStub(channel);
async for response in stub.Subscribe(service_pb2.SubscribeRequest()):
print(response.msg)
asyncio.run(run())
And of course, note that the to create the channel allowing async calls we are using the insecure_channel(...)
version of the method within grpc.aio.
path:
async with grpc.aio.insecure_channel('localhost:5555') as channel:
Conclusion
After Microsoft all but deprecated WPC, the gRPC - google RPC is the best framework for creating various server/client communications. On top of usual request/reply paradigm, it also facilitates implementation of publish/subscribe paradigm with output and input streams.
In this article, I demonstrate using gRPC servers (implemented in C# language) with clients written in different languages - C#, JavaScript and Python. I provide samples for both request/reply and publish/subscribe paradigms. The only missed paradigms are when the client sends streams as inputs to the server, but they are less common and I might add a section covering them in the future.
History
- 24th January, 2023: Initial version