之前写过一篇博文:《如果终端采用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上执行的文件。