Python3搭建gRPC服务(进阶版二)之Protocol Buffers语法与实战

720 阅读6分钟

文章导读

本文主要介绍gRPC中重要的部分Protocol Buffers语法,预计需要花费15分钟左右的时间,阅读后你将收获以下知识,

  1. Protocol Buffers语法规范。
  2. 通过Protocol Buffers进行数据结构定义。
  3. 通过Protocol Buffers进行服务定义
  4. 多种数据类型的gRPC服务搭建。

这是一篇进阶版本的文章,读者应具备基本的gRPC基础,在此之前已有文章讲解Python3搭建gRPC(入门版),如不了解gRPC建议先阅读

Python3搭建gRPC(入门版)

Python3搭建gRPC服务(进阶版一)之Protocol Buffers基础概念

同时提供以下进阶材料

  1. Python3搭建gRPC(入门版)
  2. 同步视频教程(后续会更新)
  3. 其他相关联文章,包括pytest进行gRPC测试,python3客户端测试java/golang grpc服务端(后续会更新)

如果大家觉得有收获,记得一键三连,后续继续为大家输出更多有价值的文章!!!

Protocal Buffers语法

Protocal Buffers是描述型语言,语法简单,本章将详细讲解

数据类型和各语言之间的关系

Protocal Buffers最终都是需要转换成各种语言的源码,需要了解在pb的数据类型和各语言之间的对应关系

.proto TypeNotesC++ TypeJava/Kotlin Type[1]Python Type[3]Go TypeRuby TypeC# TypePHP TypeDart Type
doubledoubledoublefloatfloat64Floatdoublefloatdouble
floatfloatfloatfloatfloat32Floatfloatfloatdouble
int32int32intintint32Fixnum or Bignum (as required)intintegerint
int64int64longint/long[4]int64Bignumlonginteger/string[6]Int64
uint32uint32int[2]int/long[4]uint32Fixnum or Bignum (as required)uintintegerint
uint64uint64long[2]int/long[4]uint64Bignumulonginteger/string[6]Int64
sint32int32intintint32Fixnum or Bignum (as required)intintegerint
sint64int64longint/long[4]int64Bignumlonginteger/string[6]Int64
fixed32uint32int[2]int/long[4]uint32Fixnum or Bignum (as required)uintintegerint
fixed64uint64long[2]int/long[4]uint64Bignumulonginteger/string[6]Int64
sfixed32int32intintint32Fixnum or Bignum (as required)intintegerint
sfixed64int64longint/long[4]int64Bignumlonginteger/string[6]Int64
boolboolbooleanboolboolTrueClass/FalseClassboolbooleanbool
stringstringStringstr/unicode[5]stringString (UTF-8)stringstringString
bytesstringByteStringstr (Python 2) bytes (Python 3)[]byteString (ASCII-8BIT)ByteStringstring

常见类型如何定义

pb包含有多种类型,包括int,float,string,bool,map等

pb没有list类型,该如何定义,在list类型章节介绍

基础类型
syntax = "proto3";


message IntRequest{
  int32 id=1;
}

message FloatRequest{
  float num=1;
}

message StringRequest{
  string name=1;
}

message BoolRequest{
  bool exist=1;
}
map类型

map类型需要定义内部的泛型,如下表示search为key和value都是string类型的map,在Python中叫字典

message MapRequest{
  map<string, string> search=1;
}
list类型

list在pb中没有对应的数据类型,需要使用repeated关键词的意思是可重复0到N个相同的值,和列表的定义是一致的,列表中有0到N元素

如下定义表示UsersRequest数据结构中,usernames是一个string类型的列表

message UsersRequest{
  repeated string usernames=1;
}
外部依赖的类型怎么处理

已有一个map_demo.proto

# map_demo.proto
syntax = "proto3";

message MapDemo{
  map<string, string> keyValue=1;
}

map_use_demo.proto需要引用map_demo.proto中定义的类型

# map_use_demo.proto

syntax = "proto3";

import "map_demo.proto";

service MapService{
  rpc MapHello(Request) returns (Response){}
}

message Request{
  MapDemo mapDemo=1;
}

message Response{
  string name=1;
}

需要在map_use_demo.proto中import "map_demo.proto",这样map_demo中定义的MapDemo数据类型就可以使用了

外部引用有2个注意点:

1、在第四章中讲到grpc_tools.protoc -I参数就是在这种情况下使用,-I需要指定为map_demo.proto所在的目录,如不指定,生成map_use_demo时会找不到map_demo.proto

2、map_demo.proto也需要编译成Python代码

具体demo实例请参考第6章

message定义

message定义一个数据结构,类似于Python的类

message User{
  string name=1;       # 从1开始依次往后
  string password=2;   # 2
}

message 类名{
  类型 变量名=1; # 这里1是固定值,从1开始依次往后
  类型 变量名=2; # 这里1是固定值,从1开始依次往后
}

service定义

定义服务相对简单,通过关键字service配合关键字rpc可以进行服务定义

returns定义接口返回的数据类型

service MapService{
  rpc MapHello(Request) returns (Response){}
}

service 服务名{
  rpc 接口名(请求类型) returns (返回类型){}
}

pb转成源码作用解释

常见gRPC实战

将通过2个实际案例讲解复杂的pb定义

实战一:多种数据类型的复杂接口

  1. 第一步proto定义,
# all_demo.proto
syntax = "proto3";


service UserService{
  rpc GetUserByKeyWord(Request) returns(Response){}
}

message User{
  string name=1;
  int32 id=2;
  float age=3;
  bool isBoy=4;
}

message UserList{
  repeated User user=1;
}

message Response{
  UserList userList=1;
}

message Request{
  map<string,string> keyWord=1;
  bool fuzzy=2;
}

2.转成Python源码

python3 -m  grpc_tools.protoc -I ./ --python_out=. --pyi_out=. --grpc_python_out=. all_demo.proto

3.开发服务端

import grpc
from concurrent import futures

from grpc_demo.pb.all_demo_pb2_grpc import UserServiceServicer, add_UserServiceServicer_to_server
from grpc_demo.pb.all_demo_pb2 import Request, Response, User, UserList


class UserService(UserServiceServicer):

    def GetUserByKeyWord(self, request, context):
        user1 = User(name="zhangsan", id=1, age=24.3, isBoy=False)
        user2 = User(name="zhangsan1", id=1, age=24.3, isBoy=False)
        user_list = UserList(user=[user1, user2])
        response = Response(userList=user_list)
        return response


def serve():
    port = "50000"
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    add_UserServiceServicer_to_server(UserService(), server)
    server.add_insecure_port('[::]:' + port)
    server.start()
    print("Server started, listening on " + port)
    server.wait_for_termination()


if __name__ == '__main__':
    serve()

4.开发客户端

import grpc
from grpc_demo.pb.all_demo_pb2_grpc import UserServiceStub
from grpc_demo.pb.all_demo_pb2 import Request


def run():
    with grpc.insecure_channel("localhost:50000") as channel:
        stub = UserServiceStub(channel)
        key_word = {"name": "zhang"}
        request = Request(keyWord=key_word, fuzzy=False)
        resp = stub.GetUserByKeyWord(request)
        print(resp)


if __name__ == '__main__':
    run()

5.通过客户端调试

image.png

实战二:存在外部依赖的场景

1、定义第一个proto,map_demo.proto

这个proto定义了MapDemo的类

#map_demo.proto
syntax = "proto3";

message MapDemo{
  map<string, string> keyValue=1;
}

2、定义第二个proto,这个proto会引用第一个proto中定义的类

这个proto定义了接口,以及Request类,Request类引用了第一个proto中的MapDemo

syntax = "proto3";

import "map_demo.proto";


service MapService{
  rpc MapHello(Request) returns (Response){}
}

message Request{
  MapDemo mapDemo=1;
}

message Response{
  string name=1;
}

3、将map_demo.proto转换成python代码

这一步很重要,虽然map_demo是被use_map_demo引用,但也需要生成python代码,否则会报错

python3 -m  grpc_tools.protoc -I ./ --python_out=. --pyi_out=. --grpc_python_out=. map_demo.proto

4、将use_map_demo.proto转换成python代码

python3 -m  grpc_tools.protoc -I ./ --python_out=. --pyi_out=. --grpc_python_out=. map_use_demo.proto

注意:这行完成上述操作后会生成如下python代码

image.png

5、gRPC服务代码

from concurrent import futures

import grpc

from grpc_demo.pb.map_use_demo_pb2_grpc import MapServiceServicer, add_MapServiceServicer_to_server
from grpc_demo.pb.map_use_demo_pb2 import Response


class MapUseDemoServer(MapServiceServicer):

    def MapHello(self, request, context):
        print(request)
        return Response(name="hello map")


def serve():
    port = "40001"
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    add_MapServiceServicer_to_server(MapUseDemoServer(), server)
    server.add_insecure_port('[::]:' + port)
    server.start()
    print("Server started, listening on " + port)
    server.wait_for_termination()


if __name__ == '__main__':
    serve()

6、客户端代码

import grpc
from grpc_demo.pb.map_use_demo_pb2_grpc import MapServiceStub
from grpc_demo.pb.map_use_demo_pb2 import Request
from grpc_demo.pb.map_demo_pb2 import MapDemo


def run():
    with grpc.insecure_channel("localhost:40001") as channel:
        stub = MapServiceStub(channel)
        map_demo = MapDemo(keyValue={"hello": "a"})
        response = stub.MapHello(Request(mapDemo=map_demo))
        print(response.name)


if __name__ == '__main__':
    run()

7、启动服务端,通过客户端调用查看结果

客户端打印hello,表示调用成功

image.png

服务端打印request信息

image.png

常见错误

外部依赖没有转换

错误信息:ModuleNotFoundError: No module named 'grpc_demo.pb.map_demo_pb2'

image.png 这个错误原因是use_map_demo.proto引用了map_demo.proto,但map_demo.proto没有转换成pyton代码导致引入失败

解决方案:将map_demo.proto转换成python代码

python3 -m  grpc_tools.protoc -I ./ --python_out=. --pyi_out=. --grpc_python_out=. map_demo.proto

客户端入参类型错误

ValueError: Protocol message MapDemo has no "xxxx" field.

image.png

错误演示:Request的参数应该是mapDemo类型,不是一个字典

image.png

正确做法:定义map_demo的对象,入参给Request

image.png