GRPC以及python實現

GRPC簡介

A high-performance, open-source universal RPC framework --官網

rpc通信

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

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.pyhelloworld_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):

參考:

gRPC 官網
gRPC python QuickStart
gRPC python basic tutorial

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • gRPC 是一個高性能、通用的開源RPC框架,基于HTTP/2協議標準和Protobuf序列化協議開發,支持眾多的...
    小波同學閱讀 19,571評論 6 19
  • 1.簡介 在gRPC中,客戶端應用程序可以直接調用不同計算機上的服務器應用程序上的方法,就像它是本地對象一樣,使您...
    第八共同體閱讀 1,938評論 0 6
  • 該篇文章介紹了golang的grpc編程。 通過下面的例子,你將會學到:1. 在一個.proto文件里define...
    曉_7611閱讀 1,399評論 0 3
  • 原文出處:gRPC gRPC分享 概述 gRPC 一開始由 google 開發,是一款語言中立、平臺中立、開源的遠...
    小波同學閱讀 7,316評論 0 18
  • 最近項目需要用到 gRPC,網上gRPC 的資料較少,翻譯了官網的 gRPC guide 文檔,以供組內學習,部分...
    不智魚閱讀 6,153評論 0 13