
Posted on • Originally published ativanyu2021.hashnode.dev
First glimpse into gRPC through Python (Part 2)
1. Previous Post
First glimpse into gRPC through Python (Part 1)
2. Implementation and Examples
After introduced gRPC concept and development flow in previous post, we will show how to implement gRPC in python.
Example Code link (To run the below examples, please go throughREADME.md
in the link)
2.1. 4 kinds of service method
There are 4 kinds of service methods in gRPC and the difference between them is to applysteaming or not in request or response.
2.1.1. Simple RPC
This type is just like the http request and response. Client send a single request to server and server reply a single reponse.
Proto file
rpcdoSimple(Request)returns(Response){}
Server
defdoSimple(self,request,context):output=f"Hello{request.input}!"returnResponse(output=f"Hello{request.input}!")
Client
defcall_doSimple(self):withgrpc.insecure_channel('localhost:50051')aschannel:stub=SampleServiceStub(channel)request=Request(input=self.faker.name())logger.info(f"doSimple client sent:{request.input}")response=stub.doSimple(request)logger.info(f"doSimple client received:{response.output}")
Command line output
[ 2022-07-15 09:43:27,530 ][ INFO ][ call_doSimple ] doSimple client sent: Lindsay Ross[ 2022-07-15 09:43:27,531 ][ INFO ][ call_doSimple ] doSimple client received: Hello Lindsay Ross!
2.1.2. Response-streaming RPC
For this type, streaming happens in the response. Client sends a single request to server and server returns a streaming of multiple responses.
Proto file
rpcdoResponseStreaming(Request)returns(streamResponse){}
Server
defdoResponseStreaming(self,request,context):faker=Faker()name_list=[*[faker.name()foriinrange(3)],request.input]fornameinname_list:time.sleep(0.5)yieldResponse(output=name)
Client
defcall_doResponseStreaming(self):withgrpc.insecure_channel('localhost:50051')aschannel:stub=SampleServiceStub(channel)request=Request(input=self.faker.name())logger.info(f"doResponseStreaming client sent:{request.input}")response_generator=stub.doResponseStreaming(request)forresponseinresponse_generator:logger.info(f"doResponseStreaming client received:{response.output}")
Command line output
[ 2022-07-15 10:14:27,347 ][ INFO ][ call_doResponseStreaming ] doResponseStreaming client sent: Veronica Good[ 2022-07-15 10:14:27,971 ][ INFO ][ call_doResponseStreaming ] doResponseStreaming client received: Richard Torres[ 2022-07-15 10:14:28,472 ][ INFO ][ call_doResponseStreaming ] doResponseStreaming client received: Monica Russo[ 2022-07-15 10:14:28,985 ][ INFO ][ call_doResponseStreaming ] doResponseStreaming client received: Sean Lane[ 2022-07-15 10:14:29,498 ][ INFO ][ call_doResponseStreaming ] doResponseStreaming client received: Veronica Good
2.1.3. Request-streaming RPC
Similar to the above type, but the streaming happens in the request. Client sends a streaming of multiple requests to server and server returns a single response.
Proto file
rpcdoRequestStreaming(streamRequest)returns(Response){}
Server
defdoRequestStreaming(self,request_iterator,context):result_list=[]forrequestinrequest_iterator:result_list.append(request.input.upper())returnResponse(output=", ".join(result_list))
Client
defcall_doRequestStreaming(self):defget_fake_name_generator():faker=Faker()for_inrange(10):time.sleep(0.5)name=faker.name()logger.info(f"doRequestStreaming client sent:{name}.")yieldRequest(input=name)withgrpc.insecure_channel('localhost:50051')aschannel:stub=SampleServiceStub(channel)request=get_fake_name_generator()response=stub.doRequestStreaming(request)logger.info(f"doRequestStreaming client received:{response.output}")
Command line output
[ 2022-07-15 10:21:08,058 ][ INFO ][ get_fake_name_generator ] doRequestStreaming client sent: Courtney Hammond.[ 2022-07-15 10:21:08,562 ][ INFO ][ get_fake_name_generator ] doRequestStreaming client sent: James Petersen.[ 2022-07-15 10:21:09,070 ][ INFO ][ get_fake_name_generator ] doRequestStreaming client sent: Tom Anderson.[ 2022-07-15 10:21:09,073 ][ INFO ][ call_doRequestStreaming ] doRequestStreaming client received: COURTNEY HAMMOND, JAMES PETERSEN, TOM ANDERSON
2.1.4. Bidirectionally-streaming RPC
As you may already guess the meaning from the name, the streaming happens in both request and response.
Proto file
rpcdoBidirectional(streamRequest)returns(streamResponse){}
Server
defdoBidirectional(self,request_iterator,context):forrequestinrequest_iterator:yieldResponse(output=request.input+" is excellent!")
Client
defcall_doBidirectional(self):defget_fake_name_generator():faker=Faker()for_inrange(3):time.sleep(0.5)name=faker.name()logger.info(f"doRequestStreaming client sent:{name}.")yieldRequest(input=name)withgrpc.insecure_channel('localhost:50051')aschannel:stub=SampleServiceStub(channel)request=get_fake_name_generator()response_generator=stub.doBidirectional(request)forresponseinresponse_generator:logger.info(f"doBidirectional client received:{response.output}")
Command line output
[ 2022-07-15 10:41:11,994 ][ INFO ][ get_fake_name_generator ] doRequestStreaming client sent: Sherry Hanson.[ 2022-07-15 10:41:11,996 ][ INFO ][ call_doBidirectional ] doBidirectional client received: Sherry Hanson is excellent![ 2022-07-15 10:41:12,497 ][ INFO ][ get_fake_name_generator ] doRequestStreaming client sent: Danielle Jones.[ 2022-07-15 10:41:12,499 ][ INFO ][ call_doBidirectional ] doBidirectional client received: Danielle Jones is excellent![ 2022-07-15 10:41:12,999 ][ INFO ][ get_fake_name_generator ] doRequestStreaming client sent: Alexis Goodwin.[ 2022-07-15 10:41:13,001 ][ INFO ][ call_doBidirectional ] doBidirectional client received: Alexis Goodwin is excellent!
2.2. Special Data Types and default value
gRPC can handles some speical data types, like date time, list or map, but need to pay some attention to avoid bug.
2.2.1. Date Time
gRPC use timestamp to handle date time and it uses timezone.
In proto file
google.protobuf.Timestampdate=1;
You need to include timezone in the input date time in order to transfer the data correctly to the server side.
Below is the output send the dateWITHOUT timezone from client to server side:
[ 2022-07-15 11:04:52,633 ][ INFO ][ call_doSpecialDataType ] doSpecialDataType client sent: request.date=2022-07-15 11:04:52[ 2022-07-15 11:04:52,654 ][ INFO ][ doSpecialDataType ] doSpecialDataType Server received: request.date=2022-07-15 20:04:52
We can see there are 9 hours difference between client and server since gRPC regards the date time without timezone as UTC time. When the date input received by the server, it automatically add 9 hours as we are in UTC+09:00 region.
2.2.2. List
gRPC can handle list datatype by simply addingrepeated
in front of the field in proto file.
repeatedstringnames=1;
2.2.3. Map
gRPC can handle map datatype by defining map as the type and specify key and value type in proto file.
map<string,string>name2phoneNumMap=3;
2.2.4. Default value
According to thislink, it mentioned that
if a scalar message field is set to its default, the value will not be serialized on the wire.
We will explain the meaning in the following example:
proto file
repeatedCardInfocardInfos=4;messageCardInfo{stringname=1;int32numberOfCreditCard=2;}
We have a list of cardInfo and each card info contains an integer field callednumberOfCreditCard
. In the response, we setnumberOfCreditCard
of last CardInfo of the cardInfo list to be 0.
Server
cardInfos=request.cardInfoscardInfos.append(CardInfo(name="Boris Lee",numberOfCreditCard=0))
Command line output
[ 2022-07-15 11:21:39,200 ][ INFO ][ call_doSpecialDataType ] doSpecialDataType client received: response.cardInfos= [name: "Katherine Soto"numberOfCreditCard: 1, name: "Kerry Powell"numberOfCreditCard: 1, name: "Mrs. Christina Hicks DDS"numberOfCreditCard: 1, name: "Boris Lee" <- No "numberOfCreditCard"]
We can see that Boris Lee doesNOT have numberOfCreditCard. Since 0 is regarded as default value, it will not be serialized on the wire and transfer back to client.
To solve this problem, we need to addoptional
in front of the field in proto file.
optionalint32numberOfCreditCard=2;
Generate the code and run the program again, you can see the zero appeared.
, name: "Boris Lee"numberOfCreditCard: 0]
3. Unit test
We can use python nativeunit test framework to write unit test for gRPC. This is crucial as you do not need to switch on/off the gRPC server again and again whenever there is change in your code in order to test your code manually.
3.1. Create test server
First, you need to create a test server for receiving gRPC call in thesetUp
method so that whenever the test method is called, it will first set up the test server.
classSampleServiceTest(unittest.TestCase):defsetUp(self):logger.info(f"=== Method:{self._testMethodName} =======")servicers={sample_service_pb2.DESCRIPTOR.services_by_name['SampleService']:SampleService()}self.test_server=grpc_testing.server_from_dictionary(servicers,grpc_testing.strict_real_time())
3.2. Create test method
Next, you need to create at least one test method for each gRPC method you want to test.
deftest_doSimple(self):faker=Faker()request=sample_service_pb2.Request(input=faker.name())doSimple_method=self.test_server.invoke_unary_unary(method_descriptor=(sample_service_pb2.DESCRIPTOR.services_by_name['SampleService'].methods_by_name['doSimple']),invocation_metadata={},request=request,timeout=1)response,_,code,_=doSimple_method.termination()self.assertEqual(code,grpc.StatusCode.OK)self.assertEqual(response.output,f"Hello{request.input}!")
3.3. Run test
Finally, run your test in the main thread
if__name__=='__main__':unittest.main()
For detail, take a look insample_service_test.py
4. Reason I wrote this series of blogs
I wrote this blog as I found that when I first learnt about gRPC, it is a little bit difficult to grasp the development method in a fast manner. There are a lot of built-in examples in the gRPC site, but it is a little bit too complicate for a beginner.
At first, what I wanted to know the most are:
1. How can I make a new method?2. How can I call the method in a microservice through gRPC?3. How can I handle the input and output?4. How can I do the unit test?
There are resources in Internet, but they are either scattered around or they used a lot of words to explain without a diagram. As a result, I decided to write this blog to present gRPC (using Python) in a simple and practical way so newcomer can pick up the concept easily and then start development immediately.
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse