Python gRPC 入門
gRPC 是什麼?
gRPC 也是基於以下理念:定義一個服務,指定其能夠被遠端呼叫的方法(包含引數和返回型別)。在服務端實現這個介面,並執行一個 gRPC 伺服器來處理客戶端呼叫。在客戶端擁有一個存根能夠像服務端一樣的方法。
在 gRPC 裡客戶端應用可以像呼叫本地物件一樣直接呼叫另一臺不同的機器上服務端應用的方法,使得我們能夠更容易地建立分散式應用和服務。
gRPC 客戶端和服務端可以在多種環境中執行和互動,並且可以用任何 gRPC 支援的語言來編寫。
gRPC 支援 C++ Java Python Go Ruby C# Node.js PHP Dart 等語言
gRPC 預設使用 protocol buffers
安裝 Google Protocol Buffer
方法一(建議使用)
1. 安裝 gRPC
python -m pip install grpcio
# 或者
sudo python -m pip install grpcio
# 在 El Capitan OSX 系統下可能會看到以下報錯
$ OSError: [Errno 1] Operation not permitted: '/tmp/pip-qwTLbI-uninstall/System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python/six-1.4.1-py2.7.egg-info'
# 可以使用以下命令
python -m pip install grpcio --ignore-installed
複製程式碼
2. 安裝 gRPC tools
Python gPRC tools 包含 protocol buffer 編譯器和用於從 .proto
檔案生成服務端和客戶端程式碼的外掛
python -m pip install grpcio-tools
複製程式碼
方法二:
在 github 頁面protobuf Buffers可以下載二進位制原始碼,下載後執行以下命令安裝:
tar -zxvf protobuf-all-3.5.1.tar
cd protobuf-all-3.5.1
./configure
make
make install
>> protoc --version
libprotoc 3.5.1 # 安裝成功
複製程式碼
因為是要使用 Protobuf + Python 測試,所以還要安裝 python執行環境。protobuf Buffers python 文件
# 開啟 python 目錄
cd python
python setup.py install # 安裝 python 執行環境
複製程式碼
Protobuf 基本使用
定義一個訊息型別
先來看一個非常簡單的例子。假設你想定義一個“搜尋請求”的訊息格式,每一個請求含有一個查詢字串、你感興趣的查詢結果所在的頁數,以及每一頁多少條查詢結果。可以採用如下的方式來定義訊息型別的.proto檔案了:
syntax = "proto3"; // 宣告使用 proto3 語法
message SearchRequest {
string query = 1; // 每個欄位都要指定資料型別
int32 page_number = 2; // 這裡的數字2 是識別符號,最小的標識號可以從1開始,最大到2^29 - 1, or 536,870,911。不可以使用其中的[19000-19999]
int32 result_per_page = 3; // 這裡是註釋,使用 //
}
複製程式碼
- 文章的第一行指定了你正在使用 proto3 語法:如果不指定,編譯器會使用 proto2。
這個指定語法必須是檔案的非空非註釋的第一行
。 SearchRequest
訊息格式有三個欄位,在訊息中承載的資料分別對應於每一個欄位。其中每個欄位都有一個名字和一種型別。- 向.proto檔案添加註釋,可以使用C/C++/java風格的
雙斜槓(//)
語法格式。 - 在訊息體中,每個欄位都有唯一的一個數字識別符號。這些識別符號用來在訊息的二進位制格式中識別各個欄位,一旦開始使用就不能再改變。
[1,15]之內的標識號在編碼的時候會佔用一個位元組。[16,2047]之內的標識號則佔用2個位元組。所以應該為那些頻繁出現的訊息元素保留 [1,15]之內的標識號。切記:要為將來有可能新增的、頻繁出現的標識號預留一些標識號。
指定欄位規則
所指定的訊息欄位修飾符必須是如下之一:
-
singular:一個格式良好的訊息應該有0個或者1個這種欄位(但是不能超過1個)。
-
repeated:在一個格式良好的訊息中,這種欄位可以重複任意多次(包括0次)。重複的值的順序會被保留。
在proto3中,repeated的標量域預設情況蝦使用packed。
message Test4 { repeated int32 d = 4 [packed=true]; } 複製程式碼
數值型別
一個標量訊息欄位可以含有一個如下的型別——該表格展示了定義於.proto檔案中的型別,以及與之對應的、在自動生成的訪問類中定義的型別:
.proto Type | Notes | C++ Type | Java Type | Python Type[2] | Go Type | Ruby Type |
---|---|---|---|---|---|---|
double | double | double | float | float64 | Float | |
float | float | float | float | float32 | Float | |
int32 | 使用變長編碼,對於負值的效率很低,如果你的域有可能有負值,請使用sint64替代 | int32 | int | int | int32 | Fixnum 或者 Bignum(根據需要) |
uint32 | 使用變長編碼 | uint32 | int | int/long | uint32 | Fixnum 或者 Bignum(根據需要) |
uint64 | 使用變長編碼 | uint64 | long | int/long | uint64 | Bignum |
sint32 | 使用變長編碼,這些編碼在負值時比int32高效的多 | int32 | int | int | int32 | Fixnum 或者 Bignum(根據需要) |
sint64 | 使用變長編碼,有符號的整型值。編碼時比通常的int64高效。 | int64 | long | int/long | int64 | Bignum |
fixed32 | 總是4個位元組,如果數值總是比總是比228大的話,這個型別會比uint32高效。 | uint32 | int | int | uint32 | Fixnum 或者 Bignum(根據需要) |
fixed64 | 總是8個位元組,如果數值總是比總是比256大的話,這個型別會比uint64高效。 | uint64 | long | int/long | uint64 | Bignum |
sfixed32 | 總是4個位元組 | int32 | int | int | int32 | Fixnum 或者 Bignum(根據需要) |
sfixed64 | 總是8個位元組 | int64 | long | int/long | int64 | Bignum |
bool | bool | boolean | bool | bool | TrueClass/FalseClass | |
string | 一個字串必須是UTF-8編碼或者7-bit ASCII編碼的文字。 | string | String | str/unicode | string | String (UTF-8) |
bytes | 可能包含任意順序的位元組資料。 | string | ByteString | str | []byte | String (ASCII-8BIT) |
預設值
當一個訊息被解析的時候,如果被編碼的資訊不包含一個特定的singular元素,被解析的物件鎖對應的域被設定位一個預設值,對於不同型別指定如下:
-
對於strings,預設是一個空string
-
對於bytes,預設是一個空的bytes
-
對於bools,預設是false
-
對於數值型別,預設是0
-
對於列舉,預設是第一個定義的列舉值,必須為0;
-
對於訊息型別(message),域沒有被設定,確切的訊息是根據語言確定的,詳見generated code guide
對於可重複域的預設值是空(通常情況下是對應語言中空列表)。
巢狀型別
你可以在其他訊息型別中定義、使用訊息型別,在下面的例子中,Result訊息就定義在SearchResponse訊息內,如:
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
複製程式碼
在 message SearchResponse 中,定義了巢狀訊息 Result
,並用來定義SearchResponse
訊息中的results
域。
Protobuf 檔案編譯
從.proto檔案生成了什麼?
當用protocol buffer編譯器來執行.proto檔案時,編譯器將生成所選擇語言的程式碼,這些程式碼可以操作在.proto檔案中定義的訊息型別,包括獲取、設定欄位值,將訊息序列化到一個輸出流中,以及從一個輸入流中解析訊息。
- 對C++來說,編譯器會為每個.proto檔案生成一個.h檔案和一個.cc檔案,.proto檔案中的每一個訊息有一個對應的類。
- 對Java來說,編譯器為每一個訊息型別生成了一個.java檔案,以及一個特殊的Builder類(該類是用來建立訊息類介面的)。
- 對Python來說,有點不太一樣——Python編譯器為.proto檔案中的每個訊息型別生成一個含有靜態描述符的模組,,該模組與一個元類(metaclass)在執行時(runtime)被用來建立所需的Python資料訪問類。
- 對go來說,編譯器會位每個訊息型別生成了一個.pd.go檔案。
- 對於Ruby來說,編譯器會為每個訊息型別生成了一個.rb檔案。
- javaNano來說,編譯器輸出類似域java但是沒有Builder類
- 對於Objective-C來說,編譯器會為每個訊息型別生成了一個pbobjc.h檔案和pbobjcm檔案,.proto檔案中的每一個訊息有一個對應的類。
- 對於C#來說,編譯器會為每個訊息型別生成了一個.cs檔案,.proto檔案中的每一個訊息有一個對應的類。
Python gRPC 示例
編譯
這裡我們用Python 編譯一下,看得到什麼:
// 檔名 hello.proto
syntax = "proto3";
package hello;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (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;
}
複製程式碼
使用以下命令編譯:
python -m grpc_tools.protoc -I./ --python_out=. --grpc_python_out=. ./hello.proto
複製程式碼
生成了兩個檔案:
hello_pb2.py
此檔案包含生成的 request(HelloRequest
) 和 response(HelloReply
) 類。hello_pb2_grpc.py
此檔案包含生成的 客戶端(GreeterStub
)和服務端(GreeterServicer
)的類。
原始碼地址為github.com/grpc/grpc/b…
雖然現在已經生成了服務端和客戶端程式碼,但是我們還需要手動實現以及呼叫的方法。
建立服務端程式碼
建立和執行 Greeter
服務可以分為兩個部分:
-
實現我們服務定義的生成的服務介面:做我們的服務的實際的“工作”的函式。
-
執行一個 gRPC 伺服器,監聽來自客戶端的請求並傳輸服務的響應。
在當前目錄,開啟檔案 greeter_server.py,實現一個新的函式:
from concurrent import futures
import time
import grpc
import hello_pb2
import hello_pb2_grpc
_ONE_DAY_IN_SECONDS = 60 * 60 * 24
class Greeter(hello_pb2_grpc.GreeterServicer):
# 工作函式
def SayHello(self, request, context):
return hello_pb2.HelloReply(message='Hello, %s!' % request.name)
def serve():
# gRPC 伺服器
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
hello_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
server.add_insecure_port('[::]:50051')
server.start() # start() 不會阻塞,如果執行時你的程式碼沒有其它的事情可做,你可能需要迴圈等待。
try:
while True:
time.sleep(_ONE_DAY_IN_SECONDS)
except KeyboardInterrupt:
server.stop(0)
if __name__ == '__main__':
serve()
複製程式碼
更新客戶端程式碼
在當前目錄,開啟檔案 greeter_client.py,實現一個新的函式:
from __future__ import print_function
import grpc
import hello_pb2
import hello_pb2_grpc
def run():
channel = grpc.insecure_channel('localhost:50051')
stub = hello_pb2_grpc.GreeterStub(channel)
response = stub.SayHello(hello_pb2.HelloRequest(name='goodspeed'))
print("Greeter client received: " + response.message)
if __name__ == '__main__':
run()
複製程式碼
對於返回單個應答的 RPC 方法("response-unary" 方法),gRPC Python 同時支援同步(阻塞)和非同步(非阻塞)的控制流語義。對於應答流式 RPC 方法,呼叫會立即返回一個應答值的迭代器。呼叫迭代器的
next()
方法會阻塞,直到從迭代器產生的應答變得可用。
執行程式碼
- 首先執行服務端程式碼
python greeter_server.py
複製程式碼
- 然後執行客戶端程式碼
python greeter_client.py
# output
Greeter client received: Hello, goodspeed!