Protocol Buffers 在游戏中的应用

提要

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,其中定义好可能涉及到的各种字段,如GemCostCoinCostCoinGainExpGain等,还可以嵌套复杂结构,如FriendInfo。各个模块记录日志时根据需要填入自己涉及到的字段即可。各种语言的pb binding都提供了String()函数将pb message转为格式清晰的string,转完后写入文件就得到了既清晰易读又便于统计的日志了,还可以将pb message序列化后发往远程日志收集程序,也非常方便。