gRPC is an HTTP/2-based Remote Procedure Call (RPC) framework that uses protocol buffers (protobuf
) as the underlying data serialization framework. It is an alternative to other language-neutral RPC frameworks such as Apache Thrift and Apache Arvo.
In the first part of this series on using gRPC in Python, we will implement a gRPC service in Python. Our gRPC service will be a users
service that will expose two functionalities: user signup and getting user details. In our fictional scenario, we will be interacting with this gRPC service directly from a command-line program and also from an HTTP web application.
Some of the gRPC concepts we will explore in this article are:
Streaming responses
Setting client-side metadata
Client-side timeouts
We will be using Python 3.5 for our demos in the article. The git repository has all the code listings we will be discussing in this article in the subdirectory demo1
.
Service Specification
The first step to implementing a gRPC server is to describe the server's interface
. The interface of the service is defined by the functions
it exposes and the input and output messages
. Our example service is called Users
and defines one function to create a user and another to fetch user details. The following defines the service Users
with two functions via protobuf
:
// users.proto syntax = "proto3"; import "users_types.proto"; service Users { rpc CreateUser (users.CreateUserRequest) returns (users.CreateUserResult); rpc GetUsers (users.GetUsersRequest) returns (stream users.GetUsersResult); }
syntax "proto3"
declares that we are using the proto3 version of the protocol buffer's language. Using import
, we import message definitions defined in a separate file. gRPC or protocol buffers does not require us to do this, and it is a purely personal preference to keep the service definitions and the message types separate.
The CreateUser
function accepts a request of type CreateUserRequest
defined in the users
package and returns a result of type CreateUserResult
defined in the same package. This function simulates the user-signup functionality of our Users
service.
The GetUsers
function accepts a request of type GetUsersRequest
and returns a stream of GetUsersResult
objects. Specifying the output as a stream allows us to return part of the complete response to the client as soon as they are ready. This function simulates the functionality of returning the user details of a given user or multiple users.
The user_types.proto
file imported above defines the messages. You can see the entire file in the demo1/grpc-services/protos/users/
subdirectory. It starts off by defining proto3
syntax as above. However, it also defines a package
:
syntax = "proto3"; package users;
Defining a new package allows us to define a namespace and hence we don't have to worry about not using the same message names across different services.
Next, we define the message User
describing a user in our service:
message User { string username = 1; uint32 user_id = 2; }
We then define the message we'll use to return the result of a create-user operation:
message CreateUserResult { User user = 1; }
Above, we define the CreateUserResult
message to have a single field of type User
(defined earlier). The GetUsers
function allows querying multiple users at once. Hence we define the GetUsesRequest
message as follows:
message GetUsersRequest { repeated User user = 1; }
When a field in a message is defined as repeated
, the message can have this field zero or more times. In Python, it is equivalent to defining a list of users.
Next, use the service and types definition to generate language-specific bindings that will allow us to implement servers to use the above service and clients to talk to the server. Before we can do that however, we will create Python 3.5 virtual environment, activate it, and install two packages, grpcio and grpcio-tools:
$ python3.5 -m venv ~/.virtualenvs/grpc $ . ~/.virtualenvs/grpc/activate $ python -m pip install grpcio grpcio-tools
Writing the Server
Let's generate the Python bindings from the above protobuf definition:
$ cd demo1/grpc-services/protos/ $ ./build.sh
The build.sh
is a bash script that wraps around the grpc-tools
package and essentially does the following:
$ cd users $ mkdir ../gen-py $ python -m grpc_tools.protoc \ --proto_path=. \ --python_out=../gen-py \ --grpc_python_out=../gen-py \ *.proto
The arguments serve different purposes:
proto_path
: Path to look for the protobuf definitionspython_out
: Directory to generate the protobuf Python codegrpc_python_out
: Directory to generate the gRPC Python code
The last argument specifies the protobuf files to compile.
The created gen-py
directory will have the following four generated files:
- users_types_pb2.py - users_types_pb2_grpc.py - users_pb2.py - users_pb2_grpc.py
The *_pb2.py
files has the code corresponding to the types we have defined and the *_grpc.py
files has the generated code related to the functions we have defined.
Using the above generated code, we will write our "servicer" code:
import users_pb2_grpc as users_service import users_types_pb2 as users_messages class UsersService(users_service.UsersServicer): def CreateUser(self, request, context): metadata = dict(context.invocation_metadata()) print(metadata) user = users_messages.User(username=request.username, user_id=1) return users_messages.CreateUserResult(user=user) def GetUsers(self, request, context): for user in request.user: user = users_messages.User( username=user.username, user_id=user.user_id ) yield users_messages.GetUsersResult(user=user)
The UsersService
subclasses the UsersServicer
class generated in users_pb2_grpc
and implements the two functions implemented by our service CreateUser
and GetUsers
. Each function accepts two arguments: request
referring to the incoming request message and context
giving access to various contextual data.
In the CreateUser
function, we see an example of accessing the metadata associated with the request. metadata
is a list of arbitrary key-value pairs that the client can send along with a request and is part of the gRPC specification itself. We access it by calling the invocation_metadata()
method of the context
object.
The reason we need the explicit dict
conversion is because they are returned as a list of tuples. Since the CreateUser
function merely simulates a user-signup functionality, we just return back a new User
object, with the username
being the same as that provided in the request (a CreateUser
object) accessible via request.username
and set the user_id
as 1. However, we do not send the newly created user
object on its own but wrap it in a CreateUserResult
object.
The GetUsers
function simulates the funcionality of querying user details. Recall that our GetUsersRequest
object declares the user
field as repeated. Hence, we iterate over the user field, with each item being an User
object. We access the username
and user_id
from the object and use those to create a new User
object.
The output of the GetUsers
function was declared to be a stream of GetUsersResult
messages. Hence, we wrap the created User
object in a GetUsersResult
and yield
it back to the client.
The next step is to set up our server code to be able to process incoming gRPC requests and hand it over to our UsersService
class:
def serve(): server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) users_service.add_UsersServicer_to_server(UsersService(), server) server.add_insecure_port('127.0.0.1:50051') server.start() try: while True: time.sleep(_ONE_DAY_IN_SECONDS) except KeyboardInterrupt: server.stop(0)
The grpc.Server function creates a server. We call it above with the only required argument, a futures.ThreadPoolExecutor
with the maximum number of workers set to 10. Then we call the add_UsersServicer_to_server
function to register UsersService
with the server.
Using add_insecure_port
, we set up the listening IP address and port and then start the server using the start()
method. The add_insecure_port
function is used since we are not setting SSL/TLS for our client/server communication and authentication.
The code for the server can be found in demo1/grpc-services/users/server/server.py
.
Let's now start our server:
$ cd demo1/grpc-services/users/ $ ./run-server.sh
The run-server.sh
bash script is basically:
$ PYTHONPATH=../../protos/gen-py/ python server.py
!Sign up for a free Codeship Account
Writing a Client
To interact with our above server, a gRPC client will first create a channel
to our server and create a stub
object to interact with the users service via this channel:
channel = grpc.insecure_channel('localhost:50051') try: grpc.channel_ready_future(channel).result(timeout=10) except grpc.FutureTimeoutError: sys.exit('Error connecting to server') else: stub = users_service.UsersStub(channel)
The channel_ready_future
function allows the client to wait for a specified timeout duration (in seconds) for the server to be ready. If our client times out, we exit; else, we create a UsersStub
object passing the channel created as an argument.
Using this stub object, we invoke a gRPC call as follows:
metadata = [('ip', '127.0.0.1')] response = stub.CreateUser( users_messages.CreateUserRequest(username='tom'), metadata=metadata, ) if response: print("User created:", response.user.username)
Above, we see a demonstration of how we pass additional metadata as part of a request. In a realistic scenario, we will pass in data such as a request ID, the authenticated user ID making the request, etc.
Next, we see how we create a message that has a field repeated twice:
request = users_messages.GetUsersRequest( user=[users_messages.User(username="alexa", user_id=1), users_messages.User(username="christie", user_id=1)] )
Since the GetUsersRequest
message specified the field user
as repeated, we create a list of User
objects and set it as the value of this field.
The result of the GetUsers
function is a stream. Hence, we use the following construct to access the responses:
response = users_service.GetUsers(request) for resp in response: print(resp)
Let's now run the client in the demo1/grpc-services/users/sample_client_demo.py
file:
$ cd demo1/grpc-services/users $ PYTHONPATH=../protos/gen-py/ python sample_client_demo.py User created: tom user { username: "alexa" user_id: 1 } user { username: "christie" user_id: 1 }
On the server side, we will see the metadata being printed for the incoming client request:
{'ip': '127.0.0.1', 'user-agent': 'grpc-python/1.6.0 grpc-c/4.0.0 (manylinux; chttp2; garcia)'}
Setting Client-side Timeout
When making a request, we can set client-side timeouts so that our client doesn't get into a state where it's waiting for a long time for a request to complete. There's no way currently to set a stub-wide timeout; therefore, we have to specify the timeout per call, like so:
response = stub.GetUsers(request, timeout=30)
Above, we set the timeout to 30 seconds. If the client doesn't get a response from the server within 30 seconds, it will raise a grpc.RpcError
exception with the status code DEADLINE_EXCEEDED
. Ideally, we will catch the exception and report a user-friendly error message.
Demo Web Application Talking to gRPC
The demo1/webapp
directory contains a Flask application exporting a single endpoint /users/
that invokes the GetUsers
gRPC function and streams the responses back:
@app.route('/users/') def users_get(): request = users_messages.GetUsersRequest( user=[users_messages.User(username="alexa", user_id=1), users_messages.User(username="christie", user_id=1)] ) def get_user(): response = app.config['users'].GetUsers(request) for resp in response: yield MessageToJson(resp) return Response(get_user(), content_type='application/json')
Since the response from our gRPC function is a stream, we create a generator function get_user()
to yield a response as we get one. We use the MessageToJson
function from the protobuf
Python package to convert a protobuf message to a JSON object.
What's Next?
In the next article, we will be enhancing our gRPC server and learning about:
Writing a secure gRPC server
Error handling
Writing interceptors and exporting metrics
The code listings for this article can be found in demo1
sub-directory in this git repo.