您好,登錄后才能下訂單哦!
這篇文章主要講解了“go語言怎么用Protobuf做數據交換”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“go語言怎么用Protobuf做數據交換”吧!
協議緩沖區(Protobufs)像 XML 和 JSON 一樣,可以讓用不同語言編寫并在不同平臺上運行的應用程序交換數據。例如,用 Go 編寫的發送程序可以在 Protobuf 中對以 Go 表示的銷售訂單數據進行編碼,然后用 Java 編寫的接收方可以對它進行解碼,以獲取所接收訂單數據的 Java 表示方式。
與 XML 和 JSON 相比,Protobuf 編碼是二進制而不是文本,這會使調試復雜化。但是,正如本文中的代碼示例所確認的那樣,Protobuf 編碼在大小上比 XML 或 JSON 編碼要有效得多。
Protobuf 以另一種方式提供了這種有效性。在實現級別,Protobuf 和其他編碼系統對結構化數據進行序列化和反序列化。序列化將特定語言的數據結構轉換為字節流,反序列化是將字節流轉換回特定語言的數據結構的逆運算。序列化和反序列化可能成為數據交換的瓶頸,因為這些操作會占用大量 CPU。高效的序列化和反序列化是 Protobuf 的另一個設計目標。
最近的編碼技術,例如 Protobuf 和 FlatBuffers,源自 1990 年代初期的 DCE/RPC(分布式計算環境/遠程過程調用)計劃。與 DCE/RPC 一樣,Protobuf 在數據交換中為 IDL(接口定義語言)和編碼層做出了貢獻。
本文將著眼于這兩層,然后提供 Go 和 Java 中的代碼示例以充實 Protobuf 的細節,并表明 Protobuf 是易于使用的。
像 Protobuf 一樣,DCE/RPC 被設計為與語言和平臺無關。適當的庫和實用程序允許任何語言和平臺用于 DCE/RPC 領域。此外,DCE/RPC 體系結構非常優雅。IDL 文檔是一側的遠程過程與另一側的調用者之間的協定。Protobuf 也是以 IDL 文檔為中心的。
IDL 文檔是文本,在 DCE/RPC 中,使用基本 C 語法以及元數據的語法擴展(方括號)和一些新的關鍵字,例如 interface
。這是一個例子:
[uuid (2d6ead46-05e3-11ca-7dd1-426909beabcd), version(1.0)]interface echo { const long int ECHO_SIZE = 512; void echo( [in] handle_t h, [in, string] idl_char from_client[ ], [out, string] idl_char from_service[ECHO_SIZE] );}
該 IDL 文檔聲明了一個名為 echo
的過程,該過程帶有三個參數:類型為 handle_t
(實現指針)和 idl_char
(ASCII 字符數組)的 [in]
參數被傳遞給遠程過程,而 [out]
參數(也是一個字符串)從該過程中傳回。在此示例中,echo
過程不會顯式返回值(echo
左側的 void
),但也可以返回值。返回值,以及一個或多個 [out]
參數,允許遠程過程任意返回許多值。下一節將介紹 Protobuf IDL,它的語法不同,但同樣用作數據交換中的協定。
DCE/RPC 和 Protobuf 中的 IDL 文檔是創建用于交換數據的基礎結構代碼的實用程序的輸入:
IDL 文檔 —> DCE/PRC 或 Protobuf 實用程序 —> 數據交換的支持代碼
作為相對簡單的文本,IDL 是同樣便于人類閱讀的關于數據交換細節的文檔(特別是交換的數據項的數量和每個項的數據類型)。
Protobuf 可用于現代 RPC 系統,例如 gRPC;但是 Protobuf 本身僅提供 IDL 層和編碼層,用于從發送者傳遞到接收者的消息。與原本的 DCE/RPC 一樣,Protobuf 編碼是二進制的,但效率更高。
目前,XML 和 JSON 編碼仍在通過 Web 服務等技術進行的數據交換中占主導地位,這些技術利用 Web 服務器、傳輸協議(例如 TCP、HTTP)以及標準庫和實用程序等原有的基礎設施來處理 XML 和 JSON 文檔。 此外,各種類型的數據庫系統可以存儲 XML 和 JSON 文檔,甚至舊式關系型系統也可以輕松生成查詢結果的 XML 編碼。現在,每種通用編程語言都具有支持 XML 和 JSON 的庫。那么,是什么讓我們回到 Protobuf 之類的二進制編碼系統呢?
讓我們看一下負十進制值 -128
。以 2 的補碼二進制表示形式(在系統和語言中占主導地位)中,此值可以存儲在單個 8 位字節中:10000000
。此整數值在 XML 或 JSON 中的文本編碼需要多個字節。例如,UTF-8 編碼需要四個字節的字符串,即 -128
,即每個字符一個字節(十六進制,值為 0x2d
、0x31
、0x32
和 0x38
)。XML 和 JSON 還添加了標記字符,例如尖括號和大括號。有關 Protobuf 編碼的詳細信息下面就會介紹,但現在的關注點是一個通用點:文本編碼的壓縮性明顯低于二進制編碼。
我的代碼示例著重于 Protobuf 而不是 RPC。以下是第一個示例的概述:
名為 dataitem.proto
的 IDL 文件定義了一個 Protobuf 消息,它具有六個不同類型的字段:具有不同范圍的整數值、固定大小的浮點值以及兩個不同長度的字符串。
Protobuf 編譯器使用 IDL 文件生成 Go 版本(以及后面的 Java 版本)的 Protobuf 消息及支持函數。
Go 應用程序使用隨機生成的值填充原生的 Go 數據結構,然后將結果序列化為本地文件。為了進行比較, XML 和 JSON 編碼也被序列化為本地文件。
作為測試,Go 應用程序通過反序列化 Protobuf 文件的內容來重建其原生數據結構的實例。
作為語言中立性測試,Java 應用程序還會對 Protobuf 文件的內容進行反序列化以獲取原生數據結構的實例。
我的網站上提供了該 IDL 文件以及兩個 Go 和一個 Java 源文件,打包為 ZIP 文件。
最重要的 Protobuf IDL 文檔如下所示。該文檔存儲在文件 dataitem.proto
中,并具有常規的.proto
擴展名。
syntax = "proto3"; package main; message DataItem { int64 oddA = 1; int64 evenA = 2; int32 oddB = 3; int32 evenB = 4; float small = 5; float big = 6; string short = 7; string long = 8;}
該 IDL 使用當前的 proto3 而不是較早的 proto2 語法。軟件包名稱(在本例中為 main
)是可選的,但是慣例使用它以避免名稱沖突。這個結構化的消息包含八個字段,每個字段都有一個 Protobuf 數據類型(例如,int64
、string
)、名稱(例如,oddA
、short
)和一個等號 =
之后的數字標簽(即鍵)。標簽(在此示例中為 1 到 8)是唯一的整數標識符,用于確定字段序列化的順序。
Protobuf 消息可以嵌套到任意級別,而一個消息可以是另外一個消息的字段類型。這是一個使用 DataItem
消息作為字段類型的示例:
message DataItems { repeated DataItem item = 1;}
單個 DataItems
消息由重復的(零個或多個)DataItem
消息組成。
為了清晰起見,Protobuf 還支持枚舉類型:
enum PartnershipStatus { reserved "FREE", "CONSTRAINED", "OTHER";}
reserved
限定符確保用于實現這三個符號名的數值不能重復使用。
為了生成一個或多個聲明 Protobuf 消息結構的特定于語言的版本,包含這些結構的 IDL 文件被傳遞到protoc
編譯器(可在 Protobuf GitHub 存儲庫中找到)。對于 Go 代碼,可以以通常的方式安裝支持的 Protobuf 庫(這里以 %
作為命令行提示符):
% go get github.com/golang/protobuf/proto
將 Protobuf IDL 文件 dataitem.proto
編譯為 Go 源代碼的命令是:
% protoc --go_out=. dataitem.proto
標志 --go_out
指示編譯器生成 Go 源代碼。其他語言也有類似的標志。在這種情況下,結果是一個名為 dataitem.pb.go
的文件,該文件足夠小,可以將其基本內容復制到 Go 應用程序中。以下是生成的代碼的主要部分:
var _ = proto.Marshal type DataItem struct { OddA int64 `protobuf:"varint,1,opt,name=oddA" json:"oddA,omitempty"` EvenA int64 `protobuf:"varint,2,opt,name=evenA" json:"evenA,omitempty"` OddB int32 `protobuf:"varint,3,opt,name=oddB" json:"oddB,omitempty"` EvenB int32 `protobuf:"varint,4,opt,name=evenB" json:"evenB,omitempty"` Small float32 `protobuf:"fixed32,5,opt,name=small" json:"small,omitempty"` Big float32 `protobuf:"fixed32,6,opt,name=big" json:"big,omitempty"` Short string `protobuf:"bytes,7,opt,name=short" json:"short,omitempty"` Long string `protobuf:"bytes,8,opt,name=long" json:"long,omitempty"`} func (m *DataItem) Reset() { *m = DataItem{} }func (m *DataItem) String() string { return proto.CompactTextString(m) }func (*DataItem) ProtoMessage() {}func init() {}
編譯器生成的代碼具有 Go 結構 DataItem
,該結構導出 Go 字段(名稱現已大寫開頭),該字段與 Protobuf IDL 中聲明的名稱匹配。該結構字段具有標準的 Go 數據類型:int32
、int64
、float32
和 string
。在每個字段行的末尾,是描述 Protobuf 類型的字符串,提供 Protobuf IDL 文檔中的數字標簽及有關 JSON 信息的元數據,這將在后面討論。
此外也有函數;最重要的是 Proto.Marshal
,用于將 DataItem
結構的實例序列化為 Protobuf 格式。輔助函數包括:清除 DataItem
結構的 Reset
,生成 DataItem
的單行字符串表示的 String
。
描述 Protobuf 編碼的元數據應在更詳細地分析 Go 程序之前進行仔細研究。
Protobuf 消息的結構為鍵/值對的集合,其中數字標簽為鍵,相應的字段為值。字段名稱(例如,oddA
和 small
)是供人類閱讀的,但是 protoc
編譯器的確使用了字段名稱來生成特定于語言的對應名稱。例如,Protobuf IDL 中的 oddA
和 small
名稱在 Go 結構中分別成為字段 OddA
和 Small
。
鍵和它們的值都被編碼,但是有一個重要的區別:一些數字值具有固定大小的 32 或 64 位的編碼,而其他數字(包括消息標簽)則是 varint
編碼的,位數取決于整數的絕對值。例如,整數值 1 到 15 需要 8 位 varint
編碼,而值 16 到 2047 需要 16 位。varint
編碼在本質上與 UTF-8 編碼類似(但細節不同),它偏愛較小的整數值而不是較大的整數值。(有關詳細分析,請參見 Protobuf 編碼指南)結果是,Protobuf 消息應該在字段中具有較小的整數值(如果可能),并且鍵數應盡可能少,但每個字段至少得有一個鍵。
下表 1 列出了 Protobuf 編碼的要點:
編碼 | 示例類型 | 長度 |
---|---|---|
varint | int32 、uint32 、int64 | 可變長度 |
fixed | fixed32 、float 、double | 固定的 32 位或 64 位長度 |
字節序列 | string 、bytes | 序列長度 |
表 1. Protobuf 數據類型
未明確固定長度的整數類型是 varint
編碼的;因此,在 varint
類型中,例如 uint32
(u
代表無符號),數字 32 描述了整數的范圍(在這種情況下為 0 到 232 - 1),而不是其位的大小,該位大小取決于值。相比之下,對于固定長度類型(例如 fixed32
或 double
),Protobuf 編碼分別需要 32 位和 64 位。Protobuf 中的字符串是字節序列;因此,字段編碼的大小就是字節序列的長度。
另一個高效的方法值得一提。回想一下前面的示例,其中的 DataItems
消息由重復的 DataItem
實例組成:
message DataItems { repeated DataItem item = 1;}
repeated
表示 DataItem
實例是打包的:集合具有單個標簽,在這里是 1。因此,具有重復的 DataItem
實例的 DataItems
消息比具有多個但單獨的 DataItem
字段、每個字段都需要自己的標簽的消息的效率更高。
了解了這一背景,讓我們回到 Go 程序。
dataItem
程序創建一個 DataItem
實例,并使用適當類型的隨機生成的值填充字段。Go 有一個 rand
包,帶有用于生成偽隨機整數和浮點值的函數,而我的 randString
函數可以從字符集中生成指定長度的偽隨機字符串。設計目標是要有一個具有不同類型和位大小的字段值的 DataItem
實例。例如,OddA
和 EvenA
值分別是 64 位非負整數值的奇數和偶數;但是 OddB
和 EvenB
變體的大小為 32 位,并存放 0 到 2047 之間的小整數值。隨機浮點值的大小為 32 位,字符串為 16(Short
)和 32(Long
)字符的長度。這是用隨機值填充 DataItem
結構的代碼段:
// 可變長度整數n1 := rand.Int63() // 大整數if (n1 & 1) == 0 { n1++ } // 確保其是奇數...n3 := rand.Int31() % UpperBound // 小整數if (n3 & 1) == 0 { n3++ } // 確保其是奇數 // 固定長度浮點數...t1 := rand.Float32()t2 := rand.Float32()...// 字符串str1 := randString(StrShort)str2 := randString(StrLong) // 消息dataItem := &DataItem { OddA: n1, EvenA: n2, OddB: n3, EvenB: n4, Big: f1, Small: f2, Short: str1, Long: str2,}
創建并填充值后,DataItem
實例將以 XML、JSON 和 Protobuf 進行編碼,每種編碼均寫入本地文件:
func encodeAndserialize(dataItem *DataItem) { bytes, _ := xml.MarshalIndent(dataItem, "", " ") // Xml to dataitem.xml ioutil.WriteFile(XmlFile, bytes, 0644) // 0644 is file access permissions bytes, _ = json.MarshalIndent(dataItem, "", " ") // Json to dataitem.json ioutil.WriteFile(JsonFile, bytes, 0644) bytes, _ = proto.Marshal(dataItem) // Protobuf to dataitem.pbuf ioutil.WriteFile(PbufFile, bytes, 0644)}
這三個序列化函數使用術語 marshal
,它與 serialize
意思大致相同。如代碼所示,三個 Marshal
函數均返回一個字節數組,然后將其寫入文件。(為簡單起見,忽略可能的錯誤處理。)在示例運行中,文件大小為:
dataitem.xml: 262 bytesdataitem.json: 212 bytesdataitem.pbuf: 88 bytes
Protobuf 編碼明顯小于其他兩個編碼方案。通過消除縮進字符(在這種情況下為空白和換行符),可以稍微減小 XML 和 JSON 序列化的大小。
以下是 dataitem.json
文件,該文件最終是由 json.MarshalIndent
調用產生的,并添加了以 ##
開頭的注釋:
{ "oddA": 4744002665212642479, ## 64-bit >= 0 "evenA": 2395006495604861128, ## ditto "oddB": 57, ## 32-bit >= 0 but < 2048 "evenB": 468, ## ditto "small": 0.7562016, ## 32-bit floating-point "big": 0.85202795, ## ditto "short": "ClH1oDaTtoX$HBN5", ## 16 random chars "long": "xId0rD3Cri%3Wt%^QjcFLJgyXBu9^DZI" ## 32 random chars}
盡管這些序列化的數據寫入到本地文件中,但是也可以使用相同的方法將數據寫入網絡連接的輸出流。
Go 程序接下來通過將先前寫入 dataitem.pbuf
文件的字節反序列化為 DataItem
實例來運行基本測試。這是代碼段,其中去除了錯誤檢查部分:
filebytes, err := ioutil.ReadFile(PbufFile) // get the bytes from the file...testItem.Reset() // clear the DataItem structureerr = proto.Unmarshal(filebytes, testItem) // deserialize into a DataItem instance
用于 Protbuf 反序列化的 proto.Unmarshal
函數與 proto.Marshal
函數相反。原始的 DataItem
和反序列化的副本將被打印出來以確認完全匹配:
Original:2041519981506242154 3041486079683013705 1192 18790.572123 0.326855boPb#T0O8Xd&Ps5EnSZqDg4Qztvo7IIs 9vH66AiGSQgCDxk& Deserialized:2041519981506242154 3041486079683013705 1192 18790.572123 0.326855boPb#T0O8Xd&Ps5EnSZqDg4Qztvo7IIs 9vH66AiGSQgCDxk&
用 Java 寫的示例是為了確認 Protobuf 的語言中立性。原始 IDL 文件可用于生成 Java 支持代碼,其中涉及嵌套類。但是,為了抑制警告信息,可以進行一些補充。這是修訂版,它指定了一個 DataMsg
作為外部類的名稱,內部類在該 Protobuf 消息后面自動命名為 DataItem
:
syntax = "proto3"; package main; option java_outer_classname = "DataMsg"; message DataItem {...
進行此更改后,protoc
編譯與以前相同,只是所期望的輸出現在是 Java 而不是 Go:
% protoc --java_out=. dataitem.proto
生成的源文件(在名為 main
的子目錄中)為 DataMsg.java
,長度約為 1,120 行:Java 并不簡潔。編譯然后運行 Java 代碼需要具有 Protobuf 庫支持的 JAR 文件。該文件位于 Maven 存儲庫中。
放置好這些片段后,我的測試代碼相對較短(并且在 ZIP 文件中以 Main.java
形式提供):
package main;import java.io.FileInputStream; public class Main { public static void main(String[] args) { String path = "dataitem.pbuf"; // from the Go program's serialization try { DataMsg.DataItem deserial = DataMsg.DataItem.newBuilder().mergeFrom(new FileInputStream(path)).build(); System.out.println(deserial.getOddA()); // 64-bit odd System.out.println(deserial.getLong()); // 32-character string } catch(Exception e) { System.err.println(e); } }}
當然,生產級的測試將更加徹底,但是即使是該初步測試也可以證明 Protobuf 的語言中立性:dataitem.pbuf
文件是 Go 程序對 Go 語言版的 DataItem
進行序列化的結果,并且該文件中的字節被反序列化以產生一個 Java 語言的 DataItem
實例。Java 測試的輸出與 Go 測試的輸出相同。
讓我們以一個示例作為結尾,來突出 Protobuf 效率,但又強調在任何編碼技術中都會涉及到的成本。考慮以下 Protobuf IDL 文件:
syntax = "proto3";package main; message NumPairs { repeated NumPair pair = 1;} message NumPair { int32 odd = 1; int32 even = 2;}
NumPair
消息由兩個 int32
值以及每個字段的整數標簽組成。NumPairs
消息是嵌入的 NumPair
消息的序列。
Go 語言的 numPairs
程序(如下)創建了 200 萬個 NumPair
實例,每個實例都附加到 NumPairs
消息中。該消息可以按常規方式進行序列化和反序列化。
package main import ( "math/rand" "time" "encoding/xml" "encoding/json" "io/ioutil" "github.com/golang/protobuf/proto") // protoc-generated code: startvar _ = proto.Marshaltype NumPairs struct { Pair []*NumPair `protobuf:"bytes,1,rep,name=pair" json:"pair,omitempty"`} func (m *NumPairs) Reset() { *m = NumPairs{} }func (m *NumPairs) String() string { return proto.CompactTextString(m) }func (*NumPairs) ProtoMessage() {}func (m *NumPairs) GetPair() []*NumPair { if m != nil { return m.Pair } return nil} type NumPair struct { Odd int32 `protobuf:"varint,1,opt,name=odd" json:"odd,omitempty"` Even int32 `protobuf:"varint,2,opt,name=even" json:"even,omitempty"`} func (m *NumPair) Reset() { *m = NumPair{} }func (m *NumPair) String() string { return proto.CompactTextString(m) }func (*NumPair) ProtoMessage() {}func init() {}// protoc-generated code: finish var numPairsStruct NumPairsvar numPairs = &numPairsStruct func encodeAndserialize() { // XML encoding filename := "./pairs.xml" bytes, _ := xml.MarshalIndent(numPairs, "", " ") ioutil.WriteFile(filename, bytes, 0644) // JSON encoding filename = "./pairs.json" bytes, _ = json.MarshalIndent(numPairs, "", " ") ioutil.WriteFile(filename, bytes, 0644) // ProtoBuf encoding filename = "./pairs.pbuf" bytes, _ = proto.Marshal(numPairs) ioutil.WriteFile(filename, bytes, 0644)} const HowMany = 200 * 100 * 100 // two million func main() { rand.Seed(time.Now().UnixNano()) // uncomment the modulus operations to get the more efficient version for i := 0; i < HowMany; i++ { n1 := rand.Int31() // % 2047 if (n1 & 1) == 0 { n1++ } // ensure it's odd n2 := rand.Int31() // % 2047 if (n2 & 1) == 1 { n2++ } // ensure it's even next := &NumPair { Odd: n1, Even: n2, } numPairs.Pair = append(numPairs.Pair, next) } encodeAndserialize()}
每個 NumPair
中隨機生成的奇數和偶數值的范圍在 0 到 20 億之間變化。就原始數據(而非編碼數據)而言,Go 程序中生成的整數總共為 16MB:每個 NumPair
為兩個整數,總計為 400 萬個整數,每個值的大小為四個字節。
為了進行比較,下表列出了 XML、JSON 和 Protobuf 編碼的示例 NumsPairs
消息的 200 萬個 NumPair
實例。原始數據也包括在內。由于 numPairs
程序生成隨機值,因此樣本運行的輸出有所不同,但接近表中顯示的大小。
編碼 | 文件 | 字節大小 | Pbuf/其它 比例 |
---|---|---|---|
無 | pairs.raw | 16MB | 169% |
Protobuf | pairs.pbuf | 27MB | — |
JSON | pairs.json | 100MB | 27% |
XML | pairs.xml | 126MB | 21% |
表 2. 16MB 整數的編碼開銷
不出所料,Protobuf 和之后的 XML 和 JSON 差別明顯。Protobuf 編碼大約是 JSON 的四分之一,是 XML 的五分之一。但是原始數據清楚地表明 Protobuf 也會產生編碼開銷:序列化的 Protobuf 消息比原始數據大 11MB。包括 Protobuf 在內的任何編碼都涉及結構化數據,這不可避免地會增加字節。
序列化的 200 萬個 NumPair
實例中的每個實例都包含四個整數值:Go 結構中的 Even
和 Odd
字段分別一個,而 Protobuf 編碼中的每個字段、每個標簽一個。對于原始數據(而不是編碼數據),每個實例將達到 16 個字節,樣本 NumPairs
消息中有 200 萬個實例。但是 Protobuf 標記(如 NumPair
字段中的 int32
值)使用 varint
編碼,因此字節長度有所不同。特別是,小的整數值(在這種情況下,包括標簽在內)需要不到四個字節進行編碼。
如果對 numPairs
程序進行了修改,以使兩個 NumPair
字段的值小于 2048,且其編碼為一或兩個字節,則 Protobuf 編碼將從 27MB 下降到 16MB,這正是原始數據的大小。下表總結了樣本運行中的新編碼大小。
編碼 | 文件 | 字節大小 | Pbuf/其它 比例 |
---|---|---|---|
None | pairs.raw | 16MB | 100% |
Protobuf | pairs.pbuf | 16MB | — |
JSON | pairs.json | 77MB | 21% |
XML | pairs.xml | 103MB | 15% |
表 3. 編碼 16MB 的小于 2048 的整數
總之,修改后的 numPairs
程序的字段值小于 2048,可減少原始數據中每個四字節整數值的大小。但是 Protobuf 編碼仍然需要標簽,這些標簽會在 Protobuf 消息中添加字節。Protobuf 編碼確實會增加消息大小,但是如果要編碼相對較小的整數值(無論是字段還是鍵),則可以通過 varint
因子來減少此開銷。
對于包含混合類型的結構化數據(且整數值相對較小)的中等大小的消息,Protobuf 明顯優于 XML 和 JSON 等選項。在其他情況下,數據可能不適合 Protobuf 編碼。例如,如果兩個應用程序需要共享大量文本記錄或大整數值,則可以采用壓縮而不是編碼技術。
感謝各位的閱讀,以上就是“go語言怎么用Protobuf做數據交換”的內容了,經過本文的學習后,相信大家對go語言怎么用Protobuf做數據交換這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。