What Is gRPC? – Part 2

This blog will build on top of what was discussed in Part 1 of this series. If you have not, I highly recommend checking it out here. In it, I discuss Protocol Buffers, which are an integral part of gRPC. In this blog post, we’ll build a simple gRPC client/server setup so we can actually see the definition files in action. In order to get to a fully working gRPC client/server, we need to take the following steps:

  • Create a service in our Protocol Buffer
  • Create a request method within our service
  • Create a response in our Protocol Buffer

Extending Our Protocol Buffer Definition File

Let’s go over the three additions we need to make to our Protocol Buffer definition file (the service, request, and response portions).

Adding the Service

Currently, our Protocol Buffer definition should look like this:

syntax = "proto2";

package tutorial;

message Interface {
  required string name = 1
  optional string description = 2
  optional int32 speed = 3
}

Let’s add the service block to our definition file.

service InterfaceService {
}

This line defines a service, named InterfaceService, which our gRPC server will be offering. Within this service block, we can add the methods that the gRPC client can call.

Adding the Request and Response Methods

Before we add the request and response methods, I need to discuss the different types of messages gRPC services can handle. Four basic implementations of gRPC request and response methods can be used:

  1. Unary – This is similar to REST. The client sends a request, and the server sends a single response message.
  2. Server Streaming – The client sends a message, and the server responds with a stream of messages.
  3. Client Streaming – The client sends a stream of messages, and the server responds with a single message.
  4. Bidirectional Streaming – Both the client and server send streams of messages.

Each method has its pros and cons. For the sake of keeping this blog short, I’ll be implementing a unary request/response gRPC service.

You can read official documentation on gRPC message types here.

Let’s add a unary request/response type to our InterfaceService in our definition file.

service InterfaceService {
  rpc StandardizeInterfaceDescription(Interface) returns (Interface) {}
}

We are defining an RPC remote method named StandardizeInterfaceDescription that takes in data that adheres to our Interface message type we defined in the first blog post. We also define that the method will return data that adheres to our Interface message type.

You can have a gRPC function take in and return different message types.

Now, our Protocol Buffer definition file titled interface.proto should look like this.

syntax = "proto2";


service Interface {
  rpc StandardizeInterfaceDescription(Interface) returns (Interface) {}
}

package tutorial;

message Interface {
  required string name = 1;
  optional string description = 2;
  optional int32 speed = 3;
}

Re-creating Our Python gRPC Files

Now that we have our updated interface.proto definition file, we need to recompile to update our auto-generated gRPC Python code. Here we will be using the grpcio-tools library rather than the Protocol Buffer compiler we used in the first blog post. The grpcio-tools library is the more all encompassing tool compared to the Protcol Buffer compiler. To install the grpcio-tools library, run the command pip install grpcio-tools. Then, making sure you are in the same directory as the interface.proto file, run the command python -m grpc_tools.protoc -I . --python_out=. --grpc_python_out=. interface.proto.

The -I designates the proto_path or, in other words, the path to the destination for your proto definition file. The last argument is the name of your proto file.

After running this command, you should have two new files named interface_pb2.py and interface_pb2_grpc.py. The first, is the protobuf class code and the second is the gRPC code related to our service. Here is a snapshot of the current directory structure:

├── interface_pb2_grpc.py
├── interface_pb2.py
├── interface.proto.py

Creating the gRPC Server

Now let’s create the code needed for our gRPC server. Create a new file at the root of the directory we are working in and name it grpc_server.py. Copy the below code snippet into that file:

from concurrent import futures

import grpc
import interface_pb2
import interface_pb2_grpc


class InterfaceGrpc(interface_pb2_grpc.InterfaceServiceServicer):
    def StandardizeInterfaceDescription(self, request):
        standard_description = request.description.upper()
        return interface_pb2.Interface(
            name=request.name, description=standard_description, speed=request.speed
        )


def server():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=2))
    interface_pb2_grpc.add_InterfaceServiceServicer_to_server(InterfaceGrpc(), server)
    server.add_insecure_port("[::]:5001")
    print("Starting gRPC Server")
    server.start()
    server.wait_for_termination()


if __name__ == "__main__":
    server()

Let’s take a quick look at this file. At the top we are importing a number of things. First, from the concurrent library, we are importing futures. This just allows us to asynchrously execute callables. We also import the grpc library. Lastly, we import the two files we created earlier.

At the bottom of the file, our entry point into this file is the server() function, which does four main things:

  1. Creates a gRPC server from the grpc library
  2. Registers our Interface Service to the newly created gRPC server
  3. Adds a port that gRPC server will listen on
  4. Starts the server

Near the top of the file, we are extending the InterfaceServiceServicer from our interface_pb2_grpc file. Within this class is where we define the logic for the functions we created stubs for in our .proto file. Functions created in this class take a single required argument other than self listed below:

  • request – This is the interface message type sent into our gRPC function.

The next few lines take the data passed in by the request argument, capitalizes the description, creates an interface message type and sends it back to the client. That’s the gRPC server code. Let’s quickly put together a new file for our gRPC client.

Creating the gRPC Client

Creating the gRPC client is pretty straightforward. Copy and paste the below code snippet into a file called grpc_client.py.

import grpc
import interface_pb2
import interface_pb2_grpc


def client():
    channel = grpc.insecure_channel("localhost:5001")
    grpc_stub = interface_pb2_grpc.InterfaceServiceStub(channel)
    interface = interface_pb2.Interface(
        name="GigabitEthernet0/1", description="Port to DMZ firewall", speed=20
    )
    retrieved_interface = stub.StandardizeInterfaceDescription(interface)
    print(retrieved_interface)


if __name__ == "__main__":
    client()

The client function does the following:

  1. Creates an insecure channel with a gRPC server running on your localhost on port 5001.
  2. Using the channel created in step 1, subscribes to the InterfaceService service we defined in our .proto file.
  3. Creates an Interface message object to pass into the gRPC call.
  4. Calls the StandardizeInterfaceDescription function call and passes in the interface object from step 3.
  5. Lastly, prints out what was received from the gRPC call.

Running the Client and Server Together

Now, let’s run the client and server together so we can see this code in action! First, open a terminal in the directory where the grpc_server.py lives and run python3 grpc_server.py. You should have no prompt and get the line Starting gRPC Server.

Open another terminal in the same location and run python3 grpc_client.py.

~/repo/Sandbox/blog_grpc ❯ python3 grpc_client.py

name: "GigabitEthernet0/1"
description: "PORT TO DMZ FIREWALL"
speed: 20

You can run the client side over and over again. The server will stay up and respond to however many requests it receives.

If everything was successful, you will get the response shown above. As you can see, it capitalized our entire description just like we wanted. I would definitely suggest playing around with this simple implementation of a gRPC server and client. You can even start to include non-unary functions in your gRPC server to explore more in-depth.


Conclusion

In this blog post, we updated our existing .proto definition file with our InterfaceService and added a StandardizeInterfaceDescription function within that service. We also used the grpc_tools library to generate the needed code to create our own gRPC server. Lastly, we created a small gRPC client to show our gRPC server in action. Hopefully, you now have a deeper understanding of what gRPC is and how it works. Initially, I wanted to explore gRPC in the networking world in this blog post. However, I thought it important to continue to look at gRPC a little more in-depth. In Part 3 of this series, I will be discussing where gRPC is within the networking world. We will review on the more established names, such as Cisco, Arista, and Juniper and look at how they are using gRPC and how they are enabling Network Engineers to use gRPC for their automation.

-Adam


Tags :

ntc img
ntc img

Contact Us to Learn More

Share details about yourself & someone from our team will reach out to you ASAP!

Author