提要
Protocol Buffers(后简称pb)是 google 提供的一种结构化数据格式。类似于 xml 或 JSON,有独立于语言和平台的特点,常常用来做通信协议。与 xml 或 JSON相比,pb 还有格式更紧凑的优势,并且序列化/反序列效率更高,更容易做协议版本兼容,所以在网络游戏中应用特别广泛,基本可认为是目前的业界标准。
本文主要介绍网络游戏开发中 pb 的应用思路和一些小技巧。
1. 消息通讯/RPC
消息通讯是 pb 的常规应用。主要流程就是定义好一致的消息格式后,发送方生成填充消息数据,序列化后通过网络发送,接收方从网络收到消息后反序列化出消息结构并进行处理。
一个常见的问题是 pb 打包的数据中是不带消息的类型信息的(区别于 xml 的 DOCTYPE),如果同一信道(例如同一 TCP 连接)可能会发送不同的消息,设计通信协议时要考虑对消息类型进行标识。
对于使用 TCP 的游戏,为了方便接收方进行 TCP 分节的重组,一般我们会定义固定格式的包头填入消息长度,只需要把消息包的类型加入包头即可。要注意服务器与客户端的类型保持一致,可以使用一些代码生成工具保证这一点。也可以采用字符串来标识消息类型,向下兼容性更好。消息处理模块可以使用注册的方式运行时绑定,这样就不会有一个大 switch 了。
使用 HTTP 的游戏可以使用 URI 来代替消息类型,不同的消息处理模块监听不同的 URI,实际上每一个 URI 上的消息包就是确定类型的了。当然在 HTTP Header 中添加消息类型也是个不错的主意。
另一种有些山寨的做法是定义一个大的 message,以 optional 的方式包含所有的消息包,实际传送的消息中只有一个字段是非空的,消息接收方使用 HasXxx()
依次进行判断后确定消息类型。
网游中的大部分通信是“请求-回应”模式的,即客户端发送请求,服务器处理完成后返回对应的回应消息,这个模式可以理解为一次远程过程调用(RPC)。pb 对这种模式有一定的支持,只需要把一对请求回应消息定义为 service,protoc 会生成对应的 rpc 接口。当然了生成的数据结构及一些 Stub 离能直接使用的框架还有一段距离,需要开发者自己实现网络消息发送及消息分发。有大量的第三方框架可以使用,比如百度的 sofa-pbrpc,最近(2015 年)google 也开源了自己的 rpc 框架 gRpc,值得关注。
2. 定义数据结构
pb 的 message 结构比较强大,最重要的是可以进行嵌套,有可选字段,还可以定义枚举,再加上其一次定义就能在各种语言及平台上使用的特性,我们可以用 pb 来定义一些服务器客户端都需要使用的数据结构,不仅可以省一定的工作量,还可以降低服务器客户端的沟通成本。
例如游戏中的一些常量,与其服务器和客户端各自分别定义,不如定义成 pb 的枚举然后两边分别生成代码。
例如游戏中的抽象物品,本质上是一个复杂的 union,而 pb 正好可以把字段定义成 optional,可以直接定义成 pb 的 message,既省空间又能方便地解析。
例如游戏中服务器发往客户端的系统通知消息,如果使用游戏国际化的一些建议中的方案,服务器发往客户端的是字典 key+参数变量的组合形式,其中参数中还可能包含字典 key,实际是我们需要的是一种递归的数据结构。pb 的 message 字段类型是其本身,正好符合这个需求。
3. 数据序列化存盘
游戏中每个玩家的数据比较独立,所以为了开发方便以及游戏数据加载的效率,现在很多游戏服务器都使用 Key-Value
数据库了(参见游戏数据存储)。
一般我们把玩家名字或 id 作为 key,把整个复杂的玩家数据序列化成二进制的 blob 作为 value,加载数据时将 blob 反序列化为内存中的数据结构。
直接使用 pb 做数据序列化至少有这几个优势。首先是方便,protoc 生成的代码接口丰富,而且很可靠,解决了一些语言(如 c++)缺乏对数据序列化直接支持的问题。第二是紧凑高效,pb 兼顾序列化/反序列化操作高效的同时对数据进行了一定的压缩。第三是便于做版本兼容,开发新功能是直接在 pb message 中添加新字段即可。
4. 配置文件
游戏策划一般使用 excel 配置表格数据,很多项目是服务器和客户端各自写一套解析 excel 表格的代码,打版本时将策划的配置分别拷贝至服务器和客户端。也有程序觉得 excel 表格解析起来很麻烦,使用第三方工具预先将 excel 文档转成 xml 后再读取。
这里提供一种利用 pb 读取配置文件的思路。
首先需要写一个小工具,这个工具同时做 2 件事情。1. 读取所有的 excel 文件,分析表头和类型生成对应的 proto 文件。2. 使用生成的 proto 文件生成代码,将 excel 表再次读入转成 pb message 序列化后写入新文件。
之后客户端分别使用 proto 文件生成代码,读入工具生成的配置文件后反序列化出结构化的配置。
这样做的好处是服务器和客户端不用重复实现解析 excel 的代码,新增或修改表格时重新生成即可,提升了效率。而且 pb 使用代码生成的方式,如果发生表格修改了代码却没有同步更新的情况可以在编译器就发现问题。另外使用工具将多份 excel 表格合成为单一的配置文件便于打版本和做自动更新等功能,还可以轻易做到直接从网络加载配置。
5. 记录日志
这里的日志指的是用于数据统计的玩家行为日志而不是程序运行时的诊断日志,通常是在玩家做某项操作时记录下玩家的操作类型及相关数据,包括花费的金币、花费的宝石、获得的经验、当前的等级等等。
最简单的做法是使用标识符分隔各个字段记为一行。但是这种方式很不利于后续的行为分析,比如想统计玩家宝石花费的去向就会比较麻烦,要先区分不同的操作类型再到对应的列读取宝石花费。
另一个比较常见的方式是使用 JSON 或类似 JSON 的格式,也就是把日志记录为一系列 Key-Value 对。涉及到宝石消耗的行为统一把消耗的宝石数记在 Key 为 GemCost
的字段中。但是这种方式实际操作起来会比较容易出错,比如有的地方记在 GemCost
下,其他的地方可能会误写成 CostGem
了,这样一来还是会给统计带来麻烦。
不妨使用 pb 定义一个 LogMessage,其中定义好可能涉及到的各种字段,如 GemCost
、CoinCost
、CoinGain
、ExpGain
等,还可以嵌套复杂结构,如 FriendInfo
。各个模块记录日志时根据需要填入自己涉及到的字段即可。各种语言的 pb binding 都提供了 String()
函数将 pb message 转为格式清晰的 string,转完后写入文件就得到了既清晰易读又便于统计的日志了,还可以将 pb message 序列化后发往远程日志收集程序,也非常方便。