调试自己的应用时抓到一个请求或响应,body 是二进制的:application/x-protobuf、application/grpc 或 application/grpc-web+proto。DevTools 里只能看到一串乱码,描述它的 .proto 又在另一个仓库里,或者在你手头没有的那个构建版本里。装了 protoc 的话,把字节存成文件跑一下 protoc --decode_raw 就能看结构。这个页面把同样的事搬进浏览器:贴上字节,直接看字段。
使用步骤
- 拿到字节:在 DevTools 里把请求或响应 body 复制成十六进制或 base64,或者另存为文件。想先试一下,可以点“载入示例”。
- 粘贴到上方的输入框(十六进制和 base64 会自动识别),点“解码”;也可以打开二进制文件。gRPC 的 body 请保持勾选“检测 gRPC / gRPC-Web 分帧”。
- 先看概要(输入类型、字节数、分帧),再看与
protoc --decode_raw一致的文本视图;切换到树形视图,可以逐个字段换成其他解读。
真实输出示例
我们让 protoc 36.2 把 Google 的 timestamp.proto 的描述写成二进制 FileDescriptorSet(258 字节),把它的十六进制粘贴到本页,点“解码”,再把页面显示的结果复制下来。排版做了压缩(标签和值放在同一行,省去按钮),所有值都原样保留。文本视图与 protoc --decode_raw 逐字节一致。
样本: google/protobuf/timestamp.proto(BSD-3-Clause,随 protoc 36.2 提供)的 FileDescriptorSet,2026-11-02 用 protoc --descriptor_set_out 生成
概要
输入 十六进制文本
字节数 258
分帧 无(单条 protobuf 消息)
顶层字段数 1
消息:文本(protoc --decode_raw)
1 {
1: "google/protobuf/timestamp.proto"
2: "google.protobuf"
4 {
1: "Timestamp"
2 {
1: "seconds"
3: 1
4: 1
5: 3
10: "seconds"
}
2 {
1: "nanos"
3: 2
4: 1
5: 5
10: "nanos"
}
}
8 {
1: "com.google.protobuf"
8: "TimestampProto"
10: 1
11: "google.golang.org/protobuf/types/known/timestamppb"
31: 1
36: "GPB"
37: "Google.Protobuf.WellKnownTypes"
}
12: "proto3"
}能做什么
输入。 把 body 以十六进制(空格、冒号、逗号以及 0x、\x 前缀都会被忽略)或 base64(标准或 URL 安全字母表,有没有补位的 = 都行)粘贴进来,也可以直接打开原始字节文件,最大 16 MiB。输入格式自动识别:只含十六进制字符的文本按十六进制读,其余按 base64 读。如果你的 base64 恰好全由 0-9、a-f 组成,手动选 Base64 即可。
gRPC 分帧。 gRPC 和 gRPC-Web 的 body 并不是裸的 protobuf:每条消息前面有 1 字节标志位和 4 字节大端长度,流式响应里会连着好几帧。字节能被完整切成这样的帧时,每一帧单独展示:
- 标志
0x00:普通消息,直接解码; - 标志
0x01:压缩过的消息。开头像 gzip 或 zlib 的(grpc-encoding: gzip或deflate)用浏览器自带的DecompressionStream解压后再解码;snappy、zstd 等其他编码只做提示,不去猜; - 标志
0x80:gRPC-Web 的 trailers 帧,按文本显示(grpc-status、grpc-message)。
gRPC-Web-text(application/grpc-web-text)的 body 就是这些帧的 base64,有时是好几段各自带补位的 base64 首尾相接,原样粘贴即可。如果想把字节当成单条消息解,取消勾选 检测 gRPC / gRPC-Web 分帧。整个 body 是一个 gzip 流时,会先解压。
文本视图。 默认视图和 protoc --decode_raw 的输出逐字节相同:每个字段一行 字段号: 值,varint 按无符号十进制,fixed32 和 fixed64 按补零的十六进制,嵌套消息用花括号,其余按 C 风格转义的字符串输出。可以复制或下载成 .txt,两次抓包的结果拿任何文本 diff 工具就能比。
树形视图。 wire format 只记录值是怎么编码的,不记录它是什么意思,所以树形视图把讲得通的解读都列出来:
- varint:无符号、有符号
int64(最高位为 1 时)、zigzag 的sint,值为 0 或 1 时还有bool; - fixed32 和 fixed64:无符号、有符号,以及
float或double; - length-delimited:嵌套消息、UTF-8 字符串、packed varint、packed fixed32 或 fixed64,以及十六进制字节。每个字段都有按钮切换解读,默认显示哪一种和 protoc 的优先顺序一致(先当消息);
- 已废弃的 group(wire type 3 和 4)和 protoc 一样按嵌套字段显示。
每个字段还标出字节偏移,方便在十六进制编辑器里对照。
字段类型是怎么判断的
protoc 对每个非空的 length-delimited 值都先试着当嵌套消息解析:能完整解析成字段就按消息输出,否则按字符串输出。所以两个字节的字符串 "hi" 会被打印成 1 { 13: 105 }:0x68 恰好是合法的 tag(字段 13,varint),0x69 是合法的值。文本视图保留 protoc 的这个选择,树形视图里可以把这个字段切到 字符串。
解码器照着 protoc 的两套解析逻辑实现。顶层按 C++ ParseContext 的规则读(tag 和长度最多 5 字节,长度小于 2 GiB,group 最多嵌套 100 层);嵌套的值按 TextFormat::Printer 的方式重新解析,预算从 10 开始、每深一层减 1,所以嵌套超过 10 层的消息在 protoc 和这里都会显示成字符串。
我们在 macOS 上用 libprotoc 36.2 做了对照。为本页写了 10 个测试消息(全部标量类型、packed 与非 packed 的 repeated 字段、sint32 和 sint64、含中文和 emoji 的字符串、map、oneof、空消息、最大到 536,870,911 的字段号、proto2 group、一棵 12 层的树),用 protoc --encode 编码后,文本输出与 protoc --decode_raw 逐字节一致。另外 1,000 个随机生成并随机篡改过的输入(包括 protoc 两套解析逻辑表现不同的超长 varint),凡是 protoc 能解的,输出文本都相同;protoc 拒绝的,这里也都报错。
字节不是合法 protobuf 时
protoc 会直接报 Failed to parse input.,什么都不输出。这个页面会把出错位置之前的字段解出来显示,并给出出错的字节偏移和原因:值被截断、本该是 tag 的位置出现 0x00(常见于填充字节或末尾的校验和)、字段号为 0、wire type 6 或 7、没有起始的 end-group tag、超过 10 字节的 varint。如果一个字段都解不出来,先确认粘贴的是 body 而不是整段 HTTP 报文,再试试打开或关闭分帧检测。
数据如何保持私密
- 解码器运行在一个单独的框架(iframe)里,来源是
tools.rea-reverse.com。下面说的处理都在这个框架里、在你的设备上完成。 - 这个页面在正文之后展示一个广告。广告脚本运行在
rea-reverse.com上,而框架属于另一个源(origin),浏览器不允许广告脚本或本页的任何其他脚本读取你粘贴的内容、打开的文件或结果。 - 框架的内容安全策略拦截它的脚本能发起的所有请求(
connect-src 'none',也不加载任何第三方资源)。框架不能自行打开窗口或让本页跳转;只有你点击报告里的链接时才会打开页面。它传给本页的只有自身高度和下面这些统计事件。 - 解码在你设备上的 Web Worker 里完成,粘贴或打开的内容都不会上传。
- 字段值一律按纯文本显示,payload 里的任何内容都不会被当成 HTML 解析或执行。
- 统计事件只有粗粒度信息:成功、
malformed或错误码,字节的来源(hex、base64或file),是否识别出 gRPC 分帧,大小区间和耗时区间。绝不包含字节内容、字段号或字段值。框架把事件交给本页,由本页发送。完整列表见隐私政策。 - 不想要任何广告和统计?在单独页面打开解码器。
如何自己验证
- 打开浏览器开发者工具的 Network(网络) 面板,加载本页。发往
tools.rea-reverse.com的请求就是框架:它的 HTML、样式、字体和脚本,以及 Worker 脚本/_astro/protobuf-worker-*.js。发往 Google 域名的请求是本页的广告。 - 清空日志,然后解码一段数据。在过滤框里输入
domain:tools.rea-reverse.com:最多只会再出现一次 Worker 脚本/_astro/protobuf-worker-*.js(为下一次运行准备的新 Worker),没有任何请求带着你的数据。广告请求可能还会继续,但它们来自本页,不带框架里的任何内容。 - 点开来自
tools.rea-reverse.com的protobuf-decoder/文档,查看 Headers:它的Content-Security-Policy里有connect-src 'none'和frame-ancestors https://rea-reverse.com。在 Chrome 里,Application(应用)> Frames 也能看到框架的源和策略。Worker 运行在default-src 'none'; script-src 'self'; connect-src 'none'这条策略下,浏览器不允许它发出任何网络请求。 - 检查隔离:在 Console(控制台) 里,上下文选
top,运行document.getElementById('tool-frame').contentDocument,结果是null;frames[0].document会抛出SecurityError。广告脚本拿到的也是这个结果。 - 或者先打开页面再断网(关掉 Wi-Fi,或在 Network 面板勾选 Offline),然后解码,照样能用。
局限
- 没有字段名和类型。 没有 schema,字段 2 就只是字段 2,
string和bytes也分不出来。暂不支持粘贴.proto按名字解码:常用的 protobuf.js 用new Function动态生成解码代码,而本页的内容安全策略禁止这样做。 - 有歧义的值依然有歧义。 短字符串可能被解析成消息,一串小整数的 packed 列表也可能被当成字符串。树形视图把可能的解读都列出来,哪个对得上你的接口,由你来判断。
- 压缩只支持浏览器能解的 gzip 和 deflate 帧。snappy 和 zstd 帧会标出来,但不解码。
- 大小。 最多 16 MiB 字节。树形视图只显示前 200,000 个字段,文本视图和下载的文件始终完整。
字节从哪里来
要先抓到自己 Web 应用的流量,可以在 DevTools 里录制并导出 HAR;HAR / SAZ 接口归纳会按接口归纳请求并列出内容类型,protobuf 接口一眼就能找到,HAR 文件分析指南讲了怎么读抓包文件、怎么安全地分享。只抓取和解码你自己拥有或已获授权测试的应用与服务的流量。
对应的命令行
样本的生成方式(Homebrew 安装的 protoc 36.2,常用的 .proto 文件放在 /opt/homebrew/include 下):
$ protoc -I/opt/homebrew/include --descriptor_set_out=timestamp.desc google/protobuf/timestamp.proto$ wc -c timestamp.desc
258 timestamp.desc不带 schema 的原始解码,输出与上面页面的文本视图完全相同:
$ protoc --decode_raw < timestamp.desc
1 {
1: "google/protobuf/timestamp.proto"
2: "google.protobuf"
4 {
1: "Timestamp"
…
12: "proto3"
}带上 schema(descriptor.proto)后,同样的字节有了字段名:字段 4 是 message_type,字段 8 是 options;字段内部 3 是 number,5 是 type(3 = TYPE_INT64,5 = TYPE_INT32):
$ protoc -I/opt/homebrew/include --decode=google.protobuf.FileDescriptorSet google/protobuf/descriptor.proto < timestamp.desc
file {
name: "google/protobuf/timestamp.proto"
package: "google.protobuf"
message_type {
name: "Timestamp"
field {
name: "seconds"
number: 1
label: LABEL_OPTIONAL
type: TYPE_INT64
json_name: "seconds"
}
…
syntax: "proto3"
}常见问题
没有 .proto 文件,怎样解码 protobuf?
把字节以十六进制或 base64 粘贴进来,点“解码”。你会看到字段编号、wire type 和值,与 protoc --decode_raw 的输出相同;字段名和确切类型只存在于 schema 里,上面描述文件的例子就说明了这一点。
和 protoc --decode_raw 一样吗?
文本视图与 libprotoc 36.2 的 protoc --decode_raw 逐字节一致,包括它“优先当成嵌套消息而不是字符串”的判断和 10 层嵌套上限。树形视图在此之外列出每个字段的其他可能解读。
怎样解码 gRPC-Web 响应?
按原样粘贴 body:二进制 gRPC-Web 用十六进制或文件,application/grpc-web-text 用 base64。勾选“检测 gRPC / gRPC-Web 分帧”后,每一帧分别解码,trailers 帧以文本显示。
为什么一个字符串被显示成了嵌套消息?
wire format 对字符串、bytes、嵌套消息和 packed 列表用的是同一种长度前缀类型,短字符串也可能恰好能解析成合法消息。和 protoc 一样,文本视图优先当作消息;在树形视图里可以把这个字段切换成字符串。
能用我的 .proto 文件解码吗?
不能。浏览器里常用的 protobuf.js 会用 new Function 生成解码函数,本页的内容安全策略不允许这样做。请在本地运行 protoc --decode=YourMessage your.proto < body.bin。
数据会上传吗?
不会。解码在你设备上的 Web Worker 里运行,这个 Worker 无法联网,你粘贴或打开的字节不会离开浏览器。
更新于
相关教程
- 没有 .proto 怎么解 protobuf:wire type、varint 与 gRPC 帧:用 protoc 编码一条消息,再假装丢了 schema,逐行对照
protoc --decode_raw读回来。