DCE 是一个通用路由库,除了能路由 Http 等协议外,还能路由 Websocket 、Tcp 、Udp 等非标准可路由协议的协议,只需要实现定义的可路由接口即可。实现该接口需要序列化及反序列化数据包,为了实现一个高效灵活的二进制可路由协议,我设计了两个弹性数字编码。
一开始想着用第一个字节比特位标志表示是否有请求 ID 、请求路径等信息,第二个字节表示为扩展标志,来表示是否宽请求 ID 、长请求路径等。如在有或无扩展标志时,请求 ID 为 u32 或 u16 ,请求路径长度为 u16 或 u8 ,这样可以较为高效与灵活的编码请求头。对于请求的主体数据,它的长度不好确定,u8/u16/u32 都有可能,再用标志来表示的话需三个 bit ,且不是很灵活优雅,于是我设计了个 7 比特弹性长度数字编码来表示数据长度。
0 1 2 3 4 5 6
7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
|0|S|B|B|B|B|B|B|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-
|1|B|B|B|B|B|B|B|0|S|B|B|B|B|B|B|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-
|1|B|B|B|B|B|B|B|1|B|B|B|B|B|B|B|0|S|B|B|B|B|B|B|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-
|1|B|B|B|B|B|B|B|1|B|B|B|B|B|B|B|1|B|B|B|B|B|B|B|0|S|B|B|B|B|B|B|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-
|1|B|B|B|B|B|B|B|1|B|B|B|B|B|B|B|1|B|B|B|B|B|B|B|1|B|B|B|B|B|B|B|0|S|B|B|B|B|B|B|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-
|1|B|B|B|B|B|B|B|1|B|B|B|B|B|B|B|1|B|B|B|B|B|B|B|1|B|B|B|B|B|B|B|1|B|B|B|B|B|B|B|0|S|B|B|B|B|B|B|-|-|-|-|-|-|-|-
|1|B|B|B|B|B|B|B|1|B|B|B|B|B|B|B|1|B|B|B|B|B|B|B|1|B|B|B|B|B|B|B|1|B|B|B|B|B|B|B|1|B|B|B|B|B|B|B|0|S|B|B|B|B|B|B
|1|B|B|B|B|B|B|B|1|B|B|B|B|B|B|B|1|B|B|B|B|B|B|B|1|B|B|B|B|B|B|B|1|B|B|B|B|B|B|B|1|B|B|B|B|B|B|B|1|B|B|B|B|B|B|B
|1|B|B|B|B|B|B|B|1|B|B|B|B|B|B|B|1|B|B|B|B|B|B|B|1|B|B|B|B|B|B|B|1|B|B|B|B|B|B|B|1|B|B|B|B|B|B|B|1|B|B|B|B|B|B|B
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
7 8 9 10 11 12 13
7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|
|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|
|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|
|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|
|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|
|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|
|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|
|0|S|B|B|B|B|B|B|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|
|1|B|B|B|B|B|B|B|S|B|B|B|B|B|B|B|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
0:尾字节,1:非尾字节 S:符号位,B:二进制数字位
上面为 7 比特数字编码图。字节第 7 位为标志位,为1
时表示还有下个字节,否则为尾字节。若为第九个字节,则没有标志位,这样最大可以用最多 9 个字节表示7*8+8
64 位数字,用最小一个字节表示 7 位数字,较为灵活且节省空间。字节序采用小端序,因为最高位字节可能为 8 比特数字位,而其他的字节皆为 7 比特,大端序需分情况来偏移运算,比较麻烦,而小端序则统一偏移 7 位即可,比较方便高效。
当写完这个 7 比特数字编解码算法后,发现实际应用还是有些不方便。首先用标志来表示宽窄数字不是很灵活,比如不支持 u64 的请求 ID 。其次解码数字时不太方便,无法一次性知道数字的字节长度,需要最多 9 次循环读取一个数字全部字节。针对第一个问题,将全部数字换成弹性数字即可,这样只需一个比特标记是否有,而无需标记长短。针对第二个问题,则需重新设计一个前缀式数字编码。
前缀式编码,即以第一个字节作为数字头部,存储数字字节长度信息,读取第一个字节后,就能将剩下字节一次性取出来而无需循环读取。这个前缀又不能只存长度信息,不然白白浪费了一个字节,可以在其高比特位存长度信息,低位存数字本体,于是基于这些需求设计了下图编码。
0 1 2 3 4 5 6
7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
|0|S|B|B|B|B|B|B|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|- [int7]
|1|0|S|B|B|B|B|B|B|B|B|B|B|B|B|B|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|- [int14]
|1|1|0|S|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|- [int21]
|1|1|1|0|S|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|- [int28]
|1|1|1|1|0|S|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|-|-|-|-|-|-|-|-|-|- [int35]
|1|1|1|1|1|0|S|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|-|- [int42]
|1|1|1|1|1|1|0|S|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B [int64]
|1|1|1|1|1|1|1|0|S| 保留,后续可表示 128 位数字, 最长 1+8+8=17 字节. S: 符号位
|1|1|1|1|1|1|1|1| 为其他情况保留. B: 二进制数字位
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
7 8 9 10 11 12 13
5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|
|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|
|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|
|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|
|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|
|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|
|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|B|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
上图为前缀式弹性数字(后续将省略“前缀式”而直接简称为“弹性数字”)编码图。有一个固定的头部,其中第一个0
为长度与数字本体分隔符,高位的1
的个数表示躯体数字字节数,低位为符号与数字的高位部分。当头部高位以0
开头时,剩余部分为 7 位整数,无符号最大 127 ,没有躯体部分。由于头部需一个分隔符占位,所以最大只能表示 56 位数字,为了支持 64 位数字,当头部高位的1
的个数大于 5 时,直接视为 64 位数字,加头部共 9 个字节表示。由于 6 字节起的整数皆为万亿级别,很少用到,可以忽略其浪费的字节。头部剩余的可用标志位,为以后可能需要支持的 128 位数字作保留。
细心的朋友可能已经发现,这不是很像 UTF-8 吗?是的,这个方案是以 UTF-8 编码为灵感设计出来的,只是做了一些调整以支持 64 位数字的编码。UTF-8 需要表示的范围较小,且可能为了表现出较强的特征,躯体部分都以
10
开头,浪费了很多空间。而弹性数字编码,除了头部需占用比特位来表示字节长度外,躯体部分 8 比特全部表示数字,没有浪费。
实现弹性数字编码后,就可以较优雅的实现 DCE 二进制可路由协议的编码了。总体分为三个部分:一个标志头,用以表示有哪些属性;一个数字部分,用以标注属性值长度或作为数字值;最后为按序追加的属性值部分。
0 1 . . .
0 1 2 3 4 5 6 7 0 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
+-+-+-+-+-+-+-+-+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
|I|P|S|C|M|L|N|R| LEN of| LEN of| LEN of| LEN of| ID | CODE |NumPath| same |
|D|A|I|O|S|O|P|S| Path | Sid | Msg | Body |FlexNum|FlexNum|FlexNum| order |
|E|T|D|D|G|A|A|V|FlexNum|FlexNum|FlexNum|FlexNum| HEAD | HEAD | HEAD |FlexNum|
|N|H| |E| |D|T| | HEAD | HEAD | HEAD | HEAD | | | | BODY |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - - - - - - - - - - - - - - - - - - - - - - |
| | | | |
| Path | Sid | Msg | Body Data ... |
| | | | |
+-+-------------+-+-------------+-+-------------+-------------------------------+
上图为 DCE 弹性可路由协议包编码图。图中将弹性数字头与躯体分别堆在一起,是为了取到标志开启数量后,能一次性取出所有数字头部,继而一次性取出完整数字,提升编解码效率。在表示长度的数字中,0 是无意义的,标志未开启即为 0 长度,无需占用一个字节来表示。所以专门提供了长度编解码方法,将需编码与解码的长度减或加一,可以多表示一个有效长度,这在标注如十六进制 sha512 时非常有用,可以仅用 1 个字节( 7 比特头)来表示 128 的长度,节省了一个字节。
节省这一两个字节有用吗?通过 DCE 可路由协议,仅需 2 到 3 个字节,就能发起一个有效的类 GET 请求,而 HTTP 动辄需要成百上千字节,这在一些网络珍贵的场景是不可接受的,节省的每个字节都尤为珍贵。
要二进制省流量可以用 protobuf ,要灵活可以用 json ,为什么还要自己设计一个协议呢。这两者确实不错,DCE 也实现了基于它们的可路由协议。硬要说起来,protobuf 不够灵活,它的数字需固定长度,会浪费空间。除此之外有个最重要的原因,在 tcp 连接下,无法方便的从数据流中提取一个完整的 pb 或 json 包,需要借助分隔符或长度标注。分隔符无法同时做到高效与可靠,最靠谱的还是长度标注,而这个长度标注相当于需在外层再套一层协议,所以也需弹性数字来实现这层协议。
最后感谢你读到这里,上述的技术代码,可至DCE-GO查看。DCE-GO 初发布,欢迎体验。可能有 BUG 或可改进的地方,欢迎提出建议,欢迎协同开发。持续维护的 DCE 还有RUST 版本,该版本较旧,近期大概不会更新,但最终会同步,欢迎关注。
1
meeop 3 天前
mark ,不过一般人用不上
|
2
vvhy 3 天前
可以看下 MessagePack
|
3
Hookery 3 天前
您是否在寻找 Huffman 编码?
|
6
guyeu 3 天前
您是否在寻找 “Variable-Length Integers”?
|
7
guyeu 3 天前
另外,protobuf 的数字有很多种,默认的数字就是可变长度整数。
|