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

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


前言

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

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

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

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

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

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

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

GitHubhttps://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) & 0x00FF

for i in [0, 1, 2, 3]:
  if temp_data[i] > 127:
    temp_data[i] = temp_data[i] - 256

bin_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 simplejson


if __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 = 100000
  person1.name = "zhangsan"
  person1.age = 20

  person1.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.MOBILE
  phone2.number = "876543"
  phone2.type = GeneralProto.PhoneType.HOME

  addr = person1.address
  addr.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)
  # 关闭writer
  writer.stopWriter()

  # 生成reader,需要指定两个参数:
  # 需要解析的二进制文件的路径,proto结构中的基本message
  reader = 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或者反馈,欢迎提出。

protobuf中,可以使用bytes类型来存放二进制文件数据。protobuf是一种用于序列化结构化数据的开源库,它可以将结构化数据转换为二进制格式,以便在网络传输或存储时使用。 在protobuf中,bytes类型是一种特殊的数据类型,用于存储任意二进制数据。它可以用来表示图片、音频、视频等二进制文件的内容。在.proto文件中定义消息类型时,可以使用bytes类型来声明一个字段,例如: message MyMessage { bytes file_data = 1; } 在上面的例子中,MyMessage消息类型包含一个名为file_data的字段,它的类型是bytes。这个字段可以用来存储二进制文件的内容。 当使用protobuf编码器将结构化数据编码为二进制格式时,可以将二进制文件的内容赋值给bytes类型的字段。例如,在C++中使用protobuf库编码时,可以使用以下代码: MyMessage message; message.set_file_data(file_content, file_size); 在上面的代码中,file_content是一个指向二进制文件内容的指针,file_size是文件的大小。通过调用set_file_data函数,将文件内容赋值给file_data字段。 当使用protobuf解码器将二进制数据解码为结构化数据时,可以通过访问bytes类型的字段来获取二进制文件的内容。例如,在C++中使用protobuf库解码时,可以使用以下代码: const std::string& file_data = message.file_data(); // 使用file_data进行后续处理 在上面的代码中,通过访问file_data字段,可以获取存储在其中的二进制文件内容,并进行后续处理。 需要注意的是,protobuf并不会对二进制文件的内容进行任何处理或解析,它只是将二进制数据存储在bytes类型的字段中。因此,在使用protobuf存储二进制文件时,需要确保正确地读取和写入文件内容。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值