当前位置: 首页 > news >正文

用得更顺手的 Protobuf 文件后缀、流式多消息、大数据集与“自描述消息”实战

一、文件后缀约定:看名字就知道内容

同一条消息,常会以多种格式存盘或传输。统一后缀能让同事和工具一眼识别

内容推荐扩展名说明
文本格式(TextFormat).txtpb.textproto 也常见,但更推荐更短的 .txtpb
线格式 / 二进制(Wire).binpb直接是 Protobuf 的 wire bytes
JSON.json便于与非 protobuf 系统对接

工程建议

  • 产物、工件和样例统一后缀,CI 用后缀做快速校验
  • 测试夹带样例时,foo_request.txtpb / foo_response.binpb 直观省劲。

二、在一个文件/流里写入“多条消息”的正确做法

Protobuf 的线格式不是自定界的,解析器无法仅靠字节自己判断“消息到哪儿结束”。
通用方案:前置长度(length-prefix framing)——写入每条消息前,先写消息长度

2.1 基本流程

写入端

  1. 序列化消息 → 得到 bytes
  2. 写入 len(bytes)(长度可以用 varint 或固定 32-bit 小端)
  3. 写入 bytes

读取端

  1. 先读长度 N
  2. 读取接下来 N 个字节到缓冲区
  3. 从缓冲区解析该条消息

想避免复制?C++/Java 的 CodedInputStream 可限制可读字节数(push limit),在原始流上直接解析。

2.2 示例:长度用 varint(通用且更省空间)

Java(MessageLite 自带的简化 API)

// 写
try (var out = Files.newOutputStream(Path.of("stream.bin"))) {for (MyMsg m : messages) {m.writeDelimitedTo(out); // 先写长度的 varint,再写内容}
}// 读
try (var in = Files.newInputStream(Path.of("stream.bin"))) {MyMsg msg;while ((msg = MyMsg.parseDelimitedFrom(in)) != null) {handle(msg);}
}

C++(CodedOutput/Input 流控制长度)

std::ofstream ofs("stream.bin", std::ios::binary);
google::protobuf::io::OstreamOutputStream zero_copy_out(&ofs);
google::protobuf::io::CodedOutputStream coded_out(&zero_copy_out);for (const MyMsg& m : messages) {const size_t size = m.ByteSizeLong();coded_out.WriteVarint32(static_cast<uint32_t>(size));uint8_t* buffer = coded_out.GetDirectBufferForNBytesAndAdvance(size);if (buffer) {m.SerializeWithCachedSizesToArray(buffer);} else {m.SerializeToCodedStream(&coded_out);}
}

Python(手动写 varint + bytes)

from google.protobuf.internal.encoder import _VarintEncoder
from google.protobuf.internal.decoder import _DecodeVarint32
from my_pb2 import MyMsgdef write_delimited(f, msg: MyMsg):body = msg.SerializeToString()_VarintEncoder()(f.write, len(body), False)f.write(body)def read_delimited(f):while True:# 读 varint 长度buf = f.read(1)if not buf:breaksize, _ = _DecodeVarint32(buf + f.read(4), 0)  # 简化示例body = f.read(size)m = MyMsg()m.ParseFromString(body)yield m

实践提示

  • 固定 32-bit 小端长度也很常见(便于 mmap/随机访问),但跨语言时 varint 更通用。
  • 流中允许混写不同消息类型,但强烈建议在外层加类型字段通用 envelope,方便消费端分流。
  • 错误恢复:分帧出错时,至少能丢弃当前帧并继续读下一条,避免整条流作废。

三、面对“大数据集”:别把一切塞进“一条消息”

Protobuf 不是为了“超大单条消息”设计的:>1MB/条就要警惕。
正确姿势:把大数据集拆成许多小记录,每条是结构化的小消息

常见组织方式

  • RecordIO / TFRecord / 自定义顺序文件:一条条连续存储(配合 length-prefix)。
  • 分片 + 索引.binpb 分片 + 侧写 .idx(offset, length, key),快速随机访问。
  • 数据库/对象存储:把每条消息当 value;键做主键或二级索引;配合压缩。
  • 流式处理:Kafka/Pulsar 等消息队列承载“每条消息=一条 protobuf”。

工程建议

  • 需要顺序稳定→ 自己维护排序键(时间戳、单调序列号)。
  • 开启压缩(zstd/gzip),对重复结构和文本字段收益明显。
  • 尽量按列拆分放重复大字段(如长文本、blob),避免热路径传来传去。
  • 监控“单条消息体积”分布,超过阈值报警,避免慢查询和 OOM。

四、“自描述消息”:带上 schema,自我解析

裸的 protobuf 不包含类型描述;只有 .proto 才知道字段含义。
如果你需要在没有本地 .proto 的地方也能解析消息,可以把**描述符(descriptor)**连同消息一起带上。

4.1 原理

  • protoc --descriptor_set_out=... 能导出 FileDescriptorSet(一组 .proto 的描述集合)。
  • 用一个 envelope,把 FileDescriptorSet 和真正的 Any 消息一起打包:
syntax = "proto3";import "google/protobuf/any.proto";
import "google/protobuf/descriptor.proto";message SelfDescribingMessage {google.protobuf.FileDescriptorSet descriptor_set = 1; // 类型 + 依赖google.protobuf.Any message = 2;                      // 真正载荷
}

4.2 构建与解析

导出描述符集合

protoc -I=protos \--include_imports \--descriptor_set_out=out/desc.pb \protos/my/schema/**/*.proto

打包端(以 Java 为例)

// 假设要打包 MyEvent
Any any = Any.pack(myEvent); // type_url 自动生成为 type.googleapis.com/pkg.MyEventSelfDescribingMessage sdm = SelfDescribingMessage.newBuilder().setDescriptorSet(descriptorSetFromFile("out/desc.pb")).setMessage(any).build();

解析端(Java 动态解析)

var fds = SelfDescribingMessage.parseFrom(bytes).getDescriptorSet();// 1) 用 FileDescriptorSet 构建动态 DescriptorPool
var pool = com.google.protobuf.DynamicMessage.getDefaultInstance(com.google.protobuf.DescriptorProtos.FileDescriptorSet.getDescriptor()
);
// 实际上需要递归地把 FileDescriptorProto 转为 FileDescriptor 并注册到池中// 2) 从 Any 的 type_url 拿全名,查找 Descriptor
String fullName = com.google.protobuf.Any.parseFrom(sdm.getMessage().toByteArray()).getTypeUrl().replace("type.googleapis.com/", "");// 3) 根据 Descriptor 构造 DynamicMessage 并解析
var desc = /* 从 pool 里 lookup fullName */;
var dm = com.google.protobuf.DynamicMessage.parseFrom(desc, sdm.getMessage().getValue());// 4) 通过反射读取字段
for (var field : desc.getFields()) {Object v = dm.getField(field);// 业务处理...
}

注意事项

  • 这项技术依赖描述符 + 动态消息能力(C++/Java 有;其他语言请先确认支持情况)。
  • 体积与安全:带上整个 FileDescriptorSet 会放大字节;仅打包必要子集并校验来源,避免“任意 schema 注入”带来的风险。
  • 场景优先级:若已有Schema Registry(集中管理 schema 与版本),通常更轻量;自描述更适合自包含工具离线样本跨团队调试包

五、什么时候该用 / 不该用“自描述消息”?

适合

  • 通用工具(抓包→转文本、跨语言可视化、字段 diff)
  • 交付可自解释的数据包(审计归档、复现 bug)
  • 异构/临时环境需要解析消息,但不能部署完整 schema

不太适合

  • 高吞吐在线链路(字节膨胀 + 动态解析成本)
  • 已有完善的schema 分发/注册体系(重复造轮子)

六、即拿即用的实践清单

分帧(多消息流)

  • 长度前缀(varint 或 fixed32),写读都用同一规范
  • 出错恢复策略:能跳过坏帧,继续下一条
  • 如需混合多类型 → 外层 envelope 标出类型或版本

大数据集

  • 不把“大集合”塞进“一个超大消息”
  • 分片 + 索引 + 压缩,监控单条消息大小
  • map 顺序不稳定,必要时维护显式顺序键

自描述消息

  • --descriptor_set_out --include_imports 只打包必要 proto
  • 校验 type_url / 限制可接受的包名前缀
  • 评估字节开销与解析延迟;在线路径慎用

文件后缀

  • .txtpb / .binpb / .json 一致化
  • CI 对后缀做快速 sanity check

七、结语

  • 统一后缀让产物更可读;
  • 长度前缀把多条消息装进同一条流;
  • 小消息拼大集合避免“巨型单条”;
  • 在需要“自我解释”的地方,考虑自描述消息schema registry

这些技巧看似“工程杂活”,却是让 Protobuf 在复杂系统里更耐用的关键。需要示例工程或把你们现有产物改造成长度前缀/自描述格式?把约束告诉我,我可以直接给到可运行的模板脚手架代码


文章转载自:

http://rg4BH3Pd.rrbhy.cn
http://Ha55GJd0.rrbhy.cn
http://0osadAft.rrbhy.cn
http://atDySmei.rrbhy.cn
http://Oq7tG1jG.rrbhy.cn
http://AkUJ1y8O.rrbhy.cn
http://noUet1si.rrbhy.cn
http://YJf1JaMZ.rrbhy.cn
http://4BPwjLVJ.rrbhy.cn
http://EP9leZCM.rrbhy.cn
http://l8MVVTiM.rrbhy.cn
http://2A1PLtbe.rrbhy.cn
http://rCS6peMu.rrbhy.cn
http://a6HR4yw1.rrbhy.cn
http://e4qY9iKY.rrbhy.cn
http://QJOcMkK2.rrbhy.cn
http://9Tnf7YL1.rrbhy.cn
http://nCCzqVR4.rrbhy.cn
http://VxYlpjW1.rrbhy.cn
http://eJVF16Fo.rrbhy.cn
http://cicc9UjD.rrbhy.cn
http://LSDA0KCZ.rrbhy.cn
http://pFAj4K1K.rrbhy.cn
http://7W87jquR.rrbhy.cn
http://ifnjyNeA.rrbhy.cn
http://vV8cPov8.rrbhy.cn
http://C8IjIZau.rrbhy.cn
http://tycZpvN3.rrbhy.cn
http://dcgwLuxW.rrbhy.cn
http://L1HmJ9RQ.rrbhy.cn
http://www.dtcms.com/a/368707.html

相关文章:

  • 机器学习入门,用Lima在macOS免费搭建Docker环境,彻底解决镜像与收费难题!
  • 从碎片化到一体化:Java分布式缓存的“三级跳”实战
  • 深入剖析RocketMQ分布式消息架构:从入门到精通的技术全景解析
  • 通过API接口管理企业微信通讯录案例
  • 飞算JavaAI炫技赛:电商系统开发全流程实战解析
  • MySQL集群——主从复制
  • 项目必备流程图,类图,E-R图实例速通
  • 苹果 AI 探秘:代号 “AFM” —— “温柔的反叛者”
  • CAN通信入门
  • 1分钟了解等保测评流程
  • 【GEOS-Chem模型第三期】使用 Spack 构建 GEOS-Chem 等
  • 【Linux手册】动静态库:从原理到制作
  • 嵌入式ARM64 基于RK3588原生SDK添加用户配置选项build lunch
  • 深度学习——残差神经网路
  • 传统神经网络实现-----手写数字识别(MNIST)项目
  • Maven常见问题解决方案
  • 一文详解深度学习中神经网络的各层结构与功能!
  • Java全栈开发工程师面试实录:从基础到实战的深度探讨
  • Unity打包Android应用常见问题解决指南
  • Snow Shot(截图工具) v0.2.6
  • LeNet-5:手写数字识别经典CNN
  • C++ opencv RTSP小工具 RTSP流播放、每一帧保存
  • android View详解—动画
  • 2024年9月GESPC++三级真题解析(含视频)
  • ASP.NET Core文件分片上传
  • OCA、OCP、OCM傻傻分不清?Oracle认证就看这篇
  • 面试了一个外包公司,面试不到5分钟就出来,这问题问得有点变态。。。。。。
  • Matlab使用小技巧合集(系列四):Table类型高效用法与数据处理实战
  • 25高教社杯数模国赛【C题超高质量思路+可运行代码】第十弹
  • WinForms 项目里生成时选择“首选目标平台 32 位导致有些电脑在获取office word对象时获取不到