V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
idrunk
V2EX  ›  设计

设计了两个弹性长度数字编码,可以灵活地编解码数字以便高效传输

  •  
  •   idrunk · 3 天前 · 839 次点击

    DCE 是一个通用路由库,除了能路由 Http 等协议外,还能路由 Websocket 、Tcp 、Udp 等非标准可路由协议的协议,只需要实现定义的可路由接口即可。实现该接口需要序列化及反序列化数据包,为了实现一个高效灵活的二进制可路由协议,我设计了两个弹性数字编码。

    7 比特弹性长度数字编码

    一开始想着用第一个字节比特位标志表示是否有请求 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+864 位数字,用最小一个字节表示 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 条附言  ·  2 天前
    如 7 楼 @guyeu 指正,protobuf 确实默认是变长数字。不过它要存 tag 等,还是比较浪费空间。
    9 条回复    2025-01-15 17:07:19 +08:00
    meeop
        1
    meeop  
       3 天前
    mark ,不过一般人用不上
    vvhy
        2
    vvhy  
       3 天前
    可以看下 MessagePack
    Hookery
        3
    Hookery  
       3 天前
    您是否在寻找 Huffman 编码?
    idrunk
        4
    idrunk  
    OP
       3 天前
    @meeop 文章末尾提到的 DCE 可能能用到的,可以了解下😄
    idrunk
        5
    idrunk  
    OP
       3 天前
    @vvhy
    @Hookery
    了解了下,都不是我寻找的
    guyeu
        6
    guyeu  
       3 天前
    您是否在寻找 “Variable-Length Integers”?
    guyeu
        7
    guyeu  
       3 天前
    另外,protobuf 的数字有很多种,默认的数字就是可变长度整数。
    idrunk
        8
    idrunk  
    OP
       2 天前
    @guyeu 👍,确实是的。
    不过那不是我的重点,主要是它的解码入参只能传字节序列,不支持输入流,在 TCP 下必须先知道长度取出字节序列传给它来解码。
    另外 protobuf 可能为了通用性存了 tag ,浪费了空间,我这个弹性协议包只存 value 的,比较省。
    Hookery
        9
    Hookery  
       2 天前
    @idrunk 参考 Huffman 编码的抽象逻辑,不是具体的实现逻辑。实现的话有很多种方式,主要看应用场景了。一般大的 BAT 厂在遇到性能不足的时候就可能直接魔改了。为什么使用比如何实现更重要。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2619 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 22ms · UTC 05:00 · PVG 13:00 · LAX 21:00 · JFK 00:00
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.