高質量通信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結構圖。
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){ }
1
2
2.1.2 服務端流式 RPC
客戶端發送一個請求給服務端,可獲取一個數據流用來讀取一系列消息。客戶端從返回的數據流里一直讀取直到沒有更多消息為止。
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){ }
1
2
2.1.3 客戶端流式 RPC
客戶端用提供的一個數據流寫入并發送一系列消息給服務端。一旦客戶端完成消息寫入,就等待服務端讀取這些消息并返回應答。
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) { }
1
2
2.1.4 雙向流式 RPC
兩邊都可以分別通過一個讀寫數據流來發送一系列消息。這兩個數據流操作是相互獨立的,所以客戶端和服務端能按其希望的任意順序讀寫,例如:服務端可以在寫應答前等待所有的客戶端消息,或者它可以先讀一個消息再寫一個消息,或者是讀寫相結合的其他方式。每個數據流里消息的順序會被保持。
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse){ }
1
2
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端代碼
整個工程的目錄如下:
5.1 配置gRPC環境
操作系統不同配置環境的方式也不同,我這次試用win10環境。先講解如何在win10環境下的配置方法。
5.1.1 安裝Protobuf
Ubuntu下執行:
pip install protobuf # 安裝protobuf庫 sudo apt-get install protobuf-compiler # 安裝protobuf編譯器
1
2
Win10下:
登錄官網:地址,選擇win64位的版本。
然后在環境變量,解壓下載好的文件,然后將目錄寫到環境變量。我的在C:\protoc-3.17.3-win64\bin.
然后在CMD中執行:protoc --version。
到這里環境配置完成了,然后在執行:
pip install protobuf # 安裝protobuf庫
1
安裝protobuf的python庫。
5.1.2 安裝gprc的tools庫
pip install grpcio-tools
1
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 message HelloRequest { string name = 1; } // define the data type of response message HelloReply { string message = 1; }
1
2
3
4
5
6
7
8
9
10
11
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
1
生成:helloworld_pb2.py和helloworld_pb2_grpc.py兩個文件。
在rpc_package文件夾下面建個_init_.py文件,修改helloworld_pb2_grpc.py第五行為:
import rpc_package.helloworld_pb2 as helloworld__pb2
1
不然會出現找不到庫的問題。
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()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
#!/usr/bin/env python # -*- coding: utf-8 -*- from __future__ import print_function import logging import grpc from rpc_package.helloworld_pb2 import HelloRequest, HelloReply 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) if __name__ == "__main__": logging.basicConfig() run()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
5.3 運行
先執行server端代碼
python hello_server.py
1
然后執行client端
python hello_client.py
1
運行結果:
hello client received: Hello, AI浩!
1
參考文章:
gRPC詳解 - 簡書 (jianshu.com)
RPC Socket編程
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。