乌啦呀哈呀哈乌啦!

欢迎光临,这里是喵pass的个人博客,希望有能帮到你的地方

0%

Google Protobuf

Google Protocol Buffers(简称 Protobuf)是一套“数据结构定义 + 代码生成 + 二进制序列化”工具。
先在 .proto 文件里约定字段,再生成 C#、Java、Go 等语言的读写代码,让客户端和服务端遵守同一份协议。

数据定义

以这份玩家数据为例:

1
{"chips":150,"money":12.5,"bonus":0}

不带空格的 UTF-8 JSON 是 34 字节。
JSON 是文本格式,字段名、数值、字符串以及标点,最终都编码成文本字节传输。

比如 {“chips”:150} 的chips字段,除了字段名本身,还需要两个字节传输引号。 连同冒号,”chips”: 共占 8 字节。

这里 150 是 JSON 数值,占 3 字节。
接收方解析后,才会把数值文本 150 转换为内存中的数字;它在 JSON 传输时并不是一个固定 4 字节的整数。
类似地,12.5 传的是四个字符 1、2、.、5,JSON 本身也不区分 float 和 double,由接收程序决定怎么存储。

你的protobuf数据可以定义为:

1
2
3
4
5
6
7
syntax = "proto3";

message PlayerBalance {
int32 chips = 1;
float money = 2;
float bonus = 3;
}

这里的 1、2、3 是固定的字段编号,不是字段值。

按上面的协议,Protobuf 二进制通常只需要 8 字节:

内容 JSON Protobuf
字段标识 每次发送 "chips" 等字段名 发送字段编号和编码类型组成的标签
整数 150 3 个文本字节 2 个变长整数字节
浮点数 12.5 文本表示 float 固定 4 字节
bonus = 0 仍包含字段名和值 此例中,proto3 默认值省略
结构 花括号、引号、冒号、逗号 无须这些文本符号

这个例子里的 8 字节,chips 占 3 字节,money 占 5 字节,bonus 不占空间。(每个字段标签占1字节)
Protobuf的标签不包含字段名,也不一定直接包含字段长度。标签包含这个字段的部分元数据:字段编号和底层编码方式。

整条消息的结束位置则由外层提供,例如 HTTP 响应体的长度或嵌套消息的长度前缀。Protobuf 本身没有一个通用的“消息结束符”,所以在连续的 TCP 字节流中发送多条消息时,需要另外约定每条消息的长度。

数据结构

字段编号与字段名的对应关系需要额外存储。
Protobuf 的关键是:把这份映射提前放在客户端和服务端的协议定义、生成代码里,而不是每次发送数据时重复传输。

所以它用“双方提前约定数据结构”的成本,换取“每条消息更小”的收益。

但缩小比例取决于数据:大量重复字段名、数字、列表通常收益明显;很长的字符串内容仍然要传。
Protobuf 不是压缩算法,也不是加密算法,还可以与 gzip 等压缩一起使用。
若用 Protobuf 的 JSON 输出模式,传输的仍是 JSON,拿不到上述二进制收益。

接入时需要注意这几件事:

  1. 客户端和服务端共同支持协议。 如果服务端仍然只接受表单和 JSON,单独换 Unity 序列化方式无法使用。
  2. 网络层改为处理字节。 通常用 UploadHandlerRaw 发送,读取 downloadHandler.data 接收;Protobuf 也可以继续通过普通 HTTP 使用,不要求 gRPC。
  3. Unity 可以使用 Google.Protobuf,但接入后仍需验证程序集引用和目标平台的 IL2CPP 构建

字段映射

开发时,.proto 文件就是协议定义:

字段编号 字段名 数据类型
1 chips int32
2 money float
3 bonus float

通过 protoc 编译器,可以为客户端生成 C# 类,为服务端生成对应语言的类。

字段名还可能保存在生成的描述信息中,用于反射、JSON 转换等功能。这些内容属于程序代码和元数据,不需要跟着每条网络消息发送。
可以理解为双方事先约定:

1
“编号 1 表示筹码,编号 2 表示余额,编号 3 表示奖励余额。”

此后每次通信只发送“编号和值”。

JSON 则会在每次通信时重复发送:

1
“chips 的值是 150,money 的值是 12.5……”

因此,Protobuf 并没有消灭映射,而是把映射从每条消息里移到了双方程序里。

以及二进制里并不是只有字段值,Protobuf 不能直接把 150 和 12.5 连起来,因为接收方需要知道:

  • 这是哪个字段?
  • 这个字段的值占多少字节?
  • 读完后下一个字段在哪里?

所以每个实际发送的字段,基本结构是:

1
字段标签 tag + 字段值

对于字符串等可变长度的数据,则是:

1
字段标签 tag + 内容长度 + 内容