天天看點

高品質通信gRPC入門,有了它,誰還用Socket

1 含義

RPC(remote procedure call 遠端過程調用)架構實際是提供了一套機制,使得應用程式之間可以進行通信,而且也遵從server/client模型。使用的時候用戶端調用server端提供的接口就像是調用本地的函數一樣。

gRPC 是一個高性能、開源和通用的 RPC 架構,面向移動和 HTTP/2 設計。目前提供 C、Java 和 Go 語言版本,分别是:grpc, grpc-java, grpc-go. 其中 C 版本支援 C, C++, Node.js, Python, Ruby, Objective-C, PHP 和 C# 支援.

gRPC 基于 HTTP/2 标準設計,帶來諸如雙向流、流控、頭部壓縮、單 TCP 連接配接上的多複用請求等特。這些特性使得其在移動裝置上表現更好,更省電和節省空間占用。如下圖所示就是一個典型的gRPC結構圖。

高品質通信gRPC入門,有了它,誰還用Socket

2 定義服務

gRPC 基于如下思想:定義一個服務, 指定其可以被遠端調用的方法及其參數和傳回類型。gRPC 預設使用 protocol buffers 作為接口定義語言,來描述服務接口和有效載荷消息結構。如果有需要的話,可以使用其他替代方案。

service HelloService {

 rpc SayHello (HelloRequest) returns (HelloResponse);

}

message HelloRequest {

 required string greeting = 1;

message HelloResponse {

 required string reply = 1;

1

2

3

4

5

6

7

8

9

10

11

2.1 四種定義服務的方法

2.1.1 單項 RPC

用戶端發送一個請求給服務端,從服務端擷取一個應答,就像一次普通的函數調用。

rpc SayHello(HelloRequest) returns (HelloResponse){

2.1.2 服務端流式 RPC

用戶端發送一個請求給服務端,可擷取一個資料流用來讀取一系列消息。用戶端從傳回的資料流裡一直讀取直到沒有更多消息為止。

rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){

2.1.3 用戶端流式 RPC

用戶端用提供的一個資料流寫入并發送一系列消息給服務端。一旦用戶端完成消息寫入,就等待服務端讀取這些消息并傳回應答。

rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {

2.1.4 雙向流式 RPC

兩邊都可以分别通過一個讀寫資料流來發送一系列消息。這兩個資料流操作是互相獨立的,是以用戶端和服務端能按其希望的任意順序讀寫,例如:服務端可以在寫應答前等待所有的用戶端消息,或者它可以先讀一個消息再寫一個消息,或者是讀寫相結合的其他方式。每個資料流裡消息的順序會被保持。

rpc BidiHello(stream HelloRequest) returns (stream HelloResponse){

2.1.5使用 API 接口

gRPC 提供 protocol buffer 編譯插件,能夠從一個服務定義的 .proto 檔案生成用戶端和服務端代碼。通常 gRPC 使用者可以在服務端實作這些API,并從用戶端調用它們。

在服務側,服務端實作服務接口,運作一個 gRPC 伺服器來處理用戶端調用。gRPC 底層架構會解碼傳入的請求,執行服務方法,編碼服務應答。

在客戶側,用戶端有一個存根實作了服務端同樣的方法。用戶端可以在本地存根調用這些方法,用合适的 protocol buffer 消息類型封裝這些參數— gRPC 來負責發送請求給服務端并傳回服務端 protocol buffer 響應。

2.1.6 同步 vs 異步

同步 RPC 調用一直會阻塞直到從服務端獲得一個應答,這與 RPC 希望的抽象最為接近。另一方面網絡内部是異步的,并且在許多場景下能夠在不阻塞目前線程的情況下啟動 RPC 是非常有用的。

在多數語言裡,gRPC 程式設計接口同時支援同步和異步的特點。你可以從每個語言教程和參考文檔裡找到更多内容(很快就會有完整文檔)。

2.2 RPC 生命周期

現在讓我們來仔細了解一下當 gRPC 用戶端調用 gRPC 服務端的方法時到底發生了什麼。我們不究其實作細節,關于實作細節的部分,你可以在我們的特定語言頁面裡找到更為詳盡的内容。

2.2.1 單項 RPC

首先我們來了解一下最簡單的 RPC 形式:用戶端發出單個請求,獲得單個響應。

一旦用戶端通過樁調用一個方法,服務端會得到相關通知 ,通知包括用戶端的中繼資料,方法名,允許的響應期限(如果可以的話)

服務端既可以在任何響應之前直接發送回初始的中繼資料,也可以等待用戶端的請求資訊,到底哪個先發生,取決于具體的應用。

一旦服務端獲得用戶端的請求資訊,就會做所需的任何工作來建立或組裝對應的響應。如果成功的話,這個響應會和包含狀态碼以及可選的狀态資訊等狀态明細及可選的追蹤資訊傳回給用戶端 。

假如狀态是 OK 的話,用戶端會得到應答,這将結束用戶端的調用。

2.2.2 服務端流式 RPC

服務端流式 RPC 除了在得到用戶端請求資訊後發送回一個應答流之外,與我們的簡單例子一樣。在發送完所有應答後,服務端的狀态詳情(狀态碼和可選的狀态資訊)和可選的跟蹤中繼資料被發送回用戶端,以此來完成服務端的工作。用戶端在接收到所有服務端的應答後也完成了工作。

2.2.3 用戶端流式 RPC

用戶端流式 RPC 也基本與我們的簡單例子一樣,差別在于用戶端通過發送一個請求流給服務端,取代了原先發送的單個請求。服務端通常(但并不必須)會在接收到用戶端所有的請求後發送回一個應答,其中附帶有它的狀态詳情和可選的跟蹤資料。

2.2.4 雙向流式 RPC

雙向流式 RPC ,調用由用戶端調用方法來初始化,而服務端則接收到用戶端的中繼資料,方法名和截止時間。服務端可以選擇發送回它的初始中繼資料或等待用戶端發送請求。 下一步怎樣發展取決于應用,因為用戶端和服務端能在任意順序上讀寫 - 這些流的操作是完全獨立的。例如服務端可以一直等直到它接收到所有用戶端的消息才寫應答,或者服務端和用戶端可以像"乒乓球"一樣:服務端後得到一個請求就回送一個應答,接着用戶端根據應答來發送另一個請求,以此類推。

2.2.5 截止時間

gRPC 允許用戶端在調用一個遠端方法前指定一個最後期限值。這個值指定了在用戶端可以等待服務端多長時間來應答,超過這個時間值 RPC 将結束并傳回DEADLINE_EXCEEDED錯誤。在服務端可以查詢這個期限值來看是否一個特定的方法已經過期,或者還剩多長時間來完成這個方法。 各語言來指定一個截止時間的方式是不同的 - 比如在 Python 裡一個截止時間值總是必須的,但并不是所有語言都有一個預設的截止時間。

2.2.6 RPC 終止

在 gRPC 裡,用戶端和服務端對調用成功的判斷是獨立的、本地的,他們的結論可能不一緻。這意味着,比如你有一個 RPC 在服務端成功結束(“我已經傳回了所有應答!”),到那時在用戶端可能是失敗的(“應答在最後期限後才來到!”)。也可能在用戶端把所有請求發送完前,服務端卻判斷調用已經完成了。

2.2.7 取消 RPC

無論用戶端還是服務端均可以再任何時間取消一個 RPC 。一個取消會立即終止 RPC 這樣可以避免更多操作被執行。它不是一個"撤銷", 在取消前已經完成的不會被復原。當然,通過同步調用的 RPC 不能被取消,因為直到 RPC 結束前,程式控制權還沒有交還給應用。

2.2.8中繼資料集

中繼資料是一個特殊 RPC 調用對應的資訊,這些資訊以鍵值對的形式存在,一般鍵的類型是字元串,值的類型一般也是字元串(當然也可以是二進制資料)。中繼資料對 gRPC 本事來說是不透明的 - 它讓用戶端提供調用相關的資訊給服務端,反之亦然。 對于中繼資料的通路是語言相關的。

2.2.9 頻道

在建立用戶端存根時,一個 gRPC 頻道提供一個特定主機和端口服務端的連接配接。用戶端可以通過指定頻道參數來修改 gRPC 的預設行為,比如打開關閉消息壓縮。一個頻道具有狀态,包含已連接配接和空閑 。 gRPC 如何處理關閉頻道是語言相關的。有些語言可允許詢問頻道狀态。

3 gRPC的優勢:

gRPC可以通過protobuf來定義接口,進而可以有更加嚴格的接口限制條件。關于protobuf可以參見筆者之前的小文Google Protobuf簡明教程

另外,通過protobuf可以将資料序列化為二進制編碼,這會大幅減少需要傳輸的資料量,進而大幅提高性能。

gRPC可以友善地支援流式通信(理論上通過http2.0就可以使用streaming模式, 但是通常web服務的restful api似乎很少這麼用,通常的流式資料應用如視訊流,一般都會使用專門的協定如HLS,RTMP等,這些就不是我們通常web服務了,而是有專門的伺服器應用。)

4 使用場景

需要對接口進行嚴格限制的情況,比如我們提供了一個公共的服務,很多人,甚至公司外部的人也可以通路這個服務,這時對于接口我們希望有更加嚴格的限制,我們不希望用戶端給我們傳遞任意的資料,尤其是考慮到安全性的因素,我們通常需要對接口進行更加嚴格的限制。這時gRPC就可以通過protobuf來提供嚴格的接口限制。

對于性能有更高的要求時。有時我們的服務需要傳遞大量的資料,而又希望不影響我們的性能,這個時候也可以考慮gRPC服務,因為通過protobuf我們可以将資料壓縮編碼轉化為二進制格式,通常傳遞的資料量要小得多,而且通過http2我們可以實作異步的請求,進而大大提高了通信效率。

但是,通常我們不會去單獨使用gRPC,而是将gRPC作為一個部件進行使用,這是因為在生産環境,我們面對大并發的情況下,需要使用分布式系統來去處理,而gRPC并沒有提供分布式系統相關的一些必要元件。而且,真正的線上服務還需要提供包括負載均衡,限流熔斷,監控報警,服務注冊和發現等等必要的元件。不過,這就不屬于本篇文章讨論的主題了,我們還是先繼續看下如何使用gRPC。

5 入門執行個體

這次試用python代碼,實作Hello World入門案例。gRPC的使用通常包括如下幾個步驟:

通過protobuf來定義接口和資料類型

編寫gRPC server端代碼

編寫gRPC client端代碼

整個工程的目錄如下:

高品質通信gRPC入門,有了它,誰還用Socket

5.1 配置gRPC環境

作業系統不同配置環境的方式也不同,我這次試用win10環境。先講解如何在win10環境下的配置方法。

5.1.1 安裝Protobuf

Ubuntu下執行:

pip install protobuf    # 安裝protobuf庫

sudo apt-get install protobuf-compiler  # 安裝protobuf編譯器

Win10下:

登入官網:位址,選擇win64位的版本。

高品質通信gRPC入門,有了它,誰還用Socket

然後在環境變量,解壓下載下傳好的檔案,然後将目錄寫到環境變量。我的在C:\protoc-3.17.3-win64\bin.

高品質通信gRPC入門,有了它,誰還用Socket

然後在CMD中執行:protoc --version。

高品質通信gRPC入門,有了它,誰還用Socket

到這裡環境配置完成了,然後在執行:

安裝protobuf的python庫。

5.1.2 安裝gprc的tools庫

pip install grpcio-tools

5.1.3 定義接口

通過protobuf定義接口和資料類型。建立protos檔案夾,進入裡面建立helloworld.proto檔案,添加下面的内容:

syntax = "proto3";

package rpc_package;

// define a service

service HelloWorldService {

   // define the interface and data type

   rpc SayHello (HelloRequest) returns (HelloReply) {}

// define the data type of request

   string name = 1;

// define the data type of response

message HelloReply {

   string message = 1;

12

13

14

15

建立rpc_package檔案夾,然後使用gRPC protobuf生成工具生成對應語言的庫函數。生成的目錄指定到rpc_package檔案夾。

python -m grpc_tools.protoc -I=./protos --python_out=./rpc_package --grpc_python_out=./rpc_package ./protos/helloworld.proto

生成:helloworld_pb2.py和helloworld_pb2_grpc.py兩個檔案。

在rpc_package檔案夾下面建個_init_.py檔案,修改helloworld_pb2_grpc.py第五行為:

import rpc_package.helloworld_pb2 as helloworld__pb2

不然會出現找不到庫的問題。

5.2 Server端代碼和用戶端代碼

hello_server.py

#!/usr/bin/env python

# -*-coding: utf-8 -*-

from concurrent import futures

import grpc

import logging

import time

from rpc_package.helloworld_pb2_grpc import add_HelloWorldServiceServicer_to_server,HelloWorldServiceServicer

from rpc_package.helloworld_pb2 import HelloRequest, HelloReply

class Hello(HelloWorldServiceServicer):

   # 這裡實作我們定義的接口

   def SayHello(self, request, context):

       return HelloReply(message='Hello, %s!' % request.name)

def serve():

   # 這裡通過thread pool來并發處理server的任務

   server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))

   # 将對應的任務處理函數添加到rpc server中

   add_HelloWorldServiceServicer_to_server(Hello(), server)

   # 這裡使用的非安全接口,世界gRPC支援TLS/SSL安全連接配接,以及各種鑒權機制

   server.add_insecure_port('[::]:50000')

   server.start()

   try:

       while True:

           time.sleep(60 * 60 * 24)

   except KeyboardInterrupt:

       server.stop(0)

if __name__ == "__main__":

   logging.basicConfig()

   serve()

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

hello_client.py

# -*- coding: utf-8 -*-

from __future__ import print_function

from rpc_package.helloworld_pb2_grpc import HelloWorldServiceStub

def run():

   # 使用with文法保證channel自動close

   with grpc.insecure_channel('localhost:50000') as channel:

       # 用戶端通過stub來實作rpc通信

       stub = HelloWorldServiceStub(channel)

       # 用戶端必須使用定義好的類型,這裡是HelloRequest類型

       response = stub.SayHello(HelloRequest(name='AI浩'))

   print ("hello client received: " + response.message)

   run()

5.3 運作

先執行server端代碼

python hello_server.py

然後執行client端

python hello_client.py

運作結果:

hello client received: Hello, AI浩!