Golang 使用Protocol Buffer 案例

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]之內,也要注意預留幾個,方便後期添加。