GRPC簡介
A high-performance, open-source universal RPC framework --官網
RPC(remote procedure call 遠程過程調用)
- 是什么:提供一套應用程序之間可以通信的機制,使用C/S模型,Client進程調用Server進程提供的接口就像是調用本地函數一樣。
-
為什么:相比較restful API的優勢:
(1)gRPC都使用http底層傳輸協議建立C/S通信模型,但gRPC使用http2
(2)gRPC可以通過protobuf來定義由嚴格約束條件的接口,在提供公共API服務時,如果不希望client端給我們傳遞亂七八糟的數據,就可以定義輸入的約束。
(3)gRPC通過protobuf將數據轉化為二進制編碼,減少傳輸數據量來提高傳輸性能。在需要服務器傳遞大量數據的場景下效率更高。
(4)gRPC通過http2.0可以方便的使用streaming模式做流式通信(通常的流式數據:視頻流) -
怎么用:grpc官網的一個簡單Python栗子
(1)在.proto文件中定義服務。
(2)使用協議緩沖區編譯器生成服務器和客戶端代碼。
(3)使用Python gRPC API為您的服務編寫一個簡單的客戶端和服務器。 - 安裝:
$ python -m pip install grpcio # 安裝gprc
$ python -m pip install grpcio-tools #安裝grpc-tools
- Python grpc tutorial:The example code for this tutorial is in grpc/grpc/examples/python/route_guide.
QuitStart Python gRPC
下載代碼
$ git clone -b v1.27.0 https://github.com/grpc/grpc
$ cd grpc/examples/python/helloworld
運行gRPC應用:用兩個終端窗口一個運行Server進程,一個運行Client進程
$ python greeter_server.py # 啟動Server
$ python greeter_client.py # 在另外一個terminal啟動client
Server端代碼
"""The Python implementation of the GRPC helloworld.Greeter server."""
from concurrent import futures
import logging
import grpc
import helloworld_pb2
import helloworld_pb2_grpc
class Greeter(helloworld_pb2_grpc.GreeterServicer):
# 自定義對外開放的API接口
def SayHello(self, request, context):
return helloworld_pb2.HelloReply(message='Hello, %s!' % request.name)
def serve():
# 使用ThreadPool并發處理Server任務
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
# 把對應的Greeter任務添加到rpc server中
helloworld_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
# 這里使用的非安全接口,gRPC支持TLS/SSL安全連接,以及各種鑒權機制
server.add_insecure_port('[::]:50051')
server.start()
server.wait_for_termination()
if __name__ == '__main__':
logging.basicConfig()
serve()
Client端代碼
"""The Python implementation of the GRPC helloworld.Greeter client."""
from __future__ import print_function
import logging
import grpc
import helloworld_pb2
import helloworld_pb2_grpc
def run():
# NOTE(gRPC Python Team): .close() is possible on a channel and should be
# used in circumstances in which the with statement does not fit the needs
# of the code.
# 使用with語法保證channel自動close
with grpc.insecure_channel('localhost:50051') as channel:
# 客戶端通過stub來實現rpc通信
stub = helloworld_pb2_grpc.GreeterStub(channel)
# 客戶端必須使用定義好的類型,這里是HelloRequest類型
response = stub.SayHello(helloworld_pb2.HelloRequest(name='you'))
print("Greeter client received: " + response.message)
if __name__ == '__main__':
logging.basicConfig()
run()
修改代碼增添自己的定制API
從proto文件中生成自己的grpc代碼
首先,拷貝examples/protos
文件夾到examples/python/helloworld
文件夾中并修改helloworld.proto
:
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
// Sends another greeting
rpc SayHelloAgain (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;
}
在終端執行下面命令,重新生成helloworld_pb2.py
和helloworld_pb2_grpc.py
$ python -m grpc_tools.protoc -I./protos --python_out=. --grpc_python_out=. ./protos/helloworld.proto
可以通過vimdiff對比一下update前后代碼改動
修改greeter_server.py
增加一個Server函數SayHelloAgain
class Greeter(helloworld_pb2_grpc.GreeterServicer):
# 自定義對外開放的API接口
def SayHello(self, request, context):
return helloworld_pb2.HelloReply(message='Hello, %s!' % request.name)
def SayHelloAgain(self, request, context):
return helloworld_pb2.HelloReply(message='Hello again, %s!' % request.name)
修改greeter_client.py
增加一個client端的調用SayHelloAgain函數的步驟
def run():
# NOTE(gRPC Python Team): .close() is possible on a channel and should be
# used in circumstances in which the with statement does not fit the needs
# of the code.
# 使用with語法保證channel自動close
with grpc.insecure_channel('localhost:50051') as channel:
# 客戶端通過stub來實現rpc通信
stub = helloworld_pb2_grpc.GreeterStub(channel)
# 客戶端必須使用定義好的類型,這里是HelloRequest類型
response = stub.SayHello(helloworld_pb2.HelloRequest(name='you'))
print("Greeter client received: " + response.message)
# new
response = stub.SayHelloAgain(helloworld_pb2.HelloRequest(name='you'))
print("Greeter client received: " + response.message)
重新在不同的終端運行Server和Client:
$ python greeter_server.py # 啟動Server
$ python greeter_client.py # 在另外一個terminal啟動client
Proto定義服務
- 可以在
examples/protos/route_guide.proto
中看到完整的.proto
文件 - 定義Service
service RouteGuide {
// (Method definitions not shown)
}
- 然后,在服務定義中定義rpc方法,并指定它們的請求和響應類型。gRPC可以定義四種服務方法,所有這些方法都在RouteGuide服務中看到:
- (1)simple RPC :一種簡單的RPC,客戶端使用Stub將請求發送到服務器,然后等待響應返回,就像普通的函數調用一樣。
// Obtains the feature at a given position.
rpc GetFeature(Point) returns (Feature) {}
- (2)response-streaming RPC:響應流式RPC,客戶端向服務器發送請求,并獲取 流 以讀取回一系列消息。客戶端從返回的流中讀取,直到沒有更多消息為止。如示例所示,您可以通過將stream關鍵字放在響應類型之前來指定響應流方法。
// Obtains the Features available within the given Rectangle. Results are
// streamed rather than returned at once (e.g. in a response message with a
// repeated field), as the rectangle may cover a large area and contain a
// huge number of features.
rpc ListFeatures(Rectangle) returns (stream Feature) {}
- (3)request-streaming RPC :請求流式RPC,客戶端編寫消息序列,然后再次使用提供的流將消息發送到服務器??蛻舳藢懲晗⒑?,它將等待服務器讀取所有消息并返回其響應。您可以通過將stream關鍵字放在請求類型之前來指定請求流方法。
// Accepts a stream of Points on a route being traversed, returning a
// RouteSummary when traversal is completed.
rpc RecordRoute(stream Point) returns (RouteSummary) {}
*(4)bidirectionally-streaming RPC:雙向流式RPC,雙方都使用讀寫流發送一系列消息。這兩個流是獨立運行的,因此客戶端和服務器可以按照自己喜歡的順序進行讀寫:例如,服務器可以在寫響應之前等待接收所有客戶端消息,或者可以先讀取一條消息再寫入一條消息,或其他一些讀寫組合。每個流中的消息順序都會保留。您可以通過在請求和響應之前都放置stream關鍵字來指定這種類型的方法。
// Accepts a stream of RouteNotes sent while a route is being traversed,
// while receiving other RouteNotes (e.g. from other users).
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
-
.proto
文件還包含我們服務方法中使用的所有請求和響應類型的協議緩沖區消息類型定義-例如,這是Point消息類型:
// Points are represented as latitude-longitude pairs in the E7 representation
// (degrees multiplied by 10**7 and rounded to the nearest integer).
// Latitudes should be in the range +/- 90 degrees and longitude should be in
// the range +/- 180 degrees (inclusive).
message Point {
int32 latitude = 1;
int32 longitude = 2;
}
- 舉個栗子:
route_guide_server.py
里的class RouteGuideServicer
且數據庫里的數據如下:每個地點都有name和location,其中location里包含經度和緯度:
{
"location": {
"latitude": 407838351,
"longitude": -746143763
},
"name": "Patriots Path, Mendham, NJ 07945, USA"
}
- (1)simple RPC:簡單的RPC。
# request:一個location的經度和緯度
# response:是location,包含name
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
- (2)response-streaming RPC:響應流式RPC。
# request:一個地區的四個邊的經度緯度
# response:落在這個地區的所有點,按照yield的方式挨個返回,流式返回
def ListFeatures(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
- (3)request-streaming RPC:請求流式RPC。
# request:是一個迭代器iterator,包含一系列location
# response: 篩選所有在數據庫里的點,返回點的個數,數據庫內點的個數,按順序走完所有location的距離長度
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_count=point_count,
feature_count=feature_count,
distance=int(distance),
elapsed_time=int(elapsed_time))
- (4)bidirectionally-streaming RPC:雙向流式RPC。
# request: 是一個迭代器iterator,包含一系列location
# response: 根據每個location,找到location集里一樣的點
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)
啟動Server
完成了RouteGuide
的所有API,就可以啟動Server了,另外start()
不會阻塞,所以如果在服務期間代碼沒有其他事情要做,server則可能需要進入睡眠循環。
def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
route_guide_pb2_grpc.add_RouteGuideServicer_to_server(
RouteGuideServicer(), server)
server.add_insecure_port('[::]:50051')
server.start()
創建Client
參考完整的Client代碼:
examples/python/route_guide/route_guide_client.py.
- 首先,創建一個Stub:
channel = grpc.insecure_channel('localhost:50051')
stub = route_guide_pb2_grpc.RouteGuideStub(channel)
-
然后,Call service methods:
對于返回單個響應的RPC方法(“response-unary”方法),gRPC Python支持同步(阻塞)和異步(非阻塞)控制流語義。對于響應流式RPC方法,調用立即返回響應值的迭代器。調用該迭代器的next()
方法塊,直到要從該迭代器產生的響應變為可用為止。 - (1)Simple RPC:簡單RPC。GetFeature的同步調用幾乎與調用本地方法一樣簡單。RPC調用等待服務器響應,并且將返回響應或引發異常;GetFeature的異步調用與此類似,但是就像在線程池中異步調用本地方法一樣:
# 同步調用
feature = stub.GetFeature(point)
# 異步調用
feature_future = stub.GetFeature.future(point)
feature = feature_future.result()
- (2)Response-streaming RPC:響應流RPC。調用響應流ListFeatures類似于使用序列類型:
for feature in stub.ListFeatures(rectangle):
- (3)Request-streaming RPC:請求流式RPC。調用請求流式RecordRoute類似于將迭代器傳遞給本地方法。就像上面的簡單RPC也返回單個響應一樣,可以同步或異步調用它:
# 同步
route_summary = stub.RecordRoute(point_iterator)
# 異步
route_summary_future = stub.RecordRoute.future(point_iterator)
route_summary = route_summary_future.result()
- (4)Bidirectional streaming RPC:雙向流RPC。調用雙向流RouteChat具有服務流和響應流語義的組合(在服務端就是這種情況):
for received_route_note in stub.RouteChat(sent_route_note_iterator):