python3中使用grpc
文章目录
官方demo:https://grpc.io/docs/languages/python/basics/
该案例包含对美国一些地理位置进行检索查询等操作。通过gRPC
的方式,客户端通过传递不同类型的参数调用服务端方法并获取返回结果。
gRPC
在gRPC
中,客户端应用程序可以直接调用服务器(另一台机器上运行)应用程序中的方法,就像调用本地对象一样,这更容易创建分布式应用程序和服务。
在许多RPC系统中,gRPC
是基于**定义服务(接口)**的思想,通过参数和返回类型指定可以被远程调用的方法。
在服务器端,服务器实现了这个接口,并运行一个gRPC
服务器来处理客户端调用。在客户端,客户端有一个存根stub
(在某些语言中仅称为客户端),它提供与服务器相同的方法。
具体调用过程可以看这张图:https://grpc.io/docs/what-is-grpc/introduction/
gRPC
客户端和服务器可以在各种环境中相互运行和通信,并且可以用任何gRPC支持的语言编写。例如,你可以用Java
轻松创建一个gRPC
服务器,客户端使用Go
、Python
或Ruby
。
Protocol Buffers
谷歌成熟的开源机制,用于序列化结构化数据
Protocol Buffers
的数据结构定义在.proto后缀的文件中,通过message
关键字定义,每条message
都是一个逻辑信息记录,其中包含一系列键值对,
message Person{
string name = zhangsn;
int32 id = 2;
bool has_ponycopter = 3;
}
定义完成之后,就可以使用protocol buffer
编译器编译.proto文件,然后生成所使用的语言可以访问的类。它们为每个字段提供了简单的访问函数,如name()
和set_name()
,以及将整个结构序列化/解析为原始字节的方法。
此外,可以在普通的.proto
文件中定义gRPC
服务,使用RPC方法参数并返回指定为protocol buffer
消息的类型:
// rpc方法和message定义
service Greeter {
// 基本语法
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}
python中使用grpc
安装grpc
使用pip进行安装
python -m pip install grpcio
安装python的grpc-tools,它包含协议编译器protoc
和用于从.proto
定义的服务生成服务器和客户端代码的特殊插件
python -m pip install grpcio-tools
官方route_guide案例
编写proto文件
按照需求和上述语法编写proto
文件,在service
中的定义RPC
方法和message
。注意需要在文件开始标明语法版本:syntax = "proto3";
定义service方法
通过关键字service
在proto
文件中定义服务,然后在服务中定义方法,包含请求和返回的类型。grpc允许定义四种服务方法:
-
一个简单的RPC,客户端使用存根(
stub
)向服务器发送请求,并等待响应返回,就像普通的函数调用一样。rpc GetFeature(Point) returns (Feature) {}
-
响应流RPC,客户端向服务器发送请求,并获取一个流来读取返回的消息序列。客户端从流中读取数据,直到没有消息。定义该方法需要在返回值前面使用
stream
关键字。该结果返回是流,而不是一次返回,可以在返回大量数据时使用(返回大量特征)。
rpc ListFeatures(Rectangle) returns (stream Feature) {}
-
请求流RPC,客户端写入一系列消息并将它们发送到服务器,同样使用提供的流。一旦客户端写完消息,它就会等待服务器读取所有消息并返回响应。你可以在请求类型前面加上
stream
关键字来指定请求流方法。rpc RecordRoute(stream Point) returns (RouteSummary) {}
-
双向流RPC,其中双方使用读写流发送一系列消息。这两个流是独立操作的,所以客户端和服务器可以按照自己喜欢的顺序读取和写入数据:例如,服务器可以等待接收所有客户端消息后再写入响应,或者可以交替读取一个消息,然后写入一个消息,或者其他读写的组合。每个流中的消息顺序被保留。你可以通过在请求和响应之前放置stream关键字来指定这种类型的方法。
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
定义message
.proto
文件还包含service
中的方法使用的所有请求和响应类型的protocol buffer
消息类型定义
// 地理坐标
message Point {
int32 latitude = 1;
int32 longitude = 2;
}
// 方形区域,通过两个点确定一个矩形
message Rectangle{
Point lo = 1;
Point hi = 2;
}
// 特征:名字和地理位置
message Feature{
string name = 1;
Point location = 2;
}
message RouteNote{
Point location = 1;
string message = 2;
}
// 路径统计
message RouteSummary{
int32 point_conut = 1;
int32 Feature_count = 2;
int32 distance = 3;
int32 elapsed_time = 4
}
编译proto
文件,生成对应编程语言的代码
python -m grpc_tools.protoc -I protos --python_out=. --pyi_out=. --grpc_python_out=. ./protos/route_guide.proto
-
-I
:proto文件夹 -
--xxx_out
:生成文件的对应文件夹 -
末尾为要编译的具体
proto
文件
编译之后会生成了名为route_guide_pb2.py
和route_guide_pb2_grpc.py
的代码文件,其包含以下内容:
-
route_guide_pb2
包含route_guide.proto
定义的message
的类 -
route_guide_pb2_grpc.py
文件中包含了两个类和一个方法:-
RouteGuideStub
类该类用于生成一个gRPC客户端,构造函数接收一个channel对象,proto定义的每个方法在该类中会有对应的一个属性(属性名字和方法名字相同)根据gRPC接收和返回的数据类型(单一类型或者stream),给属性赋不同类型的值。
-
RouteGuideServicer
类:对于proto中定义的每个service,都会生成一个相应的类,包含proto定义的rpc方法。这个类一般作为父类被子类继承,并根据业务需求重新定义的方法。
-
add_RouteGuideServicer_to_server
方法:对于每一个
service
,这个方法用于将该service
作为grpc.server
对象登记在RPC服务器中。帮助服务器可以将不同的请求路由到对应的service
中。该方法接收一个继承了RouteGuideServicer
的子类。
-
pb2中的2表示生成的代码遵循Protocol Buffers Python API version 2。版本1已经过时了。
创建gRPC 服务端
创建grpc server
,该项工作分为两步:
- 根据具体功能,实现
proto
文件中的service
中定义的接口 - 启动
grpc
服务,等待客户端请求和传递消息
首先,创建一个类RouteGuideServicer
,并继承route_guide_pb2_grpc
中的RouteGuideServicer
类,然后实现父类中定义的方法:
from concurrent import futures
import time
import json
import math
import sys
import logging
sys.path.append("/home/sysj/GQ/OpenSourceProject/flwithflower/grpc/")
import route_guide_pb2_grpc
import route_guide_pb2
import grpc
# 读取本地坐标数据
def read_route_guide_database():
feature_list = []
with open(r"route_guide_server/route_guide_db.json") as route_guide_db_file:
for item in json.load(route_guide_db_file):
feature = route_guide_pb2.Feature(name=item['name'],
location=route_guide_pb2.Point(
latitude=item["location"]["latitude"],
longitude=item["location"]["longitude"]
))
feature_list.append(feature)
return feature_list
# 根据位置查询本地数据是否包含该地点
def get_feature(feature_list,point):
for feature in feature_list:
print(feature)
if feature.location == point:
return feature
return None
# 计算路径
def get_distance(start, end):
"""Distance between two points."""
coord_factor = 10000000.0
lat_1 = start.latitude / coord_factor
lat_2 = end.latitude / coord_factor
lon_1 = start.longitude / coord_factor
lon_2 = end.longitude / coord_factor
lat_rad_1 = math.radians(lat_1)
lat_rad_2 = math.radians(lat_2)
delta_lat_rad = math.radians(lat_2 - lat_1)
delta_lon_rad = math.radians(lon_2 - lon_1)
# Formula is based on http://mathforum.org/library/drmath/view/51879.html
a = (pow(math.sin(delta_lat_rad / 2), 2) +
(math.cos(lat_rad_1) * math.cos(lat_rad_2) *
pow(math.sin(delta_lon_rad / 2), 2)))
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
R = 6371000
# metres
return R * c
# 继承生成的RouteGuideServicer类,并重写方法
class RouteGuideService(route_guide_pb2_grpc.RouteGuideServicer):
def __init__(self) -> None:
self.db = read_route_guide_database()
def GetFeature(self, request, context):
feature = get_feature(self.db, request)
if feature is None:
return route_guide_pb2.Feature(name="",location=request)
else:
return feature
## 获取一定区域内所有的feature
# 响应为流的方法
def ListFeatures(self, request, context):
left = min(request.lo.longtitude,request.hi.longitude)
right = max(request.lo.longitude,request.hi.longitude)
top = max(request.lo.latitude,request.lo.latitude)
bottom = min(request.lo.latitude,request.lo.latitude)
for feature in self.db:
if (feature.location.longitude >= left and
feature.location.longitude <= right and
feature.location.latitude >= bottom and
feature.location.latitude <= top):
yield feature
## 路径记录
# 请求为流的方法
def RecordRoute(self, request_iterator, context):
point_count = 0
feature_count = 0
distance = 0.0
prev_point = None
start_time = time.time()
for point in request_iterator:
point_count+=1
if get_feature(self.db,point):
feature_count+=1
if prev_point:
distance+=get_distance(prev_point,point)
prev_point = point
elapsed_time = time.time()-start_time
return route_guide_pb2.RouteSummary(
point_conut=point_count,
feature_count=feature_count,
distance=int(distance),
elapsed_time=int(elapsed_time))
# 请求和响应都为流的方法,它被传递一个请求值的迭代器,它本身也是一个响应值的迭代器。
def RouteChat(self, request_iterator, context):
prev_notes = []
# 请求值的迭代器
for new_note in request_iterator:
for pre_note in prev_notes:
if pre_note.location == new_note.location:
# 响应值的迭代器
yield prev_notes
prev_notes.append(new_note)
def server():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
route_guide_pb2_grpc.add_RouteGuideServicer_to_server(RouteGuideService(),server)
server.add_insecure_port('[::]:8080')
server.start()
# print(" 50051 server start......")
server.wait_for_termination()
if __name__=="__main__":
# db = read_route_guide_database()
# print(db)
logging.basicConfig()
server()
当实现了所有RouteGuide
的方法后,就可以启动一个gPRC
服务,然后,客户端可以请求该服务器:
def server():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
route_guide_pb2_grpc.add_RouteGuideServicer_to_server(RouteGuideService(),server)
server.add_insecure_port('[::]:50051')
server.start()
server.wait_for_termination()
server
的start()
方法是非阻塞的。一个新的线程将被实例化来处理请求。调用server.start()
的线程通常在此期间没有其他工作要做。在这种情况下,可以调用server.wait_for_termination()
来阻塞调用线程,直到服务器终止。
创建gRPC客户端
通过实例化route_guide_pb2_grpc
模块中RouteGuideStub
类的对象来创建一个stub
,然后通过该stub
来调用定义的方法。
- 对于返回单个响应的RPC方法(“response-unary”方法),
gRPC
Python同时支持同步(阻塞)和异步(非阻塞)控制流语义。 - 对于响应流RPC方法,调用会立即返回响应值的迭代器。对迭代器的next()方法的调用会阻塞,直到从迭代器得到的响应可用为止
创建gRPC客户端
logging.basicConfig()
# 创建非安全通道,创建一个存根
channel = grpc.insecure_channel('localhost:50051')
stub = route_guide_pb2_grpc.RouteGuideStub(channel)
方法调用
调用返回单个响应的PRC方法
## 调用返回单个响应的方法
point = route_guide_pb2.Point(latitude=409146138, longitude=-746188906)
feature = stub.GetFeature(point)
print(feature)
调用返回流的PRC方法,返回类型为生成器类型
## 调用返回流的方法
# 创建一个范围,返回经纬度在该范围内的所有地点
rectangle = route_guide_pb2.Rectangle(
lo=route_guide_pb2.Point(latitude=400000000, longitude=-750000000),
hi=route_guide_pb2.Point(latitude=420000000, longitude=-730000000))
print("Looking for features between 40, -75 and 42, -73")
features = stub.ListFeatures(rectangle)
# print(features.next()) 通过next()获取值
# print(features.next())
for feature in features: # 直接通过for循环取值
print("Feature called %s at %s" % (feature.name, feature.location))
调用请求为流的RecordRoute
方法,类似于将迭代器传递给本地方法。它可以被同步或异步调用:
## 调用请求参数为流的方法,需要将一个迭代器参数传递给该方法
# 获取feature_list
feature_list = read_route_guide_database()
# 通过yield语法,获取一个iterable对象。
feature_generator = generate_generator(feature_list=feature_list)
route_summary = stub.RecordRoute(feature_generator)
print("Finished trip with %s points " % route_summary.point_conut)
print("Passed %s features " % route_summary.Feature_count)
print("Travelled %s meters " % route_summary.distance)
print("It took %s seconds " % route_summary.elapsed_time)
调用双向流的RouteChat
方法(与服务端一样),结合请求流和响应流的语义:
# 通过yield语法,获取一个iterable对象。
message_generate = generate_messages()
# 传入iterable对象
responses = stub.RouteChat(message_generate)
# 接收
for response in responses:
print("Received message %s at %s" % (response.message, response.location))
参考:
https://grpc.io/docs/languages/python/basics/
https://grpc.io/docs/what-is-grpc/introduction/
https://github.com/grpc/grpc/blob/v1.54.0/examples/python/route_guide/route_guide_server.py