之前寫過一篇博文:《如果終端採用protobuf與採集前置通信,能帶來哪些變革?https://blog.csdn.net/yyz_1987/article/details/81147454》,介紹了使用protobuf作爲序列化通信格式的諸多好處。
那麼接下來在嵌入式linux之go語言開發實戰中,也嘗試用protobuf作爲序列化和通信的協議格式。
之前想做個protobuf序列化的反向解析工具,但是發現反向解析工具,現成的就有啊。可以直接拿來用。
使用方法:
E:\GOPATH\src\protobuf>cat out.bin | protoc --decode_raw
直接就輸出了反向之後的內容,且無需知道test.proto定義文件。
protoc.exe可直接從網上下載,下載後放到go/bin的安裝路徑下即可。
我下載的是protoc-3.4.0-win32.zip
protobuf的簡單使用:
先編寫*.proto定義文件如test.proto:
在這個文件中可以定義需要的結構, 例如枚舉型, 結構體等等. 那麼首先我自己定義了一個結構如下所示,
syntax = "proto2";
package test;
message myMsg
{
required int32 id = 1; // ID
required string str = 2; // str
optional int32 opt = 3; //optional field
}
注意required是必須要求的字段, optional是可選字段。
同時注意, id=1, 後面的數字僅僅是一個unique標誌而已, 保證唯一性就OK!
然後使用protoc test.proto –go_out=. 編譯這個文件, 生成的文件名稱爲test.pb.go文件!
如果這個路徑下有多個文件需要編譯, 那麼執行protoc –go_out=. *.proto就可以.
注意–go_out=後面的參數是生成的文件的路徑, 本文生成的文件在’.’當前路徑下.
【proto字段對應關係】
proto字段類型的對應關係:
【標識符】
在消息定義中,每個字段都有唯一的一個數字標識符。
標識符用來在消息的二進制格式中識別各個字段,一旦使用就不能夠再改變。
最小的標識符可以從1開始,最大到2^29 - 1(536,870,911),不可以使用其中[19000-19999]( Protobuf協議實現中進行了預留。如果非要在.proto文件中使用預留標識符,編譯時就會報警。
[1,15]內的標識號在編碼的時候會佔用一個字節。[16,2047]之內的標識號則佔用2個字節。所以應該爲頻繁出現的消息元素保留[1,15]內的標識號。
【字段規則】
消息的字段修飾符必須是如下之一:
A、singular:一個格式良好的message應該有0個或者1個該字段(但不能超過1個)。
B、repeated:在一個格式良好的消息中,該字段可以重複任意多次(包括0次),重複值的順序會被保留。
在proto3中,repeated的標量字段默認情況下使用packed。
以上關於proto對應關係和標識符,字段規則等內容引用自博客《https://blog.51cto.com/9291927/2331980?source=drh》
更多關於proto文件的定義和使用,可參見博文:《https://blog.51cto.com/9291927/2331980?source=drh》
[Protobuf序列化原理]
1、Protobuf序列化
Protobuf對於數據存儲的三大原則:
(1)Protocol Buffer將消息中的每個字段進行編碼後,利用T - L - V 存儲方式進行數據的存儲,最終得到一個二進制字節流。
(2)ProtoBuf對於不同數據類型採用不同的序列化方式(數據編碼方式與數據存儲方式)
Protobuf對於不同的字段類型採用不同的編碼和數據存儲方式對消息字段進行序列化,以確保得到高效緊湊的數據壓縮。不同類型的數據採用的編碼方式和存儲方式如下:
對於Varint編碼數據的存儲,不需要存儲字節長度Length,使用T-V存儲方式進行存儲;對於採用其它編碼方式(如LENGTH_DELIMITED)的數據,使用T-L-V存儲方式進行存儲。
(3)ProtoBuf對於數據字段值的獨特編碼方式與T-L-V數據存儲方式,使得 ProtoBuf序列化後數據量體積極小。
2、WireType=0的序列化
WireType=0的類型包括int32,int64,uint32,unint64,bool,enum以及sint32和sint64。
編碼方式採用Varint編碼(如果爲負數,採用Zigzag輔助編碼),數據存儲方式使用T-V方式存儲二進制字節流。
3、WireType=1的序列化
WireType=1的類型包括fixed64,sfixed64,double。
編碼方式採用64bit編碼(編碼後數據大小爲64bit,高位在後,低位在前),數據存儲方式使用T-V方式存儲二進制字節流。
4、WireType=2的序列化
WireType=2的類型包括string,bytes,嵌套消息,packed repeated字段。
對於編碼方式,標識符Tag採用Varint編碼,字節長度Length採用Varint編碼,string類型字段值採用UTF-8編碼,嵌套消息類型的字段值根據嵌套消息內部的字段數據類型進行選擇,
數據存儲方式使用T-L-V方式存儲二進制字節流。
5、WireType=5的序列化
WireType=5的類型包括fixed32,sfixed32,float。
編碼方式採用32bit編碼(編碼後數據大小爲32bit,高位在後,低位在前),數據存儲方式使用T-V方式存儲二進制字節流。
[Protobuf使用建議]
基於Protobuf序列化原理分析,爲了有效降低序列化後數據量的大小,可以採用以下措施:
(1)多用 optional或 repeated修飾符
若optional 或 repeated 字段沒有被設置字段值,那麼該字段在序列化時的數據中是完全不存在的,即不需要進行編碼,但相應的字段在解碼時會被設置爲默認值。
(2)字段標識號(Field_Number)儘量只使用1-15,且不要跳動使用
Tag是需要佔字節空間的。如果Field_Number>16時,Field_Number的編碼就會佔用2個字節,那麼Tag在編碼時就會佔用更多的字節;如果將字段標識號定義爲連續遞增的數值,將獲得更好的編碼和解碼性能。
(3)若需要使用的字段值出現負數,請使用sint32/sint64,不要使用int32/int64。
採用sint32/sint64數據類型表示負數時,會先採用Zigzag編碼再採用Varint編碼,從而更加有效壓縮數據。
(4)對於repeated字段,儘量增加packed=true修飾
增加packed=true修飾,repeated字段會採用連續數據存儲方式,即T - L - V - V -V方式。
以上關於protobuf序列化原理和使用建議的介紹,參見一篇寫的好的博文:《https://blog.51cto.com/9291927/2332264》
若需要生成供其他語言調用的代碼源文件,
則需要這樣:
protoc --go_out=. test.proto //生成供go語言使用的結構源文件
protoc --cpp_out=. test.proto //生成供c++語言使用的類源文件
protoc --java_out=. test.proto //生成供java語言使用的類源文件
注:能否生成供c語言調用的源碼?能否在嵌入式系統上供c使用protobuf?
也是可以的。參照博文《protobuf在嵌入式linux下的移植及c語言調用https://blog.csdn.net/yyz_1987/article/details/81126877》
注:生成供go語言使用的源文件,需要提前先獲取並安裝proto-gen-go,
因爲protoc --go_out內部自動調用了protoc-gen-go
go get github.com/golang/protobuf/protoc-gen-go
,
這條命令去獲取protoc-gen-go,然後go install即可。
或者直接go install github.com/golang/protobuf/protoc-gen-go
調用
protoc --go_out=. test.proto
生成的代碼如下:
// Code generated by protoc-gen-go.
// source: 1.proto
// DO NOT EDIT!
/*
Package test is a generated protocol buffer package.
It is generated from these files:
1.proto
It has these top-level messages:
MyMsg
*/
package test
import proto "github.com/golang/protobuf/proto"
import math "math"
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = math.Inf
type MyMsg struct {
Id *int32 `protobuf:"varint,1,req,name=id" json:"id,omitempty" bson:"id,omitempty"`
Str *string `protobuf:"bytes,2,req,name=str" json:"str,omitempty" bson:"str,omitempty"`
Opt *int32 `protobuf:"varint,3,opt,name=opt" json:"opt,omitempty" bson:"opt,omitempty"`
XXX_unrecognized []byte `json:"-"`
}
func (m *MyMsg) Reset() { *m = MyMsg{} }
func (m *MyMsg) String() string { return proto.CompactTextString(m) }
func (*MyMsg) ProtoMessage() {}
func (m *MyMsg) GetId() int32 {
if m != nil && m.Id != nil {
return *m.Id
}
return 0
}
func (m *MyMsg) GetStr() string {
if m != nil && m.Str != nil {
return *m.Str
}
return ""
}
func (m *MyMsg) GetOpt() int32 {
if m != nil && m.Opt != nil {
return *m.Opt
}
return 0
}
func init() {
}
注意: 生成的文件中的package是test, 那麼文件必須放在test文件夾下!
否則會報錯: “can’t load package: package test: found packages test (test.pb.go) and main (main.go)”
測試程序:
// main.go
package main
import (
"fmt"
"io/ioutil"
t "./test"
"github.com/golang/protobuf/proto"
)
// WriteFile 寫文件
//
func WriteFile(fname, content string) {
data := []byte(content)
if ioutil.WriteFile(fname, data, 0644) == nil {
fmt.Println("寫入文件成功:", content)
}
}
// WriteFile1 寫文件
//
func WriteFile1(fname string, content []byte) {
if ioutil.WriteFile(fname, content, 0644) == nil {
fmt.Println("寫入文件成功:", content)
}
}
func main() {
// 創建一個對象, 並填充字段, 可以使用proto中的類型函數來處理例如Int32(XXX)
hw := t.MyMsg{
Id: proto.Int32(1),
Str: proto.String("hello"),
Opt: proto.Int32(2),
}
// 對數據進行編碼, 注意參數是message指針
mData, err := proto.Marshal(&hw)
if err != nil {
fmt.Println("Error1: ", err)
return
}
WriteFile1("out.bin", mData)
// 下面進行解碼, 注意參數
var umData t.MyMsg
err = proto.Unmarshal(mData, &umData)
if err != nil {
fmt.Println("Error2: ", err)
return
}
// 輸出結果
fmt.Println(*umData.Id, " ", *umData.Str, " ", *umData.Opt)
}
序列化後的內容,這裏保存爲文件out.bin了,可以用16進制打開查看內容。
不過這protobuf格式,需要按協議解析後才能看得懂,直接看看不出來具體內容。通過protoc.exe可以直接反序列化查看。
Protobuf的編碼是盡其所能地將字段的元信息和字段的值壓縮存儲,並且字段的元信息中含有對這個字段描述的所有信息。使用了variant是一種緊湊型數字編碼。 Protobuf編碼的最終結果可以使用下圖來表示:
後續打算對protobuf的協議格式做個研究,對go的protobuf的源碼做個解讀。
在嵌入式linux上,執行看看效果,使用如下命令:
GOOS=linux GOARCH=arm GOARM=7 go build main.go
即可生成可在嵌入式linux上執行的文件。