ProtoBuf 與 gRPC


用 Protobuf 很久了,但是一直覺得很簡單,所以就沒有做一個總結,今天想嘗試一下 gRPC,順帶就一起總結一下。ProtoBuf 是個老同志了,應該是 2010 的時候發布的,然后被廣泛使用,目前在市面上應該和 Facebook 的 thrift 應該是不相上下,無論是性能上,還是用戶的支持度上。

What's ProtoBuf

ProtoBuf 是一種數據表達方式,根據 G 家自己的描述,應該叫做數據交換格式,注意這里使用的是 交換 字眼,也就是說着重於在數據的傳輸上,有別於 TOML 和 XML 較常用於配置(當然 WebService 一套也是用於數據交換)。

在使用 ProtoBuf 之后,很多時候,我都希望能夠用它來替換 json 和 XML,因為相比較於這些工具,ProtoBuf 的優勢比較明顯。例如 json 雖然表達方便,語法清晰,但是,有一個硬傷就是沒有 schema,對於 Client-Server 的應用/服務來說,這就意味着雙方需要使用其他方式進行溝通 schema,否則將無法正確的交流;相比之下,XML 確實提供了強大的 Schema 支持,但是,可能因為年紀更大的緣故,XML 自身的語法啰嗦,更別說定義它的 Schema 了,一句話概括,那就是非常得不現代。

ProtoBuf 結構

ProtoBuf 目前有兩個版本,分別是 proto2 和 proto3,雖然 proto3 看上去比 proto2 新,但是,在一些處理上其實被很多人所詬病,例如默認值和未定義的字段的處理上,proto3 不如 proto2;但是 proto3 確實也修正了 proto2 的很多問題和做了精簡,所以這里我就直接上 proto3 了,不會差別很大,所以喜歡 proto2 的同學也不用糾結。

proto 的語法有點類似於定義一個類或者說結構體更合適一些,因為它沒有方法,只有屬性,一個簡單的示例為:

  1. 第 1 行肯定是需要的,表面一下你用的是哪個版本,這里指明我的語法是 "proto3" 版本的
  2. 第 3 行這里就可以理解成定義了一個結構體,名字叫做:SearchRequest
  3. 第 4 - 6 行就是定義了結構體的屬性了,類型 + 名字,這里后面的 "=1/2/3" 這個先不用關注,保證它不一樣就好了

這樣我們就定義了一個簡單的 ProtoBuf,也就是這么簡單,然后我們應該嘗試一下如何用程序語言來使用這個結構。我這里使用的編程語言是 Go 語言,但是其實用什么語言都不是問題,因為步驟基本一致。

程序操作 ProtoBuf

不同於 json 可以直接被讀取解析,ProtoBuf 因為一些元數據,所以在使用之前,我們需要通過工具生成 Model 類,然后再使用,工具安裝可以參考官方的文檔進行安裝,安裝完成之后我們直接使用命令生成即可:

$ protoc --go_out=. --python_out=. search.proto

這里從參數中可以看到,我生成了兩種語言的 Model,分別是 Python 和 Go 的,類似的,如果你需要其他語言的 Model,可以將語言名字替換試試。

命令完成之后,我們可以在當前目錄看到一些文件了:

然后我們就可以操作使用代碼進行操作了,下面繼續:

我這里就通過這個 Model 新建一個對象,然后將它序列化(Line 7),序列化之后看一下序列化結果的類型,然后再反序列化(Line 15)回來,然后再看看反序列化之后結果是否正常(Line 20 - 22)。如果你的代碼寫得沒問題的話,那么對應的結果應該也是一樣的:

ProtoBuf 節省字節

從上邊的 Demo 里邊你可能看不出 ProtoBuf 有什么特別好的優勢,除了有 Schema 之外,但是,如果你看一下序列化之后的 data 的大小之后,你會發現它才 22 個 uint8,以為這什么,也就是說剛才的數據結構才占用 22 個字節,如果你用 json 和 XML 的話是多少?就拿 json 來說,key 和 value 兩部分,value 的大小就算和 ProtoBuf 的 value 一樣,那應該要 20 個字節,加上 key 超過 20 個字節,算下來至少的 40 個字節,可以認為這里至少省下了一半的空間。你會說這點小錢看不上,但是,你覺得在企業服務中,數據就這么幾個?在大數據的場景下,每天的數據量隨隨便便幾百個 G 甚至幾十 T;在網絡中,節省一半流量代價的下降將至少減少 2 倍以上,內存同理。

既然 ProtoBuf 能這么省字節,那么它是怎么做到的?不知道你還記得不,前面在定義 Message 的時候我讓你先忽略掉的數字:

這個數字可是有大用處的,這里我們是寫着連續的,但是事實上他們可以是不連續的,那么它們的用處是啥?根據官方文檔的介紹,在序列化 ProtoBuf 的時候,它們也是以 Key-Value 的形式壓縮的,但是,它們的 key 不是這里面的字面量,而是后面的數值,也就是說對於 "query" 這個字段,我們保存的不是 "query" 的字符串,而是 1 這個數字,這樣就將我們的壓縮量降低了一大截。

除此之外,對於 value 的處理也是有特別處理的,這里有點類似於 UTF 的處理方式,存在一種稱為 varint 的類型,如果是 0-127 的數字,那么我們可以直接用 1 個字節(最多用了 7 位)表示,如果不夠用了,要表示 128,那么分為兩個字節,不同之處在於,低位的字節要取反碼保存,就拿 128 來說吧:

這里有意思的地方在於 2 和 3 行,在第 2 行,我們可以看到是對低位的字節取反碼,然后在第 3 行,是將高低位轉化,最終成為了第 4 行中的表示。這里我有個疑惑就是,為啥要這么操作,文檔中的說明是這樣可以從左到右搜索字節,如果第一位為 1 則表示這個字節后面還有字節,那么如果我對高位進行取反的話也能得到同樣的效果啊。

我認為,這里除了最高位是 1 表示還存在后續字節之外,將高低字節調轉在解析的時候會方便不少,因為我們可以看到,字節流從左到右進行解析,最先解析的字節應該是最低位的,也就是說如果我們算 128 的話,最先解析的結果是 127 ,然后是 1,加起來就是 128 了。當然,這對於在程序語言中單類型可以表示完全的好像沒什么優勢,但是如果並不能表示完全的就有意義了,例如讓一種不支持 64 位長整數的程序語言處理 uint64 的數值。

gRPC

ProtoBuf 除了經常被用於數據保存交換之外,還被用於定義 gRPC 服務,gRPC 也是 G 家公開的高性能 RPC 調用框架,號稱高效,支持廣(題外話,似乎度娘也開源了一款不錯的 RPC 框架)。

使用 gRPC 的步驟其實還是很簡單的,因為我們只需要做簡單幾步,就將整體的代碼結構創建好了,剩下的工作都是填業務了。無論怎樣,第一步肯定還是先明確一下接口的詳情:

要定義一個接口,除了定義函數原型之外,還需要定義參數和返回值,gRPC 也一樣,這里定義接口的形式和 Go 語言有點類似,語法為:

rpc 函數名 (參數) returns (返回值) {}

參數 和 返回值 都是我們在前面已經很熟悉的 Message 定義。有了這些定義之后,我們可以很簡單得通過剛才的方式生成框架代碼:

$ protoc proto/service.proto --go_out=plugins=grpc:service

隨后我們就可以在 service 目錄下發現生成的 Go 語言代碼,然后我們看到文件:service/proto/service.pb.go,會發現已經生成了我們的函數:

重新根據我們自己的邏輯編輯它即可,但是這僅僅只是一個實現,並不能直接對外提供服務,所以我們還需要編寫一段服務器的代碼,用來驅動這個 service:

然后運行看看:

$ go run main.go

這就是如何搭配 ProtoBuf 和 gRPC 的一個方式。

Reference

  1. Golang Protobuf
  2. ProtoBuf Docs
  3. gRPC Guide


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM