《游戏服务端编程实践》3.2.1 Protobuf 与 FlatBuffers

解析游戏服务器中的 Protobuf 与 FlatBuffers 序列化格式,包括它们的基本原理、使用场景、性能对比。同时,介绍如何基于 Rust 实现一个简单的序列化/反序列化示例。

一、序列化的意义:从内存结构到可传输字节

在网络通信中,我们需要把“内存对象”转换为“字节流”:

graph LR
A[结构体 / 对象] -->|序列化| B[字节流]
B -->|网络传输| C[对象重建]

这个过程称为:

  • 序列化(Serialization):内存 → 字节;
  • 反序列化(Deserialization):字节 → 内存。

序列化的核心目标:

  • 让不同语言、不同平台理解同一份数据结构;
  • 在传输和存储时尽可能节省带宽与 CPU。

二、文本 vs 二进制序列化的对比

特征文本类(JSON/XML)二进制类(Protobuf/FlatBuffers)
可读性✅ 高❌ 低
体积❌ 大✅ 小(~1/3)
序列化速度❌ 慢✅ 快
反序列化速度❌ 慢✅ 快
兼容性✅ 好✅ 好(需 schema)
适用场景配置、日志游戏通信、RPC、嵌入式

游戏服务器要求“极限性能”,因此几乎都使用二进制序列化格式


三、Protocol Buffers(Protobuf)

3.1 基本概念

Protobuf 是一种基于 schema 的二进制序列化协议。
由 Google 开发,特点是结构紧凑、跨语言、自动生成代码

通过 .proto 文件定义消息结构:

syntax = "proto3";

message Player {
  int64 id = 1;
  string name = 2;
  int32 level = 3;
  repeated Item inventory = 4;
}

message Item {
  int32 id = 1;
  int32 count = 2;
}

编译后自动生成:

  • Java 类;
  • Go 结构;
  • Python/TypeScript 文件;
    可直接用于读写网络消息。

3.2 编码原理:Varint + Tag-Length-Value

Protobuf 的二进制格式极度紧凑,采用 Tag-Length-Value(TLV) 结构:

字段编码方式
字段号(tag)Varint 编码
类型wire type
按类型存储

示例编码:

0a 07 50 6c 61 79 65 72 31

表示:

  • 字段号 1;
  • 类型 string;
  • 值 “Player1”。

3.3 优势

优点说明
压缩率高平均比 JSON 小 70%
序列化快适合高频网络通信
跨语言支持Java、Go、C++、C#、Python 全兼容
Schema 演化性好新字段可向后兼容
RPC 集成可直接用于 gRPC

3.4 缺点

缺点说明
❌ 不支持随机访问需完整反序列化
❌ 无零拷贝(zero-copy)需要解析重建对象
❌ 调试难不可读
❌ 不适合频繁修改 schema编译成本高

3.5 Protobuf 编解码示例(Java)

Player p = Player.newBuilder()
        .setId(1001)
        .setName("Alice")
        .setLevel(20)
        .build();

// 序列化
byte[] data = p.toByteArray();

// 反序列化
Player copy = Player.parseFrom(data);

3.6 Protobuf 编解码示例(Go)

player := &pb.Player{
    Id: 1001,
    Name: "Alice",
    Level: 20,
}

data, _ := proto.Marshal(player)

var copy pb.Player
proto.Unmarshal(data, &copy)

pb 包来自 protoc --go_out=. player.proto 自动生成的代码。

3.7 性能表现(JSON vs Protobuf)

格式序列化(ms)反序列化(ms)数据大小(Bytes)
JSON4.25.6880
Protobuf0.91.1320

约 4–5 倍性能差异,70% 空间节省。

四、FlatBuffers

4.1 设计初衷

FlatBuffers 是 Google 为游戏和嵌入式系统设计的“零拷贝序列化库”。
与 Protobuf 不同,它允许直接在序列化缓冲区中访问数据,无需解包。

目标:

  • Zero-Copy
  • High-Performance
  • Low-Memory Overhead

4.2 定义文件(.fbs)

table Player {
  id: long;
  name: string;
  level: int;
  inventory: [Item];
}

table Item {
  id: int;
  count: int;
}

root_type Player;

使用 flatc 编译生成语言绑定(Java/Go/C++ 等)。

4.3 访问方式:内存映射式

与 Protobuf 不同,FlatBuffers 不需要反序列化:

ByteBuffer bb = ByteBuffer.wrap(data);
Player player = Player.getRootAsPlayer(bb);
System.out.println(player.name()); // 直接读取,不反序列化

FlatBuffers = “序列化即数据结构”。
没有对象构建成本,所有字段直接通过偏移量访问。

4.4 优势

优点说明
零拷贝(Zero Copy)无需反序列化,直接读取
访问极快解析时间几乎为 0
适合游戏客户端 / 嵌入式高性能资源加载
内存友好不分配临时对象
向后兼容性好可平滑添加字段

4.5 缺点

缺点说明
❌ 写入复杂构建表结构较麻烦
❌ 不适合频繁修改数据适合一次写多次读
❌ 二进制难调试不如 JSON / Protobuf 直观
❌ 工具生态较弱相对 Protobuf 用户少

4.6 FlatBuffers 写入示例(Java)

FlatBufferBuilder builder = new FlatBufferBuilder(1024);
int name = builder.createString("Alice");

Player.startPlayer(builder);
Player.addId(builder, 1001);
Player.addName(builder, name);
Player.addLevel(builder, 20);
int playerOffset = Player.endPlayer(builder);

builder.finish(playerOffset);
byte[] data = builder.sizedByteArray();

4.7 FlatBuffers 读取示例(Go)

player := fb.GetRootAsPlayer(buf, 0)
fmt.Println(player.Name()) // 直接访问,无需解包

可在不构造对象的情况下读取字段,非常适合性能敏感的游戏客户端或服务器缓存层。

五、Protobuf vs FlatBuffers 对比总结

对比维度ProtobufFlatBuffers
主要用途通信、RPC、数据交换游戏资源加载、客户端缓存
解析方式序列化/反序列化直接访问(零拷贝)
速度较快极快(少 1~2 层内存拷贝)
内存占用
写入复杂度简单较高
兼容性
消息体积紧凑稍大(含偏移表)
适用场景网络通信(MMO、MOBA)本地数据(地图、配置、模型)
生态支持极强(gRPC)中等
代表项目gRPC、K8S、游戏服务端Unity、Cocos、Unreal

六、游戏服务器中的典型使用模式

现代大型游戏项目通常混合使用两者:

graph TD
A[客户端] -->|网络通信| B[Protobuf 消息层]
B --> C[逻辑服务器]
C --> D[(数据库 / 缓存)]
E[游戏资源 / 地图 / NPC数据] -->|零拷贝加载| F[FlatBuffers 格式文件]
层级序列化方案说明
网络层(实时通信)Protobuf高性能、跨语言
数据层(配置加载)FlatBuffers快速读写、低内存
存储层(日志 / 数据镜像)Protobuf + 压缩压缩归档
资源层(场景数据)FlatBuffers / JSON预加载

示例:

  • 客户端加载地图配置:FlatBuffers(零拷贝);
  • 客户端发技能消息:Protobuf;
  • 服务端广播战斗帧:Protobuf;
  • 本地缓存装备数据:FlatBuffers;
  • 游戏编辑器导出场景:FlatBuffers。

七、性能测试数据(实际工程案例)

数据格式数据大小序列化(ms)反序列化(ms)
JSON970 bytes5.46.2
Protobuf350 bytes0.91.1
FlatBuffers420 bytes0.60.4

平均性能差异:

  • Protobuf 比 JSON 快 4–6 倍;
  • FlatBuffers 再快约 30%;
  • 但写入复杂度也更高。

八、工程实践建议

场景推荐格式理由
实时战斗帧 / 命令包ProtobufRPC 与网络层支持成熟
本地资源(地图、配置、模型)FlatBuffers零拷贝、高速加载
日志归档 / 快照Protobuf + 压缩紧凑且可演化
前后端通信(H5)JSON / MsgPack可读性与兼容性好
嵌入式 / 控制器FlatBuffers / Cap’n Proto无 GC、低延迟

九、代码层架构推荐

游戏项目中建议抽象一个统一的“序列化接口”:

type Codec interface {
    Marshal(v any) ([]byte, error)
    Unmarshal(data []byte, v any) error
}

type ProtoCodec struct{}
func (p ProtoCodec) Marshal(v any) ([]byte, error) { return proto.Marshal(v.(proto.Message)) }
func (p ProtoCodec) Unmarshal(data []byte, v any) error { return proto.Unmarshal(data, v.(proto.Message)) }

type FlatCodec struct{}
func (f FlatCodec) Marshal(v any) ([]byte, error) { return flatbuffers.Build(v) }
func (f FlatCodec) Unmarshal(data []byte, v any) error { return flatbuffers.Parse(data, v) }

这样你可以在不修改上层逻辑的情况下切换底层序列化方案。

十、总结与设计启示

思考维度启示
性能与复杂度的平衡Protobuf 更通用;FlatBuffers 极致性能。
网络层 vs 数据层分工网络传输用 Protobuf;资源与缓存用 FlatBuffers。
工程可维护性统一 Codec 接口解耦协议与逻辑层。
跨语言通信使用标准 schema 统一消息定义。
未来演化gRPC + Protobuf → QUIC + FlatBuffers 是趋势。

一句话总结:

  • Protobuf:结构化通信协议的事实标准;
  • FlatBuffers:高性能游戏与嵌入式的终极选择。

两者的区别,不是“谁更快”,
而是“你需要快在哪里”。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页