设计了两个弹性长度数字编码,可以灵活地编解码数字以便高效传输 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
idrunk
V2EX    设计

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

  •  
  •   idrunk 335 天前 2611 次点击
    这是一个创建于 335 天前的主题,其中的信息可能已经有所发展或是发生改变。

    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 条附言    334 天前
    如 7 楼 @guyeu 指正,protobuf 确实默认是变长数字。不过它要存 tag 等,还是比较浪费空间。
    9 条回复    2025-01-15 17:07:19 +08:00
    meeop
        1
    meeop  
       335 天前
    mark ,不过一般人用不上
    vvhy
        2
    vvhy  
       335 天前
    可以看下 MessagePack
    Hookery
        3
    Hookery  
       335 天前
    您是否在寻找 Huffman 编码?
    idrunk
        4
    idrunk  
    OP
       334 天前
    @meeop 文章末尾提到的 DCE 可能能用到的,可以了解下
    idrunk
        5
    idrunk  
    OP
       334 天前
    @vvhy
    @Hookery
    了解了下,都不是我寻找的
    guyeu
        6
    guyeu  
       334 天前
    您是否在寻找 “Variable-Length Integers”?
    guyeu
        7
    guyeu  
       334 天前
    另外,protobuf 的数字有很多种,默认的数字就是可变长度整数。
    idrunk
        8
    idrunk  
    OP
       334 天前
    @guyeu ,确实是的。
    不过那不是我的重点,主要是它的解码入参只能传字节序列,不支持输入流,在 TCP 下必须先知道长度取出字节序列传给它来解码。
    另外 protobuf 可能为了通用性存了 tag ,浪费了空间,我这个弹性协议包只存 value 的,比较省。
    Hookery
        9
    Hookery  
       334 天前
    @idrunk 参考 Huffman 编码的抽象逻辑,不是具体的实现逻辑。实现的话有很多种方式,主要看应用场景了。一般大的 BAT 厂在遇到性能不足的时候就可能直接魔改了。为什么使用比如何实现更重要。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2428 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 39ms UTC 15:33 PVG 23:33 LAX 07:33 JFK 10:33
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86