一种比特级别的数据交换格式

https://img.shields.io/badge/license-BSD3-brightgreen https://github.com/hit9/bitproto/workflows/bitproto%20ci/badge.svg https://readthedocs.org/projects/bitproto/badge/?version=latest https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg

简介

bitproto 是一种快速的、轻量的、易用的用来序列化结构化数据的比特级别数据交换格式。

bitproto 的协议描述语法和著名的 protocol buffers 类似,只不过是比特级别的:

message Data {
    uint3 the = 1
    uint3 bit = 2
    uint5 level = 3
    uint4 data = 4
    uint11 interchange = 6
    uint6 format = 7
}  // 32 bits => 4B

上面的 Data 是一个消息,它由 7 个字段构成,在编码后会占用 4 个字节。

这一张图片展示了编码后的字节流中数据字段的分布情况:

_images/data-encoding-sample.png

功能

  • 支持比特级别的数据序列化

  • 支持协议的 扩展性、向后兼容设计

  • 非常容易 上手:
    • 协议的语法 类似著名的 protobuf.

    • 生成的代码具有非常简单的 API

  • 支持以下编程语言
    • C (ANSI C) - 没有使用动态内存分配

    • Go - 没有使用反射或者类型断言

    • Python - No magic :)

  • 极快的编解码性能 (性能压测).

代码示例

一个在 C 语言中编码 bitproto 消息的代码示例:

struct Data data = {};
unsigned char s[BYTES_LENGTH_DATA] = {0};
EncodeData(&data, s);

下面的是一个解码的例子:

struct Data data = {};
DecodeData(&data, s);

非常简单,不是吗?

对于 Go 和 Python 语言,bitproto 的编解码的代码也是类似的。你可以前往 快速开始的文档 获取进一步的引导内容。

为什么会有 bitproto ?

已经有 protobuf 了,为什么要做 bitproto 呢?

由来

bitproto 最初是我在和微型控制器上的嵌入式程序打交道的时候创作的。在嵌入式的环境中,经常会有许多开发上的约束:

  • 紧凑的通信量

  • 受限制的固件大小

  • 最好不要有动态内存的使用

Protobuf 天生不是为了嵌入式领域的,它没有开箱即用的纯 C 语言的支持。

场景

对于以下的场景,是推荐使用 bitproto ,而非 protobuf 的:

  • 当你的程序需要在微型控制器上运行,或者 你的程序需要和嵌入式程序通信。

  • 想要有比特级别的数据字段

  • 想要从协议设计上清楚地知道编码后的数据会占用多少字节

对于其他场景,则推荐考虑 protobuf ,而不是 bitproto .

和 Protobuf 相比

bitproto 和 protobuf 的不同点有:

  • bitproto 支持比特级别的数据序列化,类似 C 语言中的 bit fields.

  • bitproto 不使用任何动态内存。很少有 protobuf 的 C 语言实现 支持这点,除了 nanopb.

  • bitproto 不支持变长数据,所有类型都是定长的.

    bitproto 不会把任何类型信息或者反射信息编码到字节流中。它只编码数据本身,编码后的数据排列就和内存中一样,定长且不带有任何字节缝隙,就像在C 语言中设置 结构体的对齐为 1 字节 一样。

  • Protobuf 对于 向后兼容 的特性支持的很好。对于 bitproto,直到 v0.4.0 这都是其一个主要的缺点。自从这个版本之后,bitproto 通过在编码后的字节流头部新增两个字节的方式实现了 扩展性 的支持,这两个字节存储了相关消息的占用字节数的多少。这个设计打破了 bitproto 传统的编码结构的设计,因为新增了一些对于编码大小的反射信息,因此这个功能被设计为可选的。

缺点

已知的 bitproto 的缺点如下:

  • bitproto 不支持变长数据类型。 举例来说,一个 uint37 类型的数据永远在编码后占用 37 个比特,即使你赋值它一个诸如 1 这样很小的值。

    这意味着,如果有意义的数据占用类型的占比较小的时候,编码后的数据中会有很多零字节。比如,当一个大小为 n 个字节的类型只被使用了其中一个字节的时候,会有 n-1 个字节会是零字节。

    一般来说,我们并不特别关心这个问题,因为在嵌入式设备上的通信量一般不会有多少字节。这里的协议自身一般要设计地紧凑一些。如果你真的比较关心这个问题,可以考虑使用 zlib 类似的压缩机制,在消息编码后进行一次协议压缩。

  • bitproto 无法同时提供 最佳的性能扩展性能力

    bitproto 中设计了一种 优化模式 ,这个模式下,编译器会直接生成直白的编解码的语句,以达到更好的编解码性能。因为 bitproto 中所有的类型都是定长的,因此我们可以在代码生成阶段就清楚地知道如何对其进行编解码。压测的结果表明,这个模式带来了巨大的性能提升,不过目前为止我还没有想到一个办法来支持 bitproto 的扩展性功能和这个性能优化模式一起工作。