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

email

=

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: ”

email

=

raw_input

“Enter email address (blank for none): ”

if

email

!=

“”

person

email

=

email

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

print

“Unknown phone type; leaving as default value。”

if

len

sys

argv

!=

2

print

“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

print

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

print

“Person ID:”

person

id

print

“ Name:”

person

name

if

person

HasField

‘email’

):

print

“ E-mail address:”

person

email

for

phone_number

in

person

phones

if

phone_number

type

==

addressbook_pb2

Person

MOBILE

print

“ Mobile phone #: ”

elif

phone_number

type

==

addressbook_pb2

Person

HOME

print

“ Home phone #: ”

elif

phone_number

type

==

addressbook_pb2

Person

WORK

print

“ Work phone #: ”

print

phone_number

number

# Main procedure: Reads the entire address book from a file and prints all

# the information inside。

if

len

sys

argv

!=

2

print

“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伺服器上的應用 。

基於google protobuf的gRPC實現

總結來說,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,裡面內容如下所示,具體含義在後面解釋。

基於google protobuf的gRPC實現

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!’

))

# 序列化資料傳遞過去

print

“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中定義的函式。

基於google protobuf的gRPC實現

事實上,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

(二維碼自動識別)