date: 2018-5-15 22:12:32
title: grpc| python 實戰(zhàn) grpc
description: 只要代碼可以跑起來, 很多難題都會迎刃而解. so, keep coding and stay hungry.
之前用 swoole 寫 server 時就接觸過 protobuf, 本來以為基于 protobuf 的 grpc, 上手起來會輕輕松松, 沒想到結(jié)結(jié)實實的折騰了許久, 從 php 開始配置 grpc 需要的環(huán)境, 到無奈轉(zhuǎn)到 grpc 最親和 的 go 語言, 又無奈面對各種 go get
撞墻, 直到現(xiàn)在使用 python 語言, 終于 絲般順滑 的跑完了官網(wǎng) demo. 代碼運行起來后, 之前 grpc 中不太理解的概念, 終于可以 會心一笑 了.
- grpc 的基礎(chǔ): protobuf
- grpc helloworld: python 實戰(zhàn) grpc 環(huán)境配置
- grpc basic: grpc 4 種通信方式
grpc 的基礎(chǔ): protobuf
grpc 使用 protobuf 進行數(shù)據(jù)傳輸. protobuf 是一種數(shù)據(jù)交換格式, 由三部分組成:
- proto 文件: 使用的 proto 語法的文本文件, 用來定義數(shù)據(jù)格式
proto語法現(xiàn)在有 proto2 和 proto3 兩個版本, 推薦使用 proto3, 更加簡潔明了
// [python quickstart](https://grpc.io/docs/quickstart/python.html#run-a-grpc-application)
// python -m grpc_tools.protoc --python_out=. --grpc_python_out=. -I. helloworld.proto
// helloworld.proto
syntax = "proto3";
service Greeter {
rpc SayHello(HelloRequest) returns (HelloReply) {}
rpc SayHelloAgain(HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
protoc: protobuf 編譯器(compile), 將 proto 文件編譯成不同語言的實現(xiàn), 這樣不同語言中的數(shù)據(jù)就可以和 protobuf 格式的數(shù)據(jù)進行交互
protobuf 運行時(runtime): protobuf 運行時所需要的庫, 和 protoc 編譯生成的代碼進行交互
使用 protobuf 的過程:
編寫 proto 文件 -> 使用 protoc 編譯 -> 添加 protobuf 運行時 -> 項目中集成
更新 protobuf 的過程:
修改 proto 文件 -> 使用 protoc 重新編譯 -> 項目中修改集成的地方
PS: proto3 的語法非常非常的簡單, 上手 protobuf 也很輕松, 反而是配置 protoc 的環(huán)境容易卡住, 所以推薦使用 python 入門, 配置 protoc 這一步非常省心.
grpc helloworld: python 實戰(zhàn) grpc 環(huán)境配置
上面已經(jīng)定義好了 grpc helloworld demo 所需的 proto 文件, 現(xiàn)在來具體看看 python 怎么一步步把 grpc helloworld 的環(huán)境搭建起來:
- protobuf 運行時(runtime)
這一步很簡單, 安裝 grpc 相關(guān)的 python 模塊(module) 即可
pip install grpcio
- 使用 protoc 編譯 proto 文件, 生成 python 語言的實現(xiàn)
# 安裝 python 下的 protoc 編譯器
pip install grpcio-tools
# 編譯 proto 文件
python -m grpc_tools.protoc --python_out=. --grpc_python_out=. -I. helloworld.proto
python -m grpc_tools.protoc: python 下的 protoc 編譯器通過 python 模塊(module) 實現(xiàn), 所以說這一步非常省心
--python_out=. : 編譯生成處理 protobuf 相關(guān)的代碼的路徑, 這里生成到當(dāng)前目錄
--grpc_python_out=. : 編譯生成處理 grpc 相關(guān)的代碼的路徑, 這里生成到當(dāng)前目錄
-I. helloworld.proto : proto 文件的路徑, 這里的 proto 文件在當(dāng)前目錄
編譯后生成的代碼:
-
helloworld_pb2.py
: 用來和 protobuf 數(shù)據(jù)進行交互 -
helloworld_pb2_grpc.py
: 用來和 grpc 進行交互
- 最后一步, 編寫 helloworld 的 grpc 實現(xiàn):
服務(wù)器端: helloworld_grpc_server.py
from concurrent import futures
import time
import grpc
import helloworld_pb2
import helloworld_pb2_grpc
# 實現(xiàn) proto 文件中定義的 GreeterServicer
class Greeter(helloworld_pb2_grpc.GreeterServicer):
# 實現(xiàn) proto 文件中定義的 rpc 調(diào)用
def SayHello(self, request, context):
return helloworld_pb2.HelloReply(message = 'hello {msg}'.format(msg = request.name))
def SayHelloAgain(self, request, context):
return helloworld_pb2.HelloReply(message='hello {msg}'.format(msg = request.name))
def serve():
# 啟動 rpc 服務(wù)
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
helloworld_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
server.add_insecure_port('[::]:50051')
server.start()
try:
while True:
time.sleep(60*60*24) # one day in seconds
except KeyboardInterrupt:
server.stop(0)
if __name__ == '__main__':
serve()
客戶端: helloworld_grpc_client.py
import grpc
import helloworld_pb2
import helloworld_pb2_grpc
def run():
# 連接 rpc 服務(wù)器
channel = grpc.insecure_channel('localhost:50051')
# 調(diào)用 rpc 服務(wù)
stub = helloworld_pb2_grpc.GreeterStub(channel)
response = stub.SayHello(helloworld_pb2.HelloRequest(name='czl'))
print("Greeter client received: " + response.message)
response = stub.SayHelloAgain(helloworld_pb2.HelloRequest(name='daydaygo'))
print("Greeter client received: " + response.message)
if __name__ == '__main__':
run()
運行 python helloworld_grpc_server.py
和 python helloworld_grpc_client.py
, 就可以看到效果了
grpc basic: 4 種通信方式
helloworld 使用了最簡單的 grpc 通信方式: 類似 http 協(xié)議的一次 request+response
.
根據(jù)不同的業(yè)務(wù)場景, grpc 支持 4 種通信方式:
- 客服端一次請求, 服務(wù)器一次應(yīng)答
- 客服端一次請求, 服務(wù)器多次應(yīng)答(流式)
- 客服端多次請求(流式), 服務(wù)器一次應(yīng)答
- 客服端多次請求(流式), 服務(wù)器多次應(yīng)答(流式)
官方提供了一個 route guide service
的 demo, 應(yīng)用到了這 4 種通信方式, 具體的業(yè)務(wù)如下:
- 數(shù)據(jù)源: json 格式的數(shù)據(jù)源, 存儲了很多地點, 每個地點由經(jīng)緯度(point)和地名(location)組成
- 通信方式 1: 客戶端請求一個地點是否在數(shù)據(jù)源中
- 通信方式 2: 客戶端指定一個矩形范圍(矩形的對角點坐標(biāo)), 服務(wù)器返回這個范圍內(nèi)的地點信息
- 通信方式 3: 客戶端給服務(wù)器發(fā)送多個地點信息, 服務(wù)器返回匯總信息(summary)
- 通信方式 4: 客戶端和服務(wù)器使用地點信息 聊天(chat)
對應(yīng)的 proto 文件: routeguide.proto
:
// [python quickstart](https://grpc.io/docs/quickstart/python.html#run-a-grpc-application)
// python -m grpc_tools.protoc --python_out=. --grpc_python_out=. -I. routeguide.proto
syntax = "proto3";
service RouteGuide {
// simple rpc
rpc GetFeature(Point) returns (Feature) {}
// server2client stream rpc
rpc ListFeature(Rectangle) returns (stream Feature) {}
// client2server stream rpc
rpc RecordRoute(stream Point) returns (RouteSummary) {}
// stream rpc
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
}
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_count = 1;
int32 feature_count = 2;
int32 distance = 3;
int32 elapsed_time = 4;
}
proto 中想要表示流式傳輸, 只需要添加 stream
關(guān)鍵字即可
同樣的, 使用 protoc 生成代碼:
python -m grpc_tools.protoc --python_out=. --grpc_python_out=. -I. routeguide.proto
生成了 routeguide_pb2.py
routeguide_pb2_grpc.py
文件, 和上面的 helloworld 對應(yīng)
這里需要增加一個 routeguide_db.py
, 用來處理 demo 中數(shù)據(jù)源(routeguide_db.json
)文件:
import json
import routeguide_pb2
def read_routeguide_db():
feature_list = []
with open('routeguide_db.json') as f:
for item in json.load(f):
feature = routeguide_pb2.Feature(
name = item['name'],
location = routeguide_pb2.Point(
latitude=item['location']['latitude'],
longitude=item['location']['longitude']
)
)
feature_list.append(feature)
return feature_list
處理 json 的過程很簡單, 解析 json 數(shù)據(jù)得到由坐標(biāo)點組成的數(shù)組
好了, 還剩下一個難題: 怎么處理流式數(shù)據(jù)呢?. 答案是 for-in + yield
- 客戶端讀取服務(wù)器發(fā)送的流式數(shù)據(jù)
print("-------------- ListFeatures --------------")
response = stub.ListFeature(routeguide_pb2.Rectangle(
lo = routeguide_pb2.Point(latitude=400000000, longitude=-750000000),
hi=routeguide_pb2.Point(latitude=420000000, longitude=-730000000)
))
for feature in response:
print("Feature called {name} at {location}".format(name=feature.name, location=feature.location))
- 客戶端發(fā)送流式數(shù)據(jù)給服務(wù)器
def generate_route(feature_list):
for _ in range(0, 20):
random_feature = feature_list[random.randint(0, len(feature_list) - 1)]
print("random feature {name} at {location}".format(
name=random_feature.name, location=random_feature.location))
yield random_feature.location
print("-------------- RecordRoute --------------")
feature_list = routeguide_db.read_routeguide_db()
route_iterator = generate_route(feature_list)
response = stub.RecordRoute(route_iterator)
print("point count: {point_count} feature count: {feature_count} distance: {distance} elapsed time:{elapsed_time}".format(
point_count = response.point_count,
feature_count = response.feature_count,
distance = response.distance,
elapsed_time = response.elapsed_time
))
- 完整的服務(wù)器端代碼:
routeguide_grpc_server.py
:
from concurrent import futures
import math
import time
import grpc
import routeguide_pb2
import routeguide_pb2_grpc
import routeguide_db
def get_feature(db, point):
for feature in db:
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
class RouteGuide(routeguide_pb2_grpc.RouteGuideServicer):
def __init__(self):
self.db = routeguide_db.read_routeguide_db()
def GetFeature(self, request, context):
feature = get_feature(self.db, request)
if feature is None:
return routeguide_pb2.Feature(name = '', location = request)
else:
return feature
def ListFeature(self, request, context):
left = min(request.lo.longitude, request.hi.longitude)
right = max(request.lo.longitude, request.hi.longitude)
top = max(request.lo.latitude, request.hi.latitude)
bottom = min(request.lo.latitude, request.hi.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 = 1
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 routeguide_pb2.RouteSummary(
point_count = 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 prev_note in prev_notes:
if prev_note.location == new_note.location:
yield prev_note
prev_notes.append(new_note)
def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
routeguide_pb2_grpc.add_RouteGuideServicer_to_server(RouteGuide(), server)
server.add_insecure_port('[::]:50051')
server.start()
try:
while True:
time.sleep(60*60*24) # one day in seconds
except KeyboardInterrupt:
server.stop(0)
if __name__ == '__main__':
serve()
- 完整的客戶端代碼:
routeguide_grpc_client.py
:
import grpc
import routeguide_pb2
import routeguide_pb2_grpc
import routeguide_db
import random
def get_feature(feature):
if not feature.location:
print("Server returned incomplete feature")
return
if feature.name:
print("Feature called {name} at {location}".format(name = feature.name, location = feature.location))
else:
print("Found no feature at {location}".format(location = feature.location))
def generate_route(feature_list):
for _ in range(0, 20):
random_feature = feature_list[random.randint(0, len(feature_list) - 1)]
print("random feature {name} at {location}".format(
name=random_feature.name, location=random_feature.location))
yield random_feature.location
def make_route_note(message, latitude, longitude):
return routeguide_pb2.RouteNote(
message=message,
location=routeguide_pb2.Point(latitude=latitude, longitude=longitude))
def generate_route_note():
msgs = [
make_route_note('msg 1', 0, 0),
make_route_note('msg 2', 1, 0),
make_route_note('msg 3', 0, 1),
make_route_note('msg 4', 0, 0),
make_route_note('msg 5', 1, 1),
]
for msg in msgs:
print("send message {message} location {location}".format(message = msg.message, location = msg.location))
yield msg
def run():
channel = grpc.insecure_channel('localhost:50051')
stub = routeguide_pb2_grpc.RouteGuideStub(channel)
print("-------------- GetFeature --------------")
response = stub.GetFeature(routeguide_pb2.Point(latitude=409146138, longitude=-746188906))
get_feature(response)
response = stub.GetFeature(routeguide_pb2.Point(latitude=0, longitude=-0))
get_feature(response)
print("-------------- ListFeatures --------------")
response = stub.ListFeature(routeguide_pb2.Rectangle(
lo = routeguide_pb2.Point(latitude=400000000, longitude=-750000000),
hi=routeguide_pb2.Point(latitude=420000000, longitude=-730000000)
))
for feature in response:
print("Feature called {name} at {location}".format(name=feature.name, location=feature.location))
print("-------------- RecordRoute --------------")
feature_list = routeguide_db.read_routeguide_db()
route_iterator = generate_route(feature_list)
response = stub.RecordRoute(route_iterator)
print("point count: {point_count} feature count: {feature_count} distance: {distance} elapsed time:{elapsed_time}".format(
point_count = response.point_count,
feature_count = response.feature_count,
distance = response.distance,
elapsed_time = response.elapsed_time
))
print("-------------- RouteChat --------------")
response = stub.RouteChat(generate_route_note())
for msg in response:
print("recived message {message} location {location}".format(
message=msg.message, location=msg.location))
if __name__ == '__main__':
run()
運行 python routeguide_grpc_server.py
和 python routeguide_grpc_client.py
就可以看到效果
寫在最后
只要代碼可以跑起來, 很多難題都會 迎刃而解
so, keep coding and stay hungry
關(guān)于 protobuf 的更多物料:
- Protobuf3語言指南
- blog - 服務(wù)器開發(fā)系列 1: 完整的 protobuf 業(yè)務(wù)實戰(zhàn)
- blog - devops| 日志服務(wù)實踐: 實戰(zhàn)阿里云日志服務(wù) sdk, 再探 protobuf
關(guān)于 python 實戰(zhàn) grpc 的更多物料: