打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
云风的 BLOG: 设计一种简化的 protocol buffer 协议
我们一直使用 google protocol buffer 协议做客户端服务器通讯,为此,我还编写了 pbc 库
经过近三年的使用,我发现其实我们用不着那么复杂的协议,里面很多东西都可以简化。而另一方面,我们总需要再其上再封装一层 RPC 协议。当我们做这层 RPC 协议封装的时候,这个封装层的复杂度足以比全新设计一套更合乎我们使用的全部协议更复杂了。
由于我们几乎一直在 lua 下使用它,所以可以按需定制,但也不局限于 lua 使用。这两天,我便构思了下面的东西:
我们只需要提供 boolean integer (32bit signed int) string id (64bit unsigned int) 四种基础类型,和 array 以及 struct 两种用户定义的复合类型即可。
为什么没有 float ? 因为在我们这几年的项目中,使用 float 的地方少之又少,即使有,也完全可以用 string 传输。
为什么没有 enum ? 因为在业务层完全可以自己做 int 到 enum 的互转,没必要把复杂度放在通讯协议中。
为什么不需要 union ? 因为按 protocol buffer 的做法,结构中的每个域都可以用一个数字 tag 来标识,而不是用数据布局来指示。不需要传递的域不需要打包到传输包中。
为什么不需要 default value ? 我们的项目中,依赖 default value 的地方少之又少,反而从我维护 pbc 的大量反馈看,最容易被误用的用法就是通讯协议依赖一个字段有没有最终被打包。所以干脆让(不打包)等价于 default value 就好了。明确这个(在 google protocol buffer 中是错误的)用法。
我设计的这个新协议,命名为 ejoyproto ,它的协议描述成人可读的文本大约是这样的:
.person { .address { email 0 : string phone 1 : string } name 0 : string age 1 : integer marital 2 : boolean children 3 : *person # 这是一个 person 类型的数组 address 4 : address}所有涉及命名的地方,都遵循 C 语言规则:以英文字母数字和下划线构成,但不能以数字开头,大小写敏感。
自定义类型用 . 开头。自定义类型可以嵌套。自定义类型的名字不可以是 integer, string 和 boolean 。
每个类型由若干字段构成,每个字段有一个在当前类型中唯一名字和一个唯一 tag 。tag 是一个非负整数,范围是 [0,32767] 。不要求连续,但建议用比较小的数字。
每个字段必须有一个类型,如果希望它是一个数组,在类型前标注 * 。
协议定义次序没有要求,但建议引用一个类型时,类型的定义放在前面。
符号 # 以后是注释。
换行和 tab 没有特别要求,只要是空白符即可。
同时,协议文件里可以描述 RPC 协议,范例如下:
foobar 1 { request person response { ok 0 : boolean }}这里定义了一条叫做 foobar 的 RPC 协议,赋予它一个唯一的 tag 1 。(在网络传输的时候,可以用 1 代替 foobar )
每条协议都是由两个类型 request 和 response 构成,其中,response 是可选的。
这两个类型都必须是结构,而不能是基本类型或数组。这里可以在 request 或 response 后直接写上引用的类型名,或就地定义一个匿名类型。
这样,一组协议描述数据就可以用 ejoyproto 本身描述了:
.type { .field { name 0 : string type 1 : string id 2 : integer array 3 : boolean } name 0 : string fields 1 : *field}.protocol { name 0 : string id 1 : integer request 2 : string response 3 : string}.group { type 0 : *type protocol 1 : *protocol}最终提供的 api 会类似这样:
local tag, bytes = encode("foobar.request", { name = "alice", age = 13, marital = false }) 可用来打包一个 foobar 请求,返回 foobar 的 tag 以及打包的数据。然后再将它们组合起来构成最终的通讯包(可能还需要置入 session size 等信息)。
Wire Protocol
所有的数字编码都是以小端方式(little-endian) 编码。
打包的单位是一个结构体(用户定义的类型) 每个包分两个部分:1. 字段 2. 数据块
对于数据块,用于描述字段中的大数据。它是由一个 dword 长度 + 按 4 字节对齐的字节串构成。也就是说,每个数据块的长度都一定是 4 的倍数。 对齐时,用 0 填补多余的位置。
字段必须以 tag 的升序排列, 可以不连续; 数据块的次序必须和字段中的引用次序一致。
对于每个字段,由两个 word 构成。
第一个 word 是 tag 。记录的是当前字段的 tag 相较上一个的差值 -1 (对于第一个字段,和 -1 比较)。如果被打包的字段的 tag 是连续的,那么这个位置通常是 0;如果不连续,则记录的跳开的数字差。
第二个 word ,是这个字段的值。如果值为 0 ,表示数据放在数据区;否则这个值减 1 就是这个字段的值(只有是整数和布尔值才有效)。
注:在解码的时候,遇到不能识别的 tag ,解码器应选择跳过(不必确定字段的类型)。这对协议新旧版本兼容有好处。
数据类型在协议描述数据中提供,不在通讯中传输。根据 tag 可以查询到这个字段的类型。如果是对数据块的引用,且数据类型为:
integer : 数据块长度一定为 4 ,数据内容就是一个 32bit signed integer 。
id : 数据块长度一定为 8 ,数据内容就是 8 个字节的 id 。
string : 数据块的长度就是string 的长度, 内容就是字符串。
usertype : 那么数据块里就是整个结构。
array : 那么数据块就是顺序排列的数据。对于 integer array ,每 4 个字节是一个整数;对于 boolean array ,每个字节可表示 8 个布尔量,从低位向高位排列;对于 string 和 struct ,都是顺序嵌入数据块(长度+内容)。
下面有两个范例:
person { name = "Alice" , age = 13, marital = false } :03 00 01 00 (fn = 3, dn = 1)00 00 00 00 (id = 0, ref = 0)00 00 0E 00 (id = 1, value = 13)00 00 01 00 (id = 2, value = false)05 00 00 00 (sizeof "Alice")41 6C 69 63 65 00 00 00 ("Alice" align by 4)person { name = "Bob", age = 40, marital = true, children = { { name = "Alice" , age = 13, marital = false }, }}04 00 02 00 (fn = 4, dn = 2)00 00 00 00 (id = 0, ref = 0)00 00 29 00 (id = 1, value = 40)00 00 02 00 (id = 2, value = true)00 00 00 00 (id = 3, ref = 1)03 00 00 00 (sizeof "Bob")42 6F 62 00 ("Bob" align by 4)03 00 01 00 (fn = 3, dn = 1)00 00 00 00 (id = 0, ref = 0)00 00 0E 00 (id = 1, value = 13)00 00 01 00 (id = 2, value = false)05 00 00 00 (sizeof "Alice")41 6C 69 63 65 00 00 00 ("Alice" align by 4)0 压缩
这样打包出来的数据的长度必定 4 的倍数,里面会有大量的 0 。我们可以借鉴 Cap'n proto 的压缩方案 :
首先,如果数据长度不是 8 的倍数,就补 4 个 0 。
按 8 个字节一组做压缩,用一个字节 (8bit) 表示各个字节是否为 0 ,然后把非 0 的字节紧凑排列,例如:
unpacked (hex): 08 00 00 00 03 00 02 00 19 00 00 00 aa 01 00 00packed (hex): 51 08 03 02 31 19 aa 01当 8 个字节全不为 0 时,这个标识字节为 FF ,这时后面跟一个字节表示有几组 (1~256) 连续非 0 组。 所以,在最坏情况下,如果有 2K 的全不为 0 的数据块,需要增加两个字节 FF FF 的数据头(表示后续有 256 组 8 字节的非 0 块)。
云风 提交于 July 24, 2014 08:50 PM | 固定链接
COMMENTS
这主要强调 Schema , map 其实是 schemaless 的.
如果支持 map, 不如直接用 json.
Posted by: Cloud | (17) August 4, 2014 11:45 AM
为什么不支持map类型,直接和lua表对应不是更方便?
Posted by: bbp | (16) August 4, 2014 06:24 AM
为什么选择小端序呢?ProtoBuf也选择用小端序,但是约定的网络字节序不是大端序么?是因为只使用x86架构的CPU么?
Posted by: teeceepee | (15) August 1, 2014 10:52 AM
# 这是一个 preson 类型的数组
错字
Posted by: 比尔盖子 | (14) July 30, 2014 09:45 PM
写MMO的思路,总也绕不过性能的门槛,总是追求单服在线人数,这样对程序是很有挑战,但是对于整个项目却未必有什么挑战。
Posted by: www | (13) July 26, 2014 07:10 PM
我们也有类似协设计,参考了json,可以看成是binary的json,有object类型,这样成员可以带上name,写日志会非常方便(直接转成类似json的文本后再记录)
Posted by: zcpro | (12) July 25, 2014 02:49 PM
TLV也可以,protobuffer 太大了。
Posted by: sunhuino1 | (11) July 25, 2014 02:42 PM
TLV也可以,protobuffer 太大了。
Posted by: sunhuino1 | (10) July 25, 2014 02:42 PM
@dwing
以我写过两个不同版本的 protobuffer 编解码模块的经验, protobuffer 的结构(尤其是 varint 的引入) 非常不易于实现.
它需要引入大量临时内存,编码复杂,性能很低.
Posted by: Cloud | (9) July 25, 2014 11:19 AM
如果有路由信息,比如该消息需要从ID1通过服务器转发到ID2,是不是也基于这套机制去定义在request中?
Posted by: lihao102 | (8) July 25, 2014 11:16 AM
前面的设计还不错, 后面的编码设计反而比pb更复杂了, 而且也没看出压缩后的效果比pb更省空间,除了有重复数据的情况.
Posted by: dwing | (7) July 25, 2014 09:48 AM
不是有个新出的FlatBuffers
Posted by: Anonymous | (6) July 25, 2014 09:34 AM
我一直再用pbc和pbc Lua绑定....
云风终于设计自己的协议代替掉google pb了...
rpc协议为什么不像google pb一样设计成一组的,一个service下有多个rpc方法?
repeated类型使用 *integer而不是 [integer]是出于什么考虑?解析方便吗?
Posted by: DavidFeng | (5) July 25, 2014 09:33 AM
我们内部讨论的时候谈论过名字问题,可以考虑叫 sproto .
Posted by: cloud | (4) July 24, 2014 10:46 PM
那么 google protocol buffer 这个名字有商业味道么?
Posted by: Cloud | (3) July 24, 2014 10:42 PM
cool, 如果能用一个更中立的名字就更好了, ejoyproto 感觉有些商业相关的味道.
Posted by: c4pt0r | (2) July 24, 2014 10:29 PM
设计的很好。。。游戏中就该使用这么简洁的协议。我之前想过N久都没想好怎么搞。
Posted by: hanxi | (1) July 24, 2014 09:58 PM
本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
Protocol Buffer使用简介
快来看看Google出品的Protocol Buffer,别只会用Json和XML了
Protobuf基础知识
Protocol Buffer 序列化原理大揭秘
Protocol Buffer技术详解(数据编码)
DBMS_LOB包使用和维护
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服