嵌入式linux之go语言开发(七)protobuf的使用

简介: 嵌入式linux之go语言开发(七)protobuf的使用

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


相关文章
|
26天前
|
存储 监控 算法
员工上网行为监控中的Go语言算法:布隆过滤器的应用
在信息化高速发展的时代,企业上网行为监管至关重要。布隆过滤器作为一种高效、节省空间的概率性数据结构,适用于大规模URL查询与匹配,是实现精准上网行为管理的理想选择。本文探讨了布隆过滤器的原理及其优缺点,并展示了如何使用Go语言实现该算法,以提升企业网络管理效率和安全性。尽管存在误报等局限性,但合理配置下,布隆过滤器为企业提供了经济有效的解决方案。
72 8
员工上网行为监控中的Go语言算法:布隆过滤器的应用
|
1月前
|
Go 开发工具
百炼-千问模型通过openai接口构建assistant 等 go语言
由于阿里百炼平台通义千问大模型没有完善的go语言兼容openapi示例,并且官方答复assistant是不兼容openapi sdk的。 实际使用中发现是能够支持的,所以自己写了一个demo test示例,给大家做一个参考。
|
1月前
|
程序员 Go
go语言中结构体(Struct)
go语言中结构体(Struct)
112 71
|
1月前
|
存储 Go 索引
go语言中的数组(Array)
go语言中的数组(Array)
115 67
|
7天前
|
算法 安全 Go
Go语言中的加密和解密是如何实现的?
Go语言通过标准库中的`crypto`包提供丰富的加密和解密功能,包括对称加密(如AES)、非对称加密(如RSA、ECDSA)及散列函数(如SHA256)。`encoding/base64`包则用于Base64编码与解码。开发者可根据需求选择合适的算法和密钥,使用这些包进行加密操作。示例代码展示了如何使用`crypto/aes`包实现对称加密。加密和解密操作涉及敏感数据处理,需格外注意安全性。
30 14
|
7天前
|
Go 数据库
Go语言中的包(package)是如何组织的?
在Go语言中,包是代码组织和管理的基本单元,用于集合相关函数、类型和变量,便于复用和维护。包通过目录结构、文件命名、初始化函数(`init`)及导出规则来管理命名空间和依赖关系。合理的包组织能提高代码的可读性、可维护性和可复用性,减少耦合度。例如,`stringutils`包提供字符串处理函数,主程序导入使用这些函数,使代码结构清晰易懂。
40 11
|
7天前
|
存储 安全 Go
Go语言中的map数据结构是如何实现的?
Go 语言中的 `map` 是基于哈希表实现的键值对数据结构,支持快速查找、插入和删除操作。其原理涉及哈希函数、桶(Bucket)、动态扩容和哈希冲突处理等关键机制,平均时间复杂度为 O(1)。为了确保线程安全,Go 提供了 `sync.Map` 类型,通过分段锁实现并发访问的安全性。示例代码展示了如何使用自定义结构体和切片模拟 `map` 功能,以及如何使用 `sync.Map` 进行线程安全的操作。
|
11天前
|
监控 安全 算法
深度剖析核心科技:Go 语言赋能局域网管理监控软件进阶之旅
在局域网管理监控中,跳表作为一种高效的数据结构,能显著提升流量索引和查询效率。基于Go语言的跳表实现,通过随机化索引层生成、插入和搜索功能,在高并发场景下展现卓越性能。跳表将查询时间复杂度优化至O(log n),助力实时监控异常流量,保障网络安全与稳定。示例代码展示了其在实际应用中的精妙之处。
36 9
|
19天前
|
Ubuntu Linux 开发者
Ubuntu20.04搭建嵌入式linux网络加载内核、设备树和根文件系统
使用上述U-Boot命令配置并启动嵌入式设备。如果配置正确,设备将通过TFTP加载内核和设备树,并通过NFS挂载根文件系统。
68 15
|
21天前
|
算法 安全 Go
Go 语言中实现 RSA 加解密、签名验证算法
随着互联网的发展,安全需求日益增长。非对称加密算法RSA成为密码学中的重要代表。本文介绍如何使用Go语言和[forgoer/openssl](https://github.com/forgoer/openssl)库简化RSA加解密操作,包括秘钥生成、加解密及签名验证。该库还支持AES、DES等常用算法,安装简便,代码示例清晰易懂。
56 12