基於google protobuf的gRPC實現
1。Protobuf簡介
Protobuf(Google Protocol Buffers)
提供一種靈活、高效、自動化的機制,用於序列化結構資料。Protobuf僅需自定義一次所需要的資料格式,然後我們就可以使用Protobuf編譯器自動生成各種語言的原始碼,方便我們讀寫自定義的格式化資料。另外Protobuf的使用與平臺和語言無關,可以在不破壞原資料格式的基礎上,擴充套件新的資料。
我們可以將Protobuf與XML進行對比,但Protobuf更小、更快、更加簡單。總結來說具有一下特點:
效能好、效率高。Protobuf作用與XML、json類似,但它是二進位制格式,所以效能更好。但同時因為是二進位制格式,所以缺點也就是可讀性差。
程式碼生成機制,易於使用。
解析速度快。
支援多種語言,例C++、C#、Go、Java、Python等。
向前相容,向後相容。
2。Protobuf安裝
Mac使用者可以使用brew進行安裝,命令如下所示。
brew install protobuf
如需要安裝特定版本,可以先進行搜尋有哪些版本,命令如下所示。搜尋完成之後,採用上述brew安裝方法,安裝特定版本即可。
brew search protobuf
安裝完成之後,可以透過protoc ——version檢視是否安裝成功。
protoc ——version libprotoc 3。6。0
另外可以透過which protoc命令檢視protoc安裝所在的位置。
which protoc /usr/local/bin/protoc
3。Protobuf例項
3。1編譯。proto檔案
首先我們需要建立一個以
.proto
結尾的檔案,可以在其中定義
message
來指定所需要序列化的資料格式。每一個message都是一個小的資訊邏輯單元,包含一系列的name-value值對。以官網上的示例,我們建立一個addressbook。proto檔案,內容如下所示。
syntax
=
“proto2”
;
package
tutorial
;
message
Person
{
required
string
name
=
1
;
required
int32
id
=
2
;
optional
string
=
3
;
enum
PhoneType
{
MOBILE
=
0
;
HOME
=
1
;
WORK
=
2
;
}
message
PhoneNumber
{
required
string
number
=
1
;
optional
PhoneType
type
=
2
[
default
=
HOME
];
}
repeated
PhoneNumber
phones
=
4
;
}
message
AddressBook
{
repeated
Person
people
=
1
;
}
syntax=”proto2”
代表版本,目前支援proto2和proto3,不寫預設proto2。
package
類似於C++中的namespace概念。
message
是包含了各種型別欄位的聚集,相當於struct,並且可以巢狀。
proto3版本去掉了required和optional型別,保留了repeated(陣列)。其中“=1”,“=2”表示每個元素的標識號,它會用在二進位制編碼中對域的標識,[1,15]之內的標誌符在使用時佔用一個位元組,[16,2047]之內的標識號則佔用2個位元組,所以從最最佳化角度考慮,可以將[1,15]使用在一些較常用或repeated的元素上。同時為了考慮將來可能會增加新的標誌符,我們要事先預留一些標誌符。
構建好addressbook。proto檔案後,執行Protobuf編譯器編譯。proto檔案,執行方法如下所示。其中-I表示。protoc所在的路徑,——python_out表示指定生成的目標檔案存在的路徑,最後的引數表示要編譯的。proto檔案。
protoc -I=\$SRC_DIR ——python_out=\$DST_DIR \$SRC_DIR/addressbook。proto
其中SRC_DIR為目錄,如果處於當前目錄的話,可透過如下所示命令來編譯。proto檔案。
protoc -I=。 ——python_out=。 addressbook。proto
編譯完成之後會生成addressbook_pb2。py檔案,裡面包含序列化和反序列化等方法。
3。2序列化
import
addressbook_pb2
import
sys
def
PromptForAddress
(
person
):
person
。
id
=
int
(
raw_input
(
“Enter person ID number: ”
))
person
。
name
=
raw_input
(
“Enter name: ”
)
=
raw_input
(
“Enter email address (blank for none): ”
)
if
!=
“”
:
person
。
=
while
True
:
number
=
raw_input
(
“Enter a phone number (or leave blank to finish): ”
)
if
number
==
“”
:
break
phone_number
=
person
。
phones
。
add
()
phone_number
。
number
=
number
type
=
raw_input
(
“Is this a mobile, home, or work phone? ”
)
if
type
==
“mobile”
:
phone_number
。
type
=
addressbook_pb2
。
Person
。
MOBILE
elif
type
==
“home”
:
phone_number
。
type
=
addressbook_pb2
。
Person
。
HOME
elif
type
==
“work”
:
phone_number
。
type
=
addressbook_pb2
。
Person
。
WORK
else
:
“Unknown phone type; leaving as default value。”
if
len
(
sys
。
argv
)
!=
2
:
“Usage:”
,
sys
。
argv
[
0
],
“ADDRESS_BOOK_FILE”
sys
。
exit
(
-
1
)
address_book
=
addressbook_pb2
。
AddressBook
()
# Read the existing address book。
try
:
f
=
open
(
sys
。
argv
[
1
],
“rb”
)
address_book
。
ParseFromString
(
f
。
read
())
f
。
close
()
except
IOError
:
sys
。
argv
[
1
]
+
“: Could not open file。 Creating a new one。”
# Add an address。
PromptForAddress
(
address_book
。
people
。
add
())
# Write the new address book back to disk。
f
=
open
(
sys
。
argv
[
1
],
“wb”
)
f
。
write
(
address_book
。
SerializeToString
())
f
。
close
()
建立add_person。py檔案,程式碼如上所示,然後透過SerializeToString()方法來進行序列化addressbook。proto中所定義的資訊。如果想要執行上述程式碼的話,我們首先需要建立一個輸入檔案,例如命名為input。txt,不需輸入值。然後採用
python add_person input。txt
,便可進行序列化所輸入的資料。如果執行
python add_person
的話,不指定輸入檔案,則會報錯。
Enter person ID number: 1001
Enter name: 1001
Enter email address (blank for none): hello@email。com
Enter a phone number (or leave blank to finish): 10010
Is this a mobile, home, or work phone? work
Enter a phone number (or leave blank to finish):
3。3反序列化
#! /usr/bin/python
import
addressbook_pb2
import
sys
# Iterates though all people in the AddressBook and prints info about them。
def
ListPeople
(
address_book
):
for
person
in
address_book
。
people
:
“Person ID:”
,
person
。
id
“ Name:”
,
person
。
name
if
person
。
HasField
(
‘email’
):
“ E-mail address:”
,
person
。
for
phone_number
in
person
。
phones
:
if
phone_number
。
type
==
addressbook_pb2
。
Person
。
MOBILE
:
“ Mobile phone #: ”
,
elif
phone_number
。
type
==
addressbook_pb2
。
Person
。
HOME
:
“ Home phone #: ”
,
elif
phone_number
。
type
==
addressbook_pb2
。
Person
。
WORK
:
“ Work phone #: ”
,
phone_number
。
number
# Main procedure: Reads the entire address book from a file and prints all
# the information inside。
if
len
(
sys
。
argv
)
!=
2
:
“Usage:”
,
sys
。
argv
[
0
],
“ADDRESS_BOOK_FILE”
sys
。
exit
(
-
1
)
address_book
=
addressbook_pb2
。
AddressBook
()
# Read the existing address book。
f
=
open
(
sys
。
argv
[
1
],
“rb”
)
address_book
。
ParseFromString
(
f
。
read
())
f
。
close
()
ListPeople
(
address_book
)
建立list_person。py檔案來進行反序列化,程式碼如上所示。透過
python list_person。py input。txt
命令來執行上述程式碼,輸出結果如下所示。
Person ID: 1001
Name: 1001
E-mail address: hello@email。com
Work phone #: 10010
4。RPC簡介
這裡引用知乎使用者
用心閣
關於
誰能用通俗的語言解釋一下什麼是 RPC 框架?
的問題答案來解釋什麼是RPC。
RPC(Remote Procedure Call)
是指遠端過程呼叫,也就是說兩臺伺服器A、B,一個應用部署在A伺服器上,想要呼叫B伺服器上應用提供的函式/方法,由於不在一個記憶體空間上,不能直接呼叫,需要透過網路來表達呼叫的語義和傳達呼叫的資料。如果需要實現RPC,那麼需要解決如下幾個問題。
通訊:主要是透過在客戶端和伺服器之間建立TCP連線,遠端過程呼叫的所有交換的資料都在這個連線裡傳輸。連線可以是按需連線,呼叫結束後就斷掉,也可以是長連線,多個遠端過程呼叫共享同一個連線。
定址:A伺服器上的應用怎麼告訴底層的RPC框架,如何連線到B伺服器(如主機或IP地址)以及特定的埠,方法的名稱名稱是什麼。
序列化:當A伺服器上的應用發起遠端過程呼叫時,方法的引數需要透過底層的網路協議,如TCP傳遞到B伺服器。由於網路協議是基於二進位制的,記憶體中的引數值要序列化成二進位制的形式,也就是序列化(Serialize)或編組(marshal),透過定址和傳輸將序列化的二進位制傳送給B伺服器。 B伺服器收到請求後,需要對引數進行反序列化,恢復為記憶體中的表達方式,然後找到對應的方法進行本地呼叫,然後得到返回值。 返回值還要傳送回伺服器A上的應用,也要經過序列化的方式傳送,伺服器A接到後,再反序列化,恢復為記憶體中的表達方式,交給A伺服器上的應用 。
總結來說,RPC提供一種透明呼叫機制讓使用者不必顯示區分本地呼叫還是遠端呼叫。如上圖所示,客戶方像呼叫本地方法一樣去呼叫遠端介面方法,RPC 框架提供介面的代理實現,實際的呼叫將委託給代理
RpcProxy
。代理封裝呼叫資訊並將呼叫轉交給
RpcInvoker
去實際執行。在客戶端的
RpcInvoker
透過聯結器
RpcConnector
去維持與服務端的通道
RpcChannel
,並使用
RpcProtocol
執行協議編碼(encode)並將編碼後的請求訊息透過通道傳送給服務方。RPC 服務端接收器
RpcAcceptor
接收客戶端的呼叫請求,同樣使用
RpcProtocol
執行協議解碼(decode)。解碼後的呼叫資訊傳遞給
RpcProcessor
去控制處理呼叫過程,最後再委託呼叫給
RpcInvoker
去實際執行並返回呼叫結果。
5。基於google protobuf的gRPC實現
我們可以利用protobuf實現序列化和反序列化,但如何實現RPC通訊呢。為簡單起見,我們先介紹gRPC,gRPC是google構建的RPC框架,這樣我們就不再考慮如何寫通訊方法。
5。1gRPC安裝
首先安裝gRPC,安裝命令如下所示。
pip install grpcio
然後安裝protobuf相關的依賴庫。
pip install protobuf
然後安裝python gRPC相關的protobuf相關檔案。
pip install grpcio-tools
5。2gRPC例項
建立三個資料夾,名稱為example、server、client,裡面內容如下所示,具體含義在後面解釋。
5。2。1 example
example主要用於編寫。proto檔案並生成data介面,其中__init__。py的作用是方便其他資料夾引用example資料夾中檔案,data。proto檔案內容如下所示。
syntax
=
“proto3”
;
package
example
;
message
Data
{
string
text
=
1
;
}
service
FormatData
{
rpc
DoFormat
(
Data
)
returns
(
Data
)
{}
}
然後在example目錄下利用下述命令生成data_pb2。py和data_pb2_grpc。py檔案。data_pb2。py用於序列化資訊,data_pb2_grpc。py用於通訊。
python -m grpc_tools。protoc -I。 ——python_out=。 ——grpc_python_out=。 。/data。proto
5。2。2 server
server為伺服器端,server。py實現接受客戶端傳送的資料,並對資料進行處理後返回給客戶端。FormatData的作用是將伺服器端傳過來的資料轉換為大寫,具體含義見相關程式碼和註釋。
#! /usr/bin/env python
# -*- coding: utf-8 -*-
import
grpc
import
time
from
concurrent
import
futures
#具有執行緒池和程序池、管理並行程式設計任務、處理非確定性的執行流程、程序/執行緒同步等功能
from
example
import
data_pb2
from
example
import
data_pb2_grpc
_ONE_DAY_IN_SECONDS
=
60
*
60
*
24
_HOST
=
‘localhost’
_PORT
=
‘8080’
class
FormatData
(
data_pb2_grpc
。
FormatDataServicer
):
def
DoFormat
(
self
,
request
,
context
):
str
=
request
。
text
return
data_pb2
。
Data
(
text
=
str
。
upper
())
def
serve
():
grpcServer
=
grpc
。
server
(
futures
。
ThreadPoolExecutor
(
max_workers
=
4
))
#最多有多少work並行執行任務
data_pb2_grpc
。
add_FormatDataServicer_to_server
(
FormatData
(),
grpcServer
)
# 新增函式方法和伺服器,伺服器端會進行反序列化。
grpcServer
。
add_insecure_port
(
_HOST
+
‘:’
+
_PORT
)
#建立伺服器和埠
grpcServer
。
start
()
# 啟動服務端
try
:
while
True
:
time
。
sleep
(
_ONE_DAY_IN_SECONDS
)
except
KeyboardInterrupt
:
grpcServer
。
stop
(
0
)
if
__name__
==
‘__main__’
:
serve
()
5。2。3 client
clinet為客戶端,client。py實現客戶端傳送資料,並接受server處理後返回的資料,具體含義見相關程式碼和註釋。
#! /usr/bin/env python
# -*- coding: utf-8 -*-
import
grpc
from
example
import
data_pb2
,
data_pb2_grpc
_HOST
=
‘localhost’
_PORT
=
‘8080’
def
run
():
conn
=
grpc
。
insecure_channel
(
_HOST
+
‘:’
+
_PORT
)
# 伺服器資訊
client
=
data_pb2_grpc
。
FormatDataStub
(
channel
=
conn
)
#客戶端建立連線
for
i
in
range
(
0
,
5
):
respnse
=
client
。
DoFormat
(
data_pb2
。
Data
(
text
=
‘hello,world!’
))
# 序列化資料傳遞過去
(
“received: ”
+
respnse
。
text
)
if
__name__
==
‘__main__’
:
run
()
接下來執行server。py來啟動伺服器,然後執行client。py便可以得到結果,可以看到所有資料均已大寫。最後需要關閉伺服器端,否則一直會處於執行狀態。
received: HELLO,WORLD!
received: HELLO,WORLD!
received: HELLO,WORLD!
received: HELLO,WORLD!
received: HELLO,WORLD!
6。基於google protobuf的RPC實現
因為RPC需要我們實現通訊,所以會有一定難度,程式碼量很大程度上也有增加,不方便在文中展現出來。所以我把程式碼放到了github上面,地址在
https://
github。com/weizhixiaoyi
/google-protobuf-service
,有興趣的可以看下。
總的來說,protobuf RPC定義了一個抽象的RPC框架,RpcServiceStub和RpcService類是protobuf編譯器根據proto定義生成的類,RpcService定義了服務端暴露給客戶端的函式介面,具體實現需要使用者自己繼承這個類來實現。RpcServiceStub定義了服務端暴露函式的描述,並將客戶端對RpcServiceStub中函式的呼叫統一轉換到呼叫RpcChannel中的CallMethod方法,CallMethod透過RpcServiceStub傳過來的函式描述符和函式引數對該次rpc呼叫進行encode,最終透過RpcConnecor傳送給服務方。對方以客戶端相反的過程最終呼叫RpcSerivice中定義的函式。
事實上,protobuf rpc的框架只是RpcChannel中定義了空的CallMethod,所以具體怎樣進行encode和呼叫RpcConnector都要自己實現。RpcConnector在protobuf中沒有定義,所以這個完成由使用者自己實現,它的作用就是收發rpc訊息包。在服務端,RpcChannel透過呼叫RpcService中的CallMethod來具體呼叫RpcService中暴露給客戶端的函式。
參考
用心閣-誰能用通俗的語言解釋一下什麼是 RPC 框架?
在於思考-python透過protobuf實現rpc
7。推廣
更多內容請關注公眾號
謂之小一
,若有疑問可在公眾號後臺提問,隨時回答,歡迎關注,內容轉載請註明出處。
http://
weixin。qq。com/r/Ty_4oDP
E2W6mrXfD93pd
(二維碼自動識別)