当前位置: 代码迷 >> 综合 >> 【Protobuf】proto通用二进制文件的生成与解析(C++/Python,附完整源码)
  详细解决方案

【Protobuf】proto通用二进制文件的生成与解析(C++/Python,附完整源码)

热度:6   发布时间:2023-12-18 03:22:16.0

protobuf是Google开源的一个跨平台的结构化数据存储格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。


前言

说起python版本的proto的用法,可能最熟悉的莫过于以下两句:

obj.ParseFromString(data)
data = obj.SerializeToString()

其中,obj是proto中message的实例对象,data是序列化之后的二进制数据。

ParseFromString()将二进制数据反序列化,最终保存在obj中;SerializeToString()则将obj进行序列化,赋值给data

这样可以将“一帧”的数据进行序列化和反序列化,这部分网络上的资料很多。但是如果有很多“帧”的数据,甚至还有一些不能proto序列化的二进制内容想要一起保存。这部分的资料却很少。

比如:我们有一个相机,我们可以得到每一帧的图片,然后我们对每张图片进行处理,得到一些处理结果。我们想要把图片和处理结果一起保存起来。这该怎么办呢?

本文就这部分进行讲解,其中代码部分已经开源,有需求的可以到以下链接获取,包含C++和Python版本

GitHub:https://github.com/yngzMiao/protobuf-parser-tool

下文主要以Python版本为例,进行介绍。


如何保存

同样以保存相机的数据为例。首先分析一下需要保存的内容:图像信息可以用其二进制内容进行保存,处理结果可以通过proto的文件定义进行保存。这部分合并起来就是一帧的内容。

但是有一个问题,每帧内的图像信息不一定大小都一样!图片信息和处理结果之间怎么区分?每帧之间怎么区分?

最简单的办法就是在每块内容之间加上一个固定长度的tag,该tag存放的是下面一块内容的大小长度。那么就可以通过这个tag,进行块与块之间的跳转了。

即采用记录二进制数据大小的方式,即:

在每段序列化的二进制数据前,都放置4个字节大小的内容,这块内容用来保存接下来的二进制数据的字节长度

字节长度通过以下方法获得:

proto_len = obj.ByteSize()

这块内容写入proto的方式:

temp_data = [0, 0, 0, 0]
temp_data[3] = proto_len & 0x00FF
temp_data[2] = (proto_len >> 8) & 0x00FF
temp_data[1] = (proto_len >> 16) & 0x00FF
temp_data[0] = (proto_len >> 24) & 0x00FFfor i in [0, 1, 2, 3]:if temp_data[i] > 127:temp_data[i] = temp_data[i] - 256bin_size0 = struct.pack('b', temp_data[0])
bin_size1 = struct.pack('b', temp_data[1])
bin_size2 = struct.pack('b', temp_data[2])
bin_size3 = struct.pack('b', temp_data[3])

读取这块内容的方式:

binval = [int(struct.unpack('b', temp_len[0])[0]),int(struct.unpack('b', temp_len[1])[0]),int(struct.unpack('b', temp_len[2])[0]),int(struct.unpack('b', temp_len[3])[0])]re = (binval[0] << 24) & 0xFF000000
re = ((binval[1] << 16) & 0x00FF0000) | re
re = ((binval[2] << 8) & 0x0000FF00) | re
re = ((binval[3]) & 0x000000FF) | re

除此之外,我还添加了版本的内容。所谓版本,就是在proto的最开始的4字节,设置这个内容的本意在于:

由于proto更新频率快,添加字段或者删除字段可能也是常有的事,最好需要对每个生成的二进制文件进行标注,标明是按哪个版本的proto文件生成的。


接口说明

为了通用性,Python版本提供了GeneralProtoReaderGeneralProtoWriter类,需要序列化的类名可以通过参数传递进去。由于C++无法将类名作为参数传递,采用模板编程的方法,来指定类名。

Python版本的主要内容包含在example_person.py中,就对该文件进行讲解:

# -*- coding:UTF-8 -*-import sys
import os
import proto_pb2.Person_pb2 as GeneralProto
import proto_buf.Person_buf_read as PersonRead
import proto_buf.Person_buf_write as PersonWrite
import simplejsonif __name__ == "__main__":if not os.path.exists(os.path.join(os.getcwd(), "data")):os.mkdir("data")protofile = os.path.join(os.getcwd(), "data/Person_test.proto")version = 20191001# 生成writer,需要指定三个参数:# 生成的二进制文件的路径,proto结构中的基本message,版本号(可忽略,默认值20191001)writer = PersonWrite.GeneralProtoWriter(protofile, GeneralProto.Person, version)person1 = GeneralProto.Person()person1.id = 100000person1.name = "zhangsan"person1.age = 20person1.email.append("123456@qq.com")person1.email.append("234567@qq.com")phone1 = person1.phone.add()phone2 = person1.phone.add()phone1.number = "987654"phone1.type = GeneralProto.PhoneType.MOBILEphone2.number = "876543"phone2.type = GeneralProto.PhoneType.HOMEaddr = person1.addressaddr.country = "china"addr.detail = "beijing"# 将obj对象直接写入到二进制文件中writer.writeFrameData_general(person1)person2 = '{"name":"lisi","age":22,"id":200000,"email":["345678@qq.com","456789@qq.com"],"phone":[{"type":1,"number":"765432"},{"type":2,"number":"654321"}],"address":{"country":"china","detail":"nanjing"}}'# 将json字符串写入到二进制文件中writer.writeFrameData_json(person2)# 关闭writerwriter.stopWriter()# 生成reader,需要指定两个参数:# 需要解析的二进制文件的路径,proto结构中的基本messagereader = PersonRead.GeneralProtoReader(protofile, GeneralProto.Person)# 获得版本号version_read = reader.getVersion()# 获得二进制数据的个数(帧数)frame_count = reader.getFrameCount()# 定位帧数(必须)reader.setFrameIndex(0)# 获得该帧的字段内容print(reader.getFrameData_general("id"))print(reader.getFrameData_general("name"))print(reader.getFrameData_general("age"))print(reader.getFrameData_general("email"))print(reader.getFrameData_general("phone"))print(reader.getFrameData_general("address.country"))print(reader.getFrameData_general("address.detail"))reader.setFrameIndex(1)# 将某帧的二进制内容解析,并转换成json字符串json = reader.getFrameData_json()print(json)

而C++版本的主要内容也类似:

#include <iostream>
#include "General_buf_read.h"
#include "General_buf_write.h"
#include "Person.pb.h" namespace PersonProto {
    class Person;
};namespace GeneralBuf {
    template <typename T>class GeneralProtoWriter;typedef GeneralProtoWriter<PersonProto::Person> PersonProtoWriter;template <typename T>class GeneralProtoReader;typedef GeneralProtoReader<PersonProto::Person> PersonProtoReader;
};int main(int argc, char const *argv[])
{
    GeneralBuf::PersonProtoWriter writer;std::string filename = "Person_test.proto";int64_t version = 20191001;writer.startWriter(filename, version);PersonProto::Person *person1 = new PersonProto::Person();person1->set_id(100000);person1->set_name("zhangsan");person1->set_age(20);person1->add_email("123456@qq.com");person1->add_email("234567@qq.com");PersonProto::PhoneNumber *phone1 = person1->add_phone();PersonProto::PhoneNumber *phone2 = person1->add_phone();phone1->set_number("987654");phone1->set_type(PersonProto::PhoneType::MOBILE);phone2->set_number("876543");phone2->set_type(PersonProto::PhoneType::HOME);PersonProto::Address *addr = person1->mutable_address();addr->set_country("china");addr->set_detail("beijing");writer.write(person1);writer.stopWriter();GeneralBuf::PersonProtoReader reader;reader.startReader(filename);std::cout << "version : " << reader.getVersion() << "\n";std::cout << "cnt : " << reader.getFrameCount() << "\n";reader.setFrameIndex(0);PersonProto::Person *person = new PersonProto::Person();reader.read(person);person->PrintDebugString();delete person1;delete person;return 0;
}

后言

如果喜欢本篇内容,就到GitHub点赞,谢谢。

如果有什么BUG或者反馈,欢迎提出。

  相关解决方案