Golang 使用Protocol Buffer 案例
- 2020 年 3 月 7 日
- 筆記
目錄
1. 前言
工作幾年了。ITDragon龍 的編程語言從熟悉的Java,到喜歡的Kotlin,最後到一言難盡的Golang。常用的數據交換格式也從xml到json,最後到現在的protobuf。因為底層驅動對數據的採集非常頻繁,而且對數據的可讀性並沒有太高的要求。所以採用序列化體積更小、序列化和反序列化速度更快的protobuf。考慮到後期還會繼續深入使用protobuf,先插個眼,方便後期的使用和問題排查。
2. Protobuf 簡介
Google Protocol Buffer(簡稱 Protobuf) 是Google 旗下的一款輕便高效的結構化數據存儲格式,平台無關、語言無關、可擴展,可用於通訊協議和數據存儲等領域。適合用做數據存儲和作為不同應用,不同語言之間相互通信的數據交換格式。
2.1 Protobuf 優點
1.支持多種語言、跨平台,多平台之間只需要維護一套proto協議文件(當然還需要對應的配置)。
2.序列化後是二進制流,體積比Json和Xml小,適合數據量較大的傳輸場景。
3.序列化和反序列化速度很快,據說比Json和Xml快20~100倍。(ITDragon龍 本地測過,只有在數據達到一定量時才會有明顯的差距)
小結:適合傳輸數據量較大,對響應速度有要求的數據傳輸場景。
2.2 Protobuf 缺點
1.序列化後的數據不具備可讀性。
2.需要一定的額外開發成本(.proto 協議文件),每次修改都需要重新生成協議文件。
3.應用場景並沒有Json和Xml廣,相對於使用的工具也少。
小結:自解釋性較差、通用性較差,不適合用於對基於文本的標記文檔建模。
2.3 Protobuf Golang 安裝使用
step1:下載protobuf 編譯器protoc。下載地址 。
step2:下載對應的文件。Windows系統直接將解壓後的protoc.exe放在GOPATH/bin目錄下(該目錄要放在環境變量中);Linux系統需要make編譯。
step3:安裝protobuf庫文件(因為protoc並沒有直接支持go語言)。官方goprotobuf,執行成功後GOPATH/bin目錄下會有protoc-gen-go.exe文件
go get github.com/golang/protobuf/proto go get github.com/golang/protobuf/protoc-gen-go
據說gogoprotobuf庫生成的代碼質量和編解碼性能都比官方的goprotobuf庫強,而且完全兼容官方的protobuf。ITDragon龍 我們當然也不能放過它。
step4:安裝gogoprotobuf庫文件。執行成功後GOPATH/bin目錄下會有protoc-gen-gofast.exe文件
go get github.com/gogo/protobuf/proto go get github.com/gogo/protobuf/gogoproto go get github.com/gogo/protobuf/protoc-gen-gofast
step5:使用protoc 生成go文件。先移步到xxx.proto文件所在目錄,再執行以下任意一個命令
// 官方goprotobuf protoc --go_out=. *.proto // gogoprotobuf protoc --gofast_out=. *.proto
step6:protoc是直接支持Java語言,下載後可以直接使用。
protoc xxx.proto --java_out=./
3. Protobuf 通訊案例
這裡用Golang分別實現socket的服務端和客戶端,最後通過protobuf進行數據傳輸,實現一個簡單案例。
3.1 創建.proto協議文件
1.創建一個簡單的models.proto協議文件
syntax = "proto3"; package protobuf; message MessageEnvelope{ int32 TargetId = 1; string ID = 2; bytes Payload = 3; string Type = 4; }
2.通過protoc生成對應的models.pb.go文件(這個文件內容太多,可讀性也差,就不貼出來了)
protoc --gofast_out=. *.proto
3.2 protobuf編解碼
package protobuf import ( "fmt" "github.com/golang/protobuf/proto" "testing" ) func TestProtocolBuffer(t *testing.T) { // MessageEnvelope是models.pb.go的結構體 oldData := &MessageEnvelope{ TargetId: 1, ID: "1", Type: "2", Payload: []byte("ITDragon protobuf"), } data, err := proto.Marshal(oldData) if err != nil { fmt.Println("marshal error: ", err.Error()) } fmt.Println("marshal data : ", data) newData := &MessageEnvelope{} err = proto.Unmarshal(data, newData) if err != nil { fmt.Println("unmarshal err:", err) } fmt.Println("unmarshal data : ", newData) } -----------打印結果----------- === RUN TestProtocolBuffer marshal data : [8 1 18 1 49 26 17 73 84 68 114 97 103 111 110 32 112 114 111 116 111 98 117 102 34 1 50] unmarshal data : TargetId:1 ID:"1" Payload:"ITDragon protobuf" Type:"2" --- PASS: TestProtocolBuffer (0.00s) PASS
3.3 socket通訊
1.TCP Server端
func TestTcpServer(t *testing.T) { // 為突出重點,忽略err錯誤判斷 addr, _ := net.ResolveTCPAddr("tcp4", "127.0.0.1:9000") listener, _ := net.ListenTCP("tcp4", addr) for { conn, _ := listener.AcceptTCP() go func() { for { buf := make([]byte, 512) _, _ = conn.Read(buf) newData := &MessageEnvelope{} _ = proto.Unmarshal(buf, newData) fmt.Println("server receive : ", newData) } }() } }
2.TCP Client端
func TestTcpClient(t *testing.T) { // 為突出重點,忽略err錯誤判斷 connection, _ := net.Dial("tcp", "127.0.0.1:9000") var targetID int32 = 1 for { oldData := &MessageEnvelope{ TargetId: targetID, ID: strconv.Itoa(int(targetID)), Type: "2", Payload: []byte(fmt.Sprintf("ITDragon protoBuf-%d", targetID)), } data, _ := proto.Marshal(oldData) _, _ = connection.Write(data) fmt.Println("client send : ", data) time.Sleep(2 * time.Second) targetID++ } }
4. Protobuf 基礎知識
這裡記錄工作中常用知識點和對應的注意事項,詳細知識點可以通過官網查詢:https://developers.google.com/protocol-buffers/docs/proto3
4.1 簡單模板
舉一個簡單列子。不同的編程語言的語法差別主要體現在數據類型的不同。
syntax = "proto3"; // 指定使用proto3語法 package protobuf; // 指定包名 message MessageEnvelope{ // 定義一個消息模型 uint32 TargetId = 1; // 定義一個無符號整數類型 string ID = 2; // 定義一個字符串類型 bytes Payload = 3; // 定義一個位元組類型 MessageType Type = 4; // 定義一個枚舉類型 repeated Player Players = 5; // 定義一個集合對象類型 } enum MessageType { // 定義一個枚舉類型 SYSTEM = 0; // 第一個枚舉值為零 ALARM = 1; } message Player { ... }
4.2 簡單語法
1.syntax : 指定使用proto版本的語法,缺省是proto2。若使用syntax語法,則必須位於文件的非空非注釋的第一個行。若不指定proto3,卻使用了proto3的語法,則會報錯。
2.package : 指定包名。防止不同 .proto 項目間命名發生衝突。
3.message : 定義消息類型。
4.enum : 定義枚舉類型。第一個枚舉值設置為零。
5.repeated : 表示被修飾的變量允許重複,可以理解成集合、數組、切片。
6.map : 待補充
7.Oneof : 待補充
8.定義變量 : (字段修飾符) + 數據類型 + 字段名稱 = 唯一的編號標識符;
9.編號標識符 :在message中,每個字段都有唯一的編號標識符。用來在消息的二進制格式中識別各個字段,一旦使用就不能夠再改變。[1,15]之內的標識符在編碼時佔用一個位元組。[16,2047]之內的標識符佔用2個位元組。
10.變量類型:以下來源網絡整理
.proto | Notes | Go | Java | C++ | C# | Python |
---|---|---|---|---|---|---|
double | float64 | double | double | double | float | |
float | float32 | float | float | float | float | |
int32 | 使用變長編碼,對於負值的效率很低,如果你的域有可能有負值,請使用sint64替代 | int32 | int | int32 | int | int |
uint32 | 使用變長編碼 | uint32 | int | uint32 | uint | int/long |
uint64 | 使用變長編碼 | uint64 | long | uint64 | ulong | int/long |
sint32 | 使用變長編碼,這些編碼在負值時比int32高效的多 | int32 | int | int32 | int | int |
sint64 | 使用變長編碼,有符號的整型值。編碼時比通常的int64高效。 | int64 | long | int64 | long | int/long |
fixed32 | 總是4個位元組,如果數值總是比總是比228大的話,這個類型會比uint32高效。 | uint32 | int | uint32 | uint | int |
fixed64 | 總是8個位元組,如果數值總是比總是比256大的話,這個類型會比uint64高效。 | uint64 | long | uint64 | ulong | int/long |
sfixed32 | 總是4個位元組 | int32 | int | int32 | int | int |
sfixed64 | 總是8個位元組 | int64 | long | int64 | long | int/long |
bool | bool | boolean | bool | bool | bool | |
string | 一個字符串必須是UTF-8編碼或者7-bit ASCII編碼的文本。 | string | String | string | string | str /unicode |
bytes | 可能包含任意順序的位元組據。 | []byte | ByteString | string | ByteString | str |
4.3 注意事項
1.整數數據類型區分好有符號和無符號類型,建議少用萬金油式的int32。
2.將可能頻繁使用的字段設置在[1,15]之內,也要注意預留幾個,方便後期添加。