No-nonsense gRPC guide for the C# developers, Bonus: Interoperability


source code

Python interoperability

One of the greatest gRPC features is its programming language-neutral nature. You can write gRPC services in C# and access them from other languages. The list includes pretty much everything used in software industry. Conversely, one can connect gRPC clients written in C# to service written in Java, Go, Ruby, you name it.

We are going to demonstrate these capabilities using Python. Python is one of the programming languages I really enjoy working with.

If you want to follow along, have Python installed. I am using version 3.9.

Setup

Make the python directory on the same level as Shared, Client and Service

mkdir python
cd python

we are going to do all subsequent work in this article in the python directory unless specified otherwise.

Next we will create a virtual environment, so all python packages we install will be confined to that virtual environment and not interfere with the system-installed packages.

python -m venv .venv

Next, activate the virtual environment On Mac and Linux:

source ./.venv/bin/activate

On Windows:

./.venv/Scripts/activate

Now let install necessary python grpc packages:

pip install grpcio
pip install grpcio-tools

Generate gRPC plumbing code for the Python project

With C# we had this nice nuget package which took care of generating gRPC plumbing code out of the contracts.proto. In Python, we’ll do it manually which still is pretty trivial

python -m grpc_tools.protoc -I../Shared --python_out=. --grpc_python_out=. contracts.proto

command line argument specifies where to find the proto file (../Shared), where to drop the generated files (. that is, current directory) and, finally the name of the actual proto file.

After running this command you should notice contracts_pb2.py and contracts_pb_grpc.py files. These are the generated gRPC plumbing code, analogous what was generated into Shared/obj in our C# project. So the workflow is pretty much the same as in C# project.

Client

We’ll start with the Python client first. The functionality of the Client and, subsequently, Service is going to mimic pretty much perfectly their C# analogs.

touch client.py

on top of this file, add the necessary imports. We’ll need os and random from python’s standard library, grpc package and our generated modules

import random
import sys

import grpc

import contracts_pb2
import contracts_pb2_grpc

As in the C# project, the first argument is going to be the service host to connect to, and the second is going to be the service port. The following takes care of that:

# ...
# new
host = sys.argv[1]
port = int(sys.argv[2])

Next, we need to establish the connection to the service. As you recall we wanted to provide the Certificate Authority’s certificate to the connection so the client can verify service’s certificate upon connection. We are going to use the same certificates as in our C# exercise, so verify you still have them in the cert directory. The following sets up the connection as described:

# ...
with open("../cert/ca.pem", "rb") as f:
    ca_cert = f.read()
creds = grpc.ssl_channel_credentials(root_certificates=ca_cert)
channel = grpc.secure_channel(f"{host}:{port}", creds)

Finally, create the client

# ...
client = contracts_pb2_grpc.SvcStub(channel)

We will want to demonstrate both basic RPC (calculator) and streaming RPC(time series) functionality. so let’s add two placeholders for now, at the top of the client.py, just below the imports

#...
def do_calculator(client):
    pass

def do_time_series(client):
    pass

At the bottom of the file, add our silly logic to dispatch either to do_calculator or to do_time_series, depending of how many arguments were provided.

# ...
if len(sys.argv) > 3:
        do_calculator(client)
    else:
        do_time_series(client)

Let’s implement the calculator functionality first as it is a little bit simpler

def do_calculator(client):
    x = int(sys.argv[3])
    op = sys.argv[4]
    y = int(sys.argv[5])
    request = contracts_pb2.CalculateRequest(x=x, y=y, op=op)
    reply = client.Calculate(request)
    print(f"The result is {reply.result}")

Ok, let’s give it a try:

in the separate terminal window navigate to the root project’s directory and run the service

dotnet run -p Service 9000

Switch back to the python-specific terminal and invoke simple calculator request:

python client.py localhost 9000 17 + 25

Wow, our C# service calculates the requests coming from the python client!

Let’s see if we can do the time series exercise as well. As you remember, the client is supposed push the stream of temperature readings to the service, ad get the stream of medians back. We will use the same random walk strategy to generate the temperature readings. Add this logic just above the do_time_series(client)

def generate_messages():

    ts = 1
    temp = 10
    while True:
        msg = contracts_pb2.Temperature(timestamp=ts, value=temp)
        ts += 1
        temp += random.random() - 0.5
        yield msg

With this in place, replace the do_time_series with:

def do_time_series(client):
    for msg in client.Median(generate_messages()):
        print(f"{msg.timestamp}: {msg.value}")

Make sure that the C# service is running and invoking the client triggering the time series logic:

python client.py localhost 9000

You’ll be seeing a stream of medians coming. Ctrl-C when you get bored.

The complete python client, for the reference

import random
import sys

import grpc

import contracts_pb2
import contracts_pb2_grpc


def do_calculator(client):
    x = int(sys.argv[3])
    op = sys.argv[4]
    y = int(sys.argv[5])
    request = contracts_pb2.CalculateRequest(x=x, y=y, op=op)
    reply = client.Calculate(request)
    print(f"The result is {reply.result}")


def generate_messages():
    ts = 1
    temp = 10
    while True:
        msg = contracts_pb2.Temperature(timestamp=ts, value=temp)
        ts += 1
        temp += random.random() - 0.5
        yield msg


def do_time_series(client):
    for msg in client.Median(generate_messages()):
        print(f"{msg.timestamp}: {msg.value}")


host = sys.argv[1]
port = int(sys.argv[2])

with open("../cert/ca.pem", "rb") as f:
    ca_cert = f.read()
creds = grpc.ssl_channel_credentials(root_certificates=ca_cert)
channel = grpc.secure_channel(f"{host}:{port}", creds)
client = contracts_pb2_grpc.SvcStub(channel)
if len(sys.argv) > 3:
    do_calculator(client)
else:
    do_time_series(client)

As you may observe, conceptually Python client is not that different from the C# client, modulo idioms specific to each language. The same can actually be said by the service implementation as well.

Service

We got the Python client working, let’s build the Python service.

touch service.py

Add the import se will need

from concurrent import futures
import statistics
import sys

import grpc

import contracts_pb2
import contracts_pb2_grpc

The service implementation should be pretty straightforward by now:

class Service(contracts_pb2_grpc.SvcServicer):
    def Calculate(self, request, context):
        result = -1
        if request.op == "+":
            result = request.x + request.y
        elif request.op == "-":
            result = request.x - request.y
        elif request.op == "*":
            result = request.x * request.y
        elif request.op == "/":
            if request.y != 0:
                result = request.x // request.y
        return contracts_pb2.CalculateReply(result=result)

    def Median(self, request_iterator, context):  # noqa
        vals = []
        for temp in request_iterator:
            vals.append(temp.value)
            med = 0
            if len(vals) == 10:
                med = statistics.median(vals)
                vals = []
                yield contracts_pb2.Temperature(timestamp=temp.timestamp, value=med)

The service will need to be provided with the certificate and private key

with open("../cert/service-key.pem", "rb") as f:
    key = f.read()

with open("../cert/service.pem", "rb") as f:
    cert = f.read()

creds = grpc.ssl_server_credentials(
    [
        (
            key,
            cert,
        ),
    ]
)

Once that is done, start it running:

port = int(sys.argv[1])
svc = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
contracts_pb2_grpc.add_SvcServicer_to_server(Service(), svc)
svc.add_secure_port(f"[::]:{port}", creds)
svc.start()
print(f"service listening on port {port}...")
svc.wait_for_termination()

Give it a try

python service.py 9010

Notice the different port so the C# service and Python service don’t conflict one with another. In a separate terminal, go to the root project directory and run the C# client, the calculator:

dotnet run -p Client localhost 9010 17 + 25

and the time series

dotnet run -p Client localhost 9010

Both will work fine. If you want, you can call python client to python service. Same result.

The complete Python service code

from concurrent import futures
import statistics
import sys

import grpc

import contracts_pb2
import contracts_pb2_grpc


class Service(contracts_pb2_grpc.SvcServicer):
    def Calculate(self, request, context):  # noqa
        result = -1
        if request.op == "+":
            result = request.x + request.y
        elif request.op == "-":
            result = request.x - request.y
        elif request.op == "*":
            result = request.x * request.y
        elif request.op == "/":
            if request.y != 0:
                result = request.x // request.y
        return contracts_pb2.CalculateReply(result=result)

    def Median(self, request_iterator, context):  # noqa
        vals = []
        for temp in request_iterator:
            vals.append(temp.value)
            med = 0
            if len(vals) == 10:
                med = statistics.median(vals)
                vals = []
                yield contracts_pb2.Temperature(timestamp=temp.timestamp, value=med)


with open("../cert/service-key.pem", "rb") as f:
    key = f.read()

with open("../cert/service.pem", "rb") as f:
    cert = f.read()

creds = grpc.ssl_server_credentials(
    [
        (
            key,
            cert,
        ),
    ]
)

port = int(sys.argv[1])
svc = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
contracts_pb2_grpc.add_SvcServicer_to_server(Service(), svc)
svc.add_secure_port(f"[::]:{port}", creds)
svc.start()
print(f"service listening on port {port}...")
svc.wait_for_termination()

Interoperability between different languages is one of the most exciting gRPC features. You can implement the micro-service in the most suitable language and it will be a accessible from variety of clients.


See also