应用协议V1.0设计方案
在《卧龙转》的开发中,我设计了一种最简洁、最高效的网络协议结构,我就称它为v1.0:
一个数据包 = 消息尺寸(int32) + 消息类型码(short) + 消息正文
消息正文 = 数据项1 + 数据项2 +。。。
客户端用一个md文档进行约束,格式如下:
1050 客户端请求同步时间
数据:
+ 发送协议时客户端的时间戳(t1)
2050 服务端返回时间
数据:
+ 发送协议时客户端的时间戳(t1)
+ 服务器端就收到协议时服务器端的时间戳(t2)
+ 回发该协议时服务器端的时间戳(t3)
+ 心跳频率(秒数)
在程序里这样写:
t2 = int(time.time())
wr=RW.RWStream(body)
t1 = wr.readUInt()
wr.init()
wr.writeUInt([t1,t2])
wr.writeUInt(timer.getNetTimestamp(int(time.time())))
wr.writeUInt(com._cc.CLEAR_INTERVAL_TIME)
body=wr.getBytes()
connection.sendmsg(2010,body)
*备注:
- 其中数据项都采用了varint 编码,极大的节省的传输数据流。
- 由于应用简单得“简陋”,也使得它的数据编码效率超高,几乎没有多余字节
- 对较长的字符串数据,我们进行gzip压缩。
- 以上几点是《卧龙传》在2011、2012年的国内移动网络较低带宽水平下流畅运行的重要前提。
- 不需要依赖外部的协议书写工具(如google protocol buffer 需要依赖编码工具和数据结构定义文档),很适合小型开发团队高效设计、高效开发(只需维护一份如上的协议定义文本文档)。
V1.0的问题
显而易见,V1.0版效率虽然高,但是很简陋,一些先天不足在前端、后端工程师编写协议解析代码时显现出来:
- 必须严格遵循协议定义顺序读写数据项
- 必须注意数据项的数据类型,如果将unsigned varint 与unsigned integer 32 混淆,导致数据项读写错误,且只能在运行过程中才能单步跟踪才能定位,而编码错误是不可避免
- 几乎不能支持客户端、与服务器端协议版本不一致的情况,而实际运营过程中,常常需要允许多个版本的客户端同时存在,如微信新老版本都可以正常运行。
- 协议文档不能多种形式查看,它是一个markdown格式纯文本文件,呵呵。
思考
数据总是按一定顺序编码的,所以在数据编解码层级是必须顺序读写数据项,但是我们可以开发辅助工具:
- 自动读写(如golang语言的gob包)
- 或者自动生成协议读写代码,如google protocol buffer等
数据项类型读写错误问题:
- 给每个数据项编码规则上加上一个类型码,是数据有一定的自描述性
- 读写函数可以判断类型是否正确,如readUint()读出的是string时就可以抛出异常进行处理,方便类型读写错误定位。
- 通过识别、写入类型码,可以设计编写通用的读写函数(关于通用读函数可能只能在动态语言里可以实现了)。
协议的多版本兼容:
协议文档的编写格式:
- 为了方便不同文件版本的比较(我们会用git管理设计文档),所以要求必须要用文本文件定义
- 可以用xml定义
- 可以自定义特定的格式来书写协议
现有方案的分析
google protocol buffer(protobuf) 是现在应用的非常广泛的一种解决方案,基本上可以解决以上所有问题,但是我们开发团队成员讨论分析有如下顾虑:
- 修改一个协议,调用不同语言版本的工具重新生成读写文件,而开发初期阶段可能更改很频繁,比较麻烦;而一般修改协议,意味着逻辑也会修改,自动生成数据读写代码并没有省却多少代码量而仅仅只是起到了规避数据读写代码编码错误,这是可以通过其他方式解决的。
- protobuf 数据定义功能很强大,但是我们游戏数据似乎只需要两种
- protobuf 有默认值的功能,这是个很好的设计,在某种情况下可以减少数据传输,但是我们的游戏数据这种情况很少
- 很多协议之间只是简单传递几个数据,如果都要定义数据文件(.proto)文件再生成语言代码,似乎有点“大财小用”;而如果这时不用protobuf,又会导致方法不统一的不良后果
- protobuf 给每个数据项前增加一个字节的元数据,而列表数据明显是重复冗余了
- 我细看了一些语言的代码,由于要实现protobuf的强大的特性,一些语言生成的代码非常复杂,代码量巨大,效率低下
终上所述,protobuf 用于我们的游戏数据传输,无效数据率较大,且我们团队认为用起来较繁复。
golang 的gob包的设计可以解决以上问题,它也是google的大牛们设计的,规避了protobuf一些问题:
- 数据默认值(0,'')不传送,但不支持自定义默认值
- 不考虑必选项/可选项,所有都采用属性名:属性值的方式传输,一种方法解决了这个问题
- 不考虑多语言通用(原设计只考虑golang之间通讯),这样设计可以很轻巧。
- 充分考虑了golang得特性(如高效的反射、struct)等,gob代码很简短
- 自描述,且只在两端第一次传输时传送元数据(这个我还没有搞懂具体实现原理)
- 所有整数型数据它都已varint 方式编码。
终上所述,我们不能用它了(我们是多语言体系),但我们可以吸收它的一些优点。
V2.0数据编码基本思路
- 包含自描述元数据:序号、类型
- 将整数型数据归结为两种:varint和unsigned varint。
- 0,'' 不传输
- 版本号成为协议必要数据项
明天再写我们最终的方案。
|