先给你一段字节:
55 个字节。你知道它是一段 protobuf,但
.proto 文件不在你手上。可能是上游团队没给,可能是抓包抓下来的 gRPC body,也可能是哪个老服务的日志里打了一串 byte array。反正现在的局面是:你得搞清楚这堆字节里到底装了什么,但你没有生成类,没有 schema。
很多人到这一步就卡住了。平时用 protobuf,都是先写
.proto,再 protoc 生成类,然后 parseFrom(bytes) 一把梭。生成类没了,好像就没招了。先说结论:不用。protobuf 的二进制流自带结构,没有
.proto 一样能解,甚至能反过来自己造一条出来。
1. 为什么没有 .proto 也能解
上篇【gRPC 请求变成字节后,到底长什么样】讲了 protobuf 的 wire format:每个字段在字节流里是
[tag] [value],tag 里压着字段号和 wire type。关键在于 wire type 自带"接下来这个值该读几个字节"的规则:
- VARINT(0):一直读到某个字节最高位是 0。
- I64(1):固定 8 字节。
- LEN(2):先读一个 varint 当长度 N,再读 N 字节。
- I32(5):固定 4 字节。
每个字段的边界都能算出来。所以哪怕完全不知道 schema,也能像切香肠一样,把字节流切成一段段独立的字段。

当然,没有
.proto 不是没代价。你丢了两样东西:字段名。 你只知道这是"字段 4",不知道它叫
score_delta。精确的语义类型。 同样是 VARINT 的
9,它可能是 int 的 9,也可能是 sint ZigZag 后的 -5,还可能是 enum 的第 9 项。光看字节分不出来。同理一个 LEN,可能是字符串、bytes、嵌套 message 或 packed 数组。所以没有 schema 时,正确做法不是硬猜一个唯一答案,而是把原始值留着,把几种可能的解释都列出来。
2. Java 落地:UnknownFieldSet
先排除一个容易想到的方案:
DynamicMessage。它能不依赖生成类,但需要喂一个 Descriptor——本质还是 schema。我们的前提是没有 schema,所以它出局。真正合适的是:
它本来干嘛的?protobuf 为了向前兼容,解析时碰到不认识的字段不会报错,而是塞进
UnknownFieldSet。它内部按字段号,把值分到四个桶里:varint、fixed32、fixed64、length-delimited——正好对应 4 种 wire type。我们可以直接拿它当「无 schema 编解码器」用:
- 编码:往四个桶里塞值,
toByteArray()序列化。tag 怎么写、长度怎么算,库全包了。
- 解码:
UnknownFieldSet.parseFrom(bytes),然后按字段号取各桶里的原始值。

2.1 编码:手搓一条 SearchRequest
直接用
UnknownFieldSet 有个麻烦:它只认 wire 表示。你不能直接丢个 -5 进去,得先 ZigZag;不能直接丢个 0.85,得先转位模式。所以我写了个小工具类 
。
ProtobufWireUtils,把这些预处理包掉,代码见protobuf-codec-demo
Github
protobuf-codec-demo
Owner
yibinerUpdated
Jun 29, 2026核心方法:
有了它,组装那条 SearchRequest 就很直白:
2.2 验证:和官方生成类逐字节比
你可能会问:手搓出来的字节,真的和
protoc 正经生成类编出来的一样吗?这个得验。
protobuf-java 自带了
Int32Value、StringValue、DoubleValue、FloatValue、BoolValue 这些 wrapper 类型,都是 protoc 生成的,里面就一个字段 value = 1。用
UnknownFieldSet 手搓字段 1,再跟官方类的 toByteArray() 逐字节比。真实运行结果:五个类型,逐字节一致。
UnknownFieldSet 写出来的,就是标准 protobuf,谁来解都认。我又用 gRPC 的 marshaller 和
DynamicMessage 做了三方对比,字节也完全一致(详见protobuf-codec-demo)。2.3 无 schema 解码
反过来。把那 55 字节当成"天上掉下来的"数据,用
parseFrom 解,遍历每个字段,把 wire type 和候选解释都打出来:没有 schema,整棵结构都出来了。嵌套 message 也自动递归进去了。
field 4 同时给了
uint=9 和 sint=-5。它到底是 int 还是 sint?光看字节分不出来,两种都摆出来让你判断。
2.4 一个必须说的局限:packed
field 8 本来是
repeated int32 [10, 20, 30],但解码输出里是:被当成 bytes 了,不是数组。
这不是 bug,是没 schema 时的本质局限。
0a 14 1e 这三个字节,可以解释成 packed 的 [10, 20, 30],也可以就是三个原始字节。光看 wire 分不出来。有 schema 才知道字段 8 是 repeated int32,才敢按 packed 解。很多解码工具会用启发式去猜:能不能整齐拆成连续 varint、是不是不像可打印字符串……猜对的概率不低,但终究是猜。
2.5 有 schema 就能完整还原
补上类型信息后,所有值精确还原:
塞进去什么,原样拿回来什么。一条
.proto 没写,编解码闭环跑通。3. 手解太累了
回头看,55 字节我画了张表才讲明白。真实业务里随便一条消息几百字节、嵌套四五层,靠肉眼对着 hex 数字节,谁顶得住。
而且 packed 到底是不是数组、那个 LEN 到底是字符串还是子消息,全得人去判断。联调 protobuf 协议、定位中间件问题的时候,你真没工夫一个字节一个字节去数。
所以我在github上找到了可以在浏览器中解码pb数据流的工具: , 并在其基础上做了优化 
。
ProtoDecodeX
Github
ProtoDecodeX
Owner
yibinerUpdated
Mar 30, 20264. ProtoDecodeX:浏览器里解 protobuf
ProtoDecodeX 是一个跑在浏览器里的 protobuf 解码工具。贴一段数据进去,它帮你识别格式、切字段、递归嵌套、给候选解释。
它想解决的问题很具体:
- 这段 protobuf 二进制里到底有哪些字段?
- 这份数据到底是 hex、Base64 还是 Java 字节数组?
- 某个 bytes 字段是不是嵌套了另一层 message?
- 能不能贴一小段
.proto,把字段号映射成名字?
4.1 支持的输入格式
做联调的时候,日志里拷出来的字节格式五花八门。有的是 hex,有的是 base64,有的是 Java 打出来的十进制数组。ProtoDecodeX 自动识别格式,不需要你手动转:
格式 | 示例 |
十六进制 | 08 96 01 |
Base64 | CJYB |
Java 十进制字节数组 | 8, 150, 1 |
Java 转义字符串 | \010\226\001 |
本地文件上传 | 任意二进制文件 |
粘进去就行,它自动转成规范化 Hex 再解码。
4.2 本地解码,没有数据安全风险
这点是硬需求。很多场景你解的是线上业务数据,不可能随便贴到某个在线工具的后端去。
ProtoDecodeX 的所有解码逻辑都在浏览器端用 JavaScript 完成,不依赖远程 API,不上传数据到服务端。本地文件也是浏览器直接读取。
你在公司内网或敏感环境使用,数据不会离开你的浏览器。
4.3 其他功能
gRPC 帧头识别。 如果输入带 gRPC 的 5 字节 framing header,会自动跳过再解析。省一步手工操作。
多消息流解析。 一段数据里串着多条 protobuf 消息,每条前面有 varint 长度前缀的那种,它能按消息边界拆开。这种场景在流式传输和批量消息里挺常见。
轻量 schema 支持。 可以在高级面板贴一小段
.proto 定义,把字段号映射成字段名和类型。不用完整编译,能辅助阅读就够了。Smart Decode / XOR。 有些业务载荷会把数据塞在 bytes 字段里再做一层 XOR。工具支持条件 XOR 和手动 XOR,解完还能直接切回主输入继续往下看。
5. AI Skill 版本:让 Agent 帮你解
除了浏览器版,我还做了一个面向 AI 编程工具的版本:proto-decode-x-skill。
它是一个 Cursor / Claude Code 的 Skill 包。装上以后,你可以直接在 IDE 里让 Agent 帮你解 protobuf 二进制数据。
Skill 定义了完整的解码流程:
- 自动识别输入格式(hex、base64、Java 字节数组、Java 转义串)
- 按 protobuf wire format 解码,支持 gRPC 5 字节头识别
- 嵌套 message 递归展开
- VARINT / FIXED32 / FIXED64 / LEN 每个字段都给出多种候选解释
- 用固定的树形格式输出,方便阅读和对照
使用场景很直接:联调 protobuf 协议、定位中间件问题时,直接在编辑器里让 Agent 解一段字节。比切到浏览器里贴数据更顺手。
输出长这样:
每行都有字段号、wire type、原始值和候选解释。不需要你再自己去翻 wire format 规则。
6. 小结
没有
.proto 不可怕。protobuf 的 wire format 自带结构,靠 tag 里的字段号和 wire type 就能切出字段边界。Java 里用官方的 UnknownFieldSet,parseFrom 解、toByteArray 编,不用自己撸 varint。手解小段数据够用,但真实业务场景用工具更高效:
- AI Skill 版:proto-decode-x-skill
文中所有编解码代码都在配套工程里,
mvn test 可以直接跑通编解码闭环。如果这两个工具对你有帮助,欢迎去 GitHub 点个 star。
- 作者:Yibin
- 链接:https://yibin.dev/article/39160b50-99a4-8092-8655-d71855627c41
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
相关文章






