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:
- Unary – This is similar to REST. The client sends a request, and the server sends a single response message.
- Server Streaming – The client sends a message, and the server responds with a stream of messages.
- Client Streaming – The client sends a stream of messages, and the server responds with a single message.
- 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 theproto_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:
- Creates a gRPC server from the
grpc
library - Registers our Interface Service to the newly created gRPC server
- Adds a port that gRPC server will listen on
- 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:
- Creates an insecure channel with a gRPC server running on your localhost on port 5001.
- Using the channel created in step 1, subscribes to the
InterfaceService
service we defined in our.proto
file. - Creates an
Interface
message object to pass into the gRPC call. - Calls the
StandardizeInterfaceDescription
function call and passes in theinterface
object from step 3. - 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 :
Contact Us to Learn More
Share details about yourself & someone from our team will reach out to you ASAP!