Click here to Skip to main content
15,559,568 members
Articles / Programming Languages / Python
Article
Posted 24 Jan 2023

Stats

5.9K views
13 bookmarked

gRPC in Easy Samples for C#, JavaScript and Python

Rate me:
Please Sign up or sign in to vote.
5.00/5 (8 votes)
24 Jan 2023CPOL9 min read
This article provides simple examples of creating both client and server code for Google RPCs
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:

  1. GRPC works on all popular platforms with most of the software languages, including but not limited to
    1. C#
    2. Java
    3. JavaScript/TypeScript
    4. Python
    5. Go
    6. Objective-C
  2. gRPC is very fast and takes small bandwidth - the data is packed by Protobuffer software in the most rational way.
  3. gRPC and its underlying Protobuffer are very simple to learn and very natural to use.
  4. 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:

  1. Protos - contains the service.proto gRPC protobuffer file
  2. SimpleGrpcServer - C# Grpc server
  3. SimpleGrpcClient - C# Grpc client
  4. SimpleNodeJSGrpcClient - Node JS Grpc client
  5. 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:

Image 1

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.

protobuf
// taken from 
// https://grpc-ecosystem.github.io/grpc-gateway/docs/tutorials/simple_hello_world/

syntax = "proto3";

package greet;

service Greeter
{
    // client takes HelloRequest and returns HelloReply
    rpc SayHello (HelloRequest) returns (HelloReply);
}

// HelloRequest has only one string field - name
message HelloRequest
{
    string name = 1;
}

// HelloReply has only one string name - msg
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):

Image 2

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:

Image 3

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:

C#
using Grpc.Core;
using static Greet.Greeter;

// get the channel connecting the client to the server
var channel = new Channel("localhost", 5555, ChannelCredentials.Insecure);

// create the GreeterClient service
var client = new GreeterClient(channel);

// call SetHello RPC on the server asynchronously and wait for the reply.
var reply = 
    await client.SayHelloAsync(new Greet.HelloRequest { Name = "C# Client" });

// print the Msg within the reply.
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:

C#
internal class GreeterImplementation : Greeter.GreeterBase
{
    // provides implementation for the abstract method SayHello(...)
    // from the generated server stub
    public override async Task<HelloReply> SayHello
    (
        HelloRequest request, 
        ServerCallContext context)
    {
        // return HelloReply with Msg consisting of the word Hello
        // and the name passed by the request
        return new HelloReply
        {
            Msg = $"Hello {request.Name}"
        };
    }
}  

Here is the Program.cs code:

C#
using Greet;
using Grpc.Core;
using SimpleGrpcServerTest;

// create GreeterImplementation object providing the 
// RPC SayHello implementation
GreeterImplementation greeterImplementation = new GreeterImplementation();

// bind the server with the greeterImplementation so that SayHello RPC called on 
// the server will be channeled over to greeterImplementation.SayHello
Server server = new Server
{
    Services = { Greeter.BindService(greeterImplementation) }
};

// set the server host, port and security (insecure)
server.Ports.Add(new ServerPort("localhost", 5555, ServerCredentials.Insecure));

// start the server
server.Start();

// wait with shutdown until the user presses a key
Console.ReadLine();

// shutdown the server
server.ShutdownAsync().Wait();    

The most important line is the one that binds the service and GreeterImplementation:

C#
// bind the server with the greeterImplementation so that SayHello RPC called on 
// the server will be channeled over to greeterImplementation.SayHello
Server server = new Server
{
    Services = { Greeter.BindService(greeterImplementation) }
};  

Node JS gRPC Client

To run the client, first make sure that the server is already running.

Then rebuild the SimpleNodeJSGrpcClient client in order to restore the npm packages (you need to do it only once).

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):

JavaScript
module;

// import grpc functionality 
let grpc = require('@grpc/grpc-js');

// import protoLoader functionality
let protoLoader = require('@grpc/proto-loader');

// load the services from service.proto file
const root =
    protoLoader.loadSync
        (
            '../Protos/service.proto', // path to service.proto file
            {
                keepCase: true, // service loading parameters
                longs: String,
                enums: String,
                defaults: true,
                oneofs: true
            });

// get the client package definitions for greet package
// defined within the services.proto file
const greet = grpc.loadPackageDefinition(root).greet;

// connect the client to the server
const client = new greet.Greeter("localhost:5555", grpc.credentials.createInsecure());

// call sayHello RPC passing "Java Script" as the name parameter
client.sayHello({ name: "Java Script" }, function (err, response) {
    // obtain the response and print its msg field
    console.log(response.msg);
});

// prevent the program from exiting right away
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):

Image 4

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):

Python
# import require packages
import grpc
import grpc_tools.protoc

# generate service_pb2 (for proto messages) and 
# service_pb2_grpc (for RPCs) stubs
grpc_tools.protoc.main([
    'grpc_tools.protoc',
    '-I{}'.format("../Protos/."),
    '--python_out=.',
    '--grpc_python_out=.',
    '../Protos/service.proto'
])

# import the generated stubs
import service_pb2;
import service_pb2_grpc;

# create the channel connecting to the server at localhost:5555
channel = grpc.insecure_channel('localhost:5555')

# get the server gRCP stub
greeterStub = service_pb2_grpc.GreeterStub(channel)

# call SayHello RPC on the server passing HelloRequest message
# whose name is set to 'Python'
response = greeterStub.SayHello(service_pb2.HelloRequest(name='Python'))

# print the result
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:

Image 5

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.

protobuf
// gRPC service (RelayService)
service RelayService
{
    // Publish RPC - takes a Message with a msg string field
    rpc Publish (Message) returns (PublishConfirmed) {}

    // Subscribe RPC take SubscribeRequest and returns a stream 
    // of Message objects
    rpc Subscribe(SubscribeRequest) returns (stream Message){}
}

// Relay Message class that 
// contains a single msg field
message Message
{
    string msg = 1;
}

// Empty class used to confirm that Published Message has been received
message PublishConfirmed
{

}

// Empty message that requests a subscription to Relay Messages.
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:

C#
using Grpc.Core;
using Service;
using static Service.RelayService;

// Channel contains information for establishing a connection to the server
Channel channel = new Channel("localhost", 5555, ChannelCredentials.Insecure);

// create the RelayServiceClient from the channel
RelayServiceClient client = new RelayServiceClient(channel);

// call PublishAsync and get the confirmation reply
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:

C#
// channel contains info for connecting to the server
Channel channel = new Channel("localhost", 5555, ChannelCredentials.Insecure);

// create RelayServiceClient
RelayServiceClient client = new RelayServiceClient(channel);

// replies is an async stream
using var replies = client.Subscribe(new Service.SubscribeRequest());

// move to the next message within the reply stream
while(await replies.ResponseStream.MoveNext())
{
    // get the current message within reply stream
    var message = replies.ResponseStream.Current;

    // print the current message
    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:

C#
// bind RelayServiceImplementations to the gRPC server.
Server server = new Server
{
    Services = { RelayService.BindService(new RelayServiceImplementations()) }
};

// set the server to be connected to port 5555 on the localhost without 
// any security
server.Ports.Add(new ServerPort("localhost", 5555, ServerCredentials.Insecure));

// start the server
server.Start();

// prevent the server from exiting.
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:

C#
internal class RelayServiceImplementations : RelayServiceBase
{
    // all client subscriptions
    List<Subscription> _subscriptions = new List<Subscription>();

    // Publish implementation
    public override async Task<PublishConfirmed> Publish
    (
        Message request, 
        ServerCallContext context)
    {
        // add a published message to every subscription
        foreach (Subscription subscription in _subscriptions)
        {
            subscription.AddMessage(request.Msg);
        }

        // return PublishConfirmed reply
        return new PublishConfirmed();
    }

    // Subscribe implementation
    public override async Task Subscribe
    (
        SubscribeRequest request, 
        IServerStreamWriter<Message> responseStream, 
        ServerCallContext context)
    {
        // create subscription object for a client subscription
        Subscription subscription = new Subscription();

        // add subscription to the list of subscriptions
        _subscriptions.Add(subscription);

        // subscription loop
        while (true)
        {
            try
            {
                // take message one by one from subscription 
                string msg = subscription.TakeMessage(context.CancellationToken);

                // create Message reply
                Message message = new Message { Msg = msg };

                // write the message into the output stream. 
                await responseStream.WriteAsync(message);
            }
            catch when(context.CancellationToken.IsCancellationRequested)
            {
                // if subscription is cancelled, break the loop
                break;
            }
        }

        // once the subscription is broken, remove it 
        // from the list of subscriptions
        _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:

C#
// represents a single client subscription
internal class Subscription
{
    private BlockingCollection<string> _messages = 
        new BlockingCollection<string>();

    // add a message to the _messages collection
    public void AddMessage(string message)
    {
        _messages.Add(message);
    }

    // remove the first message from the _messages collection
    // If there are no message in the collection, TakeMessage will wait
    // blocking the thread. 
    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:

JavaScript
// import grpc packages
let grpc = require('@grpc/grpc-js');
let protoLoader = require('@grpc/proto-loader');

// load and parse the service.proto file
const root = protoLoader.loadSync
(
    '../../Protos/service.proto',
    {
        keepCase: true,
        longs: String,
        enums: String,
        defaults: true,
        oneofs: true
    });

// get the service package containing RelayService object
const service = grpc.loadPackageDefinition(root).service;

// create the RelayService client connected to localhost:5555 port
const client = new service.RelayService
               ("localhost:5555", grpc.credentials.createInsecure());

// publish the Message object "Published from JS Client"
// (as long as the Json structure matches the Message object structure it will be
// converted to Message object)
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:

protobuf
// Relay Message class that 
// contains a single msg field
message Message
{
	string msg = 1;
}  

Subscribe Node JS Sample

The important code for this sample is located within app.js file of SubscribeNodeJsSample project:

JavaScript
// import grpc packages
let grpc = require('@grpc/grpc-js');
let protoLoader = require('@grpc/proto-loader');

// load and parse the service.proto file
const root = protoLoader.loadSync
(
    '../../Protos/service.proto',
    {
        keepCase: true,
        longs: String,
        enums: String,
        defaults: true,
        oneofs: true
    });

// get the service package containing RelayService object
const service = grpc.loadPackageDefinition(root).service;

// create the RelayService client connected to localhost:5555 port
const client = new service.RelayService
               ("localhost:5555", grpc.credentials.createInsecure());

// create the client subcription by passing
// an empty Json message (matching empty SubscribeRequest from service.proto)
var call = client.Subscribe({});

// process data stream combing from the server in 
// response to calling Subscribe(...) gRPC
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:

Python
# import python packages
import grpc
import grpc_tools.protoc

# generate the client stubs for service.proto file in python
grpc_tools.protoc.main([
    'grpc_tools.protoc',
    '-I{}'.format("../../Protos/."),
    '--python_out=.',
    '--grpc_python_out=.',
    '../../Protos/service.proto'
])

# import the client stubs (service_pb2 contains messages, 
# service_pb2_grpc contains RPCs)
import service_pb2;
import service_pb2_grpc;

# create the channel
channel = grpc.insecure_channel('localhost:5555')

# create the client stub object for RelayService
stub = service_pb2_grpc.RelayServiceStub(channel);

# create and publish the message
response = stub.Publish(service_pb2.Message(msg='Publish from Python Client'));  

Subscribe Python Sample

Here is the code (from SubscribePythonSample.py file of the same named project):

Python
# import python packages
import asyncio
import grpc
import grpc_tools.protoc

# generate the client stubs for service.proto file in python
grpc_tools.protoc.main([
    'grpc_tools.protoc',
    '-I{}'.format("../../Protos/."),
    '--python_out=.',
    '--grpc_python_out=.',
    '../../Protos/service.proto'
])

# import the client stubs (service_pb2 contains messages, 
# service_pb2_grpc contains RPCs)
import service_pb2;
import service_pb2_grpc;

# define async loop
async def run() -> None:
    #create the channel
    async with grpc.aio.insecure_channel('localhost:5555') as channel:
        # create the client stub object for RelayService
        stub = service_pb2_grpc.RelayServiceStub(channel);

        # call Subscribe gRCP and print the responses asynchronously
        async for response in stub.Subscribe(service_pb2.SubscribeRequest()):
            print(response.msg)

# run the async method calling that subscribes and 
# prints the messages coming from the server
asyncio.run(run())  

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect AWebPros
United States United States
I am a software architect and a developer with great passion for new engineering solutions and finding and applying design patterns.

I am passionate about learning new ways of building software and sharing my knowledge with others.

I worked with many various languages including C#, Java and C++.

I fell in love with WPF (and later Silverlight) at first sight. After Microsoft killed Silverlight, I was distraught until I found Avalonia - a great multiplatform package for building UI on Windows, Linux, Mac as well as within browsers (using WASM) and for mobile platforms.

I have my Ph.D. from RPI.

here is my linkedin profile

Comments and Discussions

 
QuestionI vote 5 stars. Pin
roylenkyu26-Jan-23 6:40
roylenkyu26-Jan-23 6:40 
AnswerRe: I vote 5 stars. Pin
Nick Polyak26-Jan-23 6:59
mvaNick Polyak26-Jan-23 6:59 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.