嵌入式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上执行的文件。


相关文章
|
24天前
|
存储 安全 Java
【Golang】(4)Go里面的指针如何?函数与方法怎么不一样?带你了解Go不同于其他高级语言的语法
结构体可以存储一组不同类型的数据,是一种符合类型。Go抛弃了类与继承,同时也抛弃了构造方法,刻意弱化了面向对象的功能,Go并非是一个传统OOP的语言,但是Go依旧有着OOP的影子,通过结构体和方法也可以模拟出一个类。
77 1
|
3月前
|
Cloud Native 安全 Java
Go:为云原生而生的高效语言
Go:为云原生而生的高效语言
262 1
|
9月前
|
编译器 Go
揭秘 Go 语言中空结构体的强大用法
Go 语言中的空结构体 `struct{}` 不包含任何字段,不占用内存空间。它在实际编程中有多种典型用法:1) 结合 map 实现集合(set)类型;2) 与 channel 搭配用于信号通知;3) 申请超大容量的 Slice 和 Array 以节省内存;4) 作为接口实现时明确表示不关注值。此外,需要注意的是,空结构体作为字段时可能会因内存对齐原因占用额外空间。建议将空结构体放在外层结构体的第一个字段以优化内存使用。
|
9月前
|
运维 监控 算法
监控局域网其他电脑:Go 语言迪杰斯特拉算法的高效应用
在信息化时代,监控局域网成为网络管理与安全防护的关键需求。本文探讨了迪杰斯特拉(Dijkstra)算法在监控局域网中的应用,通过计算最短路径优化数据传输和故障检测。文中提供了使用Go语言实现的代码例程,展示了如何高效地进行网络监控,确保局域网的稳定运行和数据安全。迪杰斯特拉算法能减少传输延迟和带宽消耗,及时发现并处理网络故障,适用于复杂网络环境下的管理和维护。
|
3月前
|
Cloud Native Go API
Go:为云原生而生的高效语言
Go:为云原生而生的高效语言
316 0
|
3月前
|
Cloud Native Java Go
Go:为云原生而生的高效语言
Go:为云原生而生的高效语言
210 0
|
3月前
|
Cloud Native Java 中间件
Go:为云原生而生的高效语言
Go:为云原生而生的高效语言
177 0
|
3月前
|
Cloud Native Java Go
Go:为云原生而生的高效语言
Go:为云原生而生的高效语言
273 0
|
3月前
|
数据采集 Go API
Go语言实战案例:多协程并发下载网页内容
本文是《Go语言100个实战案例 · 网络与并发篇》第6篇,讲解如何使用 Goroutine 和 Channel 实现多协程并发抓取网页内容,提升网络请求效率。通过实战掌握高并发编程技巧,构建爬虫、内容聚合器等工具,涵盖 WaitGroup、超时控制、错误处理等核心知识点。
|
3月前
|
数据采集 JSON Go
Go语言实战案例:实现HTTP客户端请求并解析响应
本文是 Go 网络与并发实战系列的第 2 篇,详细介绍如何使用 Go 构建 HTTP 客户端,涵盖请求发送、响应解析、错误处理、Header 与 Body 提取等流程,并通过实战代码演示如何并发请求多个 URL,适合希望掌握 Go 网络编程基础的开发者。

热门文章

最新文章