4 个版本
0.5.3-dev | 2021 年 3 月 6 日 |
---|---|
0.5.2 | 2021 年 3 月 6 日 |
0.5.1 | 2021 年 3 月 2 日 |
0.5.0 | 2021 年 2 月 19 日 |
734 在 编码
230 每月下载量
在 samotop-delivery 中使用
97KB
2K SLoC
Lozizol 0.5
可扩展且高效的序列化协议。
针对网络和存储,复用流,连接,事件溯源...
事件溯源?历史是不可变的,只有在应用程序版本之间的解释可能发生变化。缺点是,应用程序需要知道如何解释所有先前版本的记录。优点是在应用程序升级时无需迁移或数据更新。
定义
sequence
- 具有属性的条目 Lozizol 序列- id (
uuid
) - 序列的 UUID。 - version (semver2) - "major.minor.patch" 格式的 Lozizol 协议版本。
- id (
entry
- 具有以下属性的序列项- sequence - 对包含的 Lozizol 序列的引用
- type - 确定条目意义的条目类型
- data - 条目携带的二进制数据
type
- 由 URI 识别的序列化条目数据的含义
这些条目类型是 Lozizol 默认隐含的
- Lozizol 头部 (URI
urn:lozizol:header
) 是一个放置在序列开头的条目,用于传达 Lozizol 版本、标识符和诊断信息。它隐含删除所有显式的条目类型分配。隐含的 类型分配是 111 (0x6F
) - 类型分配 (URI
urn:lozizol:type
) 是类型数字的保留,并分配给识别条目类型和可能提供详细信息的 URI。空 URI 表示已从序列中删除类型分配。隐含的 类型分配是 1 - 已删除的条目 (URI
urn:lozizol:deleted
) 是应考虑从序列中删除的条目。固定的 类型分配是 0
其他条目类型由应用程序定义,使用URI进行指定,并在Lozizol序列中使用类型分配条目进行分配。即使在Lozizol序列的中间,类型也可以被分配、重新分配或删除。在Lozizol序列中使用之前,必须分配或暗示一个类型。
高级实现必须使用类型URI,而不是类型ID。因此,可以在多个类型ID下分配相同的类型,并且它们都将被同等对待。
二进制定义
byte
:一个8位序列vuint
:可变长度编码的大小(非负整数)。- 它以大端字节顺序存储 - 最高的字节首先。
- 最高位设置为1(1)的字节表示后续字节。
- 最高位未设置(0)的字节结束二进制表示。
- 要从二进制表示中提取整数值,将每个字节的7个低位拼接在一起。例如,将两个字节的整数值
abcd efgh ijkl mnop
在二进制vuint中表示为1000 00ab 1cde fghi 0jkl mnop
。 - 此方案对于启用条目安全擦除和填充处理至关重要。
- 空的前导字节(
0x80
)是无效的。实现必须在遇到这种情况时将序列视为损坏。 - 另请参阅VLQ。
uri
:表示RFC 3986中有效的URI字符串的字节序列record
:条目的二进制表示size
(vuint
):序列化类型和数据大小(实际上包括类型vuint长度)type
(vuint
):条目类型data
:根据类型对条目的二进制表示
padding
:Lozizol序列中的所有字节都是记录或填充。由于定义上记录至少需要两个字节(大小和类型),因此记录外的空字节(0x00
)将成为一个零长度的记录,没有类型,因此被视为单个字节的填充。填充可用于对齐记录或可能是删除记录的结果。
选择Vuint的二进制表示
- 有许多表示整数值的方案。变量长度被选择以节省空间,并具有灵活性和对未来uint大小的保证(8、16、32、64、128、...)。
- 选择了一个人容易推理的方案。单独的位很容易关联。尝试
lozizol serialize vuint 16777215 | xxd -b
并将其与echo -n -e '\xFF\xFF\xFF' | xxd -b
进行比较。虽然这引入了一些冗余(vuint0x8011
与0x11
相同),但它是为了人类和更容易的实现。其他方案在编码/解码过程中减去/加上1
,但这意味着0和1无法通过视觉检查,并增加了计算开销。 - 还选择了大端,考虑到人类理解和视觉比较。
- 一种类似于UTF-8的方案,将所有标记
1
放在第一个字节中,计算效率更高,但会破坏擦除/填充的语义。如果删除了第一个字节而没有删除其他字节,将无法确定字节的意义。分散标记位是至关重要的。
Lozizol头部(111)
- “lozizol”标记
- 'l' 或
0x6C
也是一个长度为108的vuint,因此完整头部长度为109(1 + 108)字节 - 'o' 或
0x6F
是预定义的条目类型111 - "zizol"只是"lozizol"的一部分
- " " 是一个空格(0x20)
- 'l' 或
- "0.5":当前Lozizol semver2版本
- " " 又是一个空格 :)
- 序列标识符(UUID按照RFC 4122以字符串形式表示 - 36字节)
- " " 再加一个空格 =D,总共给我们三个宇宙来开始
- 剩余60字节(109 - 1 - 1 - 5 - 1 - 3 - 1 - 36 - 1 = 60)的未定义数据,可以由Lozizol编写者根据需要插入一些诊断信息,例如软件和版本。读者应忽略这些信息,或者最好用于诊断,不得用于任何应用程序数据或逻辑。这些应由其他条目携带。
由于Lozizol头部通常是一个文件中存储的第一个记录,因此它允许基于魔数字节进行文件类型检测。它是故意可读的,以便未察觉的探索者可以了解存储的和/或传输的数据的性质。
如果没有Lozizol头部,实现必须
- 假设文件不是Lozizol序列 - 认为它是损坏的,或者
- 确保它确切地知道它属于哪个序列(和状态)
- 序列(可能是嵌套的)
- 序列的起始位置
- 类型定义和上下文
尝试
try_lozizol () {
data="zizol 0.5 039343bb-9019-4d6e-825e-631aa578ff7f lozizol-cli 0.0.1-preview and what not ....................."
lozizol serialize entry 111 "$data" | xxd
# lozizol magic:
lozizol serialize entry 111 "$data" | dd bs=1 count=109 status=none | cut -d ' ' -f1
# lozizol version:
lozizol serialize entry 111 "$data" | dd bs=1 count=109 status=none | cut -d ' ' -f2
# sequence ID:
lozizol serialize entry 111 "$data" | dd bs=1 count=109 status=none | cut -d ' ' -f3
# diagnostic info:
lozizol serialize entry 111 "$data" | dd bs=1 count=109 status=none | cut -d ' ' -f4-
}
try_lozizol
Lozizol序列通过头部初始化。头部设置序列 id
和协议 version
。它将给定序列中的所有类型分配重置为默认值(取消所有分配并只分配由lozizol隐含的类型)。
注意:一旦序列初始化,Lozizol头部类型id 111可以重新分配。
类型分配(1)
size
(vuint
):类型长度 +typeid
+typeuri
entry_type
(vuint
):类型分配条目类型(Lozizol默认为0x01
,除非重新分配)assigned_type
(vuint
):新的类型分配非零数字uri
(uri
):类型的URI字符串
尝试
lozizol serialize vuint 63 | xxd # find that 63 serializes as 0x3F ('?')
lozizol serialize entry 1 "?urn:my-awesome-type" | xxd
或特定的快捷方式
lozizol serialize type 1 63 urn:my-awesome-type | xxd
注意:类型分配类型id 1可以重新分配。
已删除条目(0)
- 大小(
vuint
):1 + 删除数据的长度 - null(
0x00
):已删除条目类型 - 删除/未定义数据
警告:删除类型id 0绝不能重新分配,因为它会破坏填充语义。
操作
该协议预计最具常见场景是只读的读者紧随只写者。这适用于文件操作和网络流。通过在条目前添加长度支持快速向前搜索。
通过应用采取一些谨慎措施,也可以适应复用或随机写入访问。延迟清理和索引写入可以在不影响完整性和读取的情况下进行。可以在Lozizol之上构建索引,以实现随机读取访问。
阻塞
- 只要是一个负责附加的单线程、单任务追加器,追加写就不需要阻塞。并发追加器必须在记录级别上进行同步。
- 除了等待更多数据之外,追加写不需要通过读取来阻塞。
- 随机访问写入
- 在预期的随机访问写入修改之后定位的读取/写入不需要阻塞。
- 在预期的随机访问写入修改之前定位的读取/写入必须阻塞,或者应用程序需要在记录级别上进行阻塞。
仅向前读取和写入
用例:所有用于序列化条目的数据都可用。换句话说,它不会因为等待序列化数据(I/O或密集序列化)而过度阻塞。
写入
示例
- 记录TCP流,写入者只需按原样持久化立即可用的缓冲区,而无需为序列化添加很多开销。
- 记录用户操作,所有数据都可在内存中找到,序列化是微不足道的。
写入者
- 如果尚未写入此Lozizol序列,则写入条目类型记录,
- 写入条目记录 - 大小、类型、数据,
当所有数据根据大小写入时,记录被认为是已提交的。
读取
读取者简单地贪婪地读取所有可用数据。当它到达记录的末尾时,它生成条目。
读取者
- 读取vuint,直到第一个位被设置,学习记录的大小,
- 读取条目类型,直到第一个位被设置,学习和使用此信息,
- 将剩余数据(记录大小 - 类型vuint长度)读取到缓冲区中,
- 生成条目
并行方法是读取者通过记录长度跳过记录。实际的记录读取是通过异步记录读取器委托给它们的字节范围来完成的。
删除
用例:应用程序需要将已写入的记录标记为已删除。
应用程序应有自己的语义来标记数据结构冗余,例如添加一个表示实体已删除的条目,以事件源的精神。然而,为了存档/清理的目的,应用程序应能够标记记录为已删除。另请参阅擦除和安全擦除。
已删除的条目类型(0
)旨在应用于已写入的记录以标记它们为已删除。只要单字节写入操作可以被认为是原子的,删除就可以与读取和写入结合,并且对读取者是安全的,因为读取者要么在类型ID之前,将其视为已删除,要么在类型ID之后,将其视为原始记录。
数据没有改变,但是由于条目类型被重置,其含义不再可以可靠地解释。读取者应跳过已删除的记录,除非它们从事回收未使用空间以进行压缩或执行数据擦除的业务。
删除是单字节写入操作。要将记录标记为已删除,将null(0x00
)字节放在记录长度后的位置。如果此操作不能以原子的方式执行,则必须阻塞给定记录的读取者和随机访问写入者,以确保一致性。
擦除
Lozizol序列可以像任何其他流一样整体压缩。一个合适的优化是在擦除所有已删除记录之前用null(0x00
)字节擦除。
这可以在存储的Lozizol序列上作为后台作业执行,或者通过擦除过程完成。
后台擦除过程
- 断言在适用的情况下已停止随机访问的读写(只写可以继续),
- 定位下一个删除的记录,
- 在类型位置写入null (
0x00
) 字节, - 在数据位置写入null (
0x00
) 字节, - 在大小位置写入null (
0x00
) 字节。
这将有效地创建填充。在任何时刻失败都应该是安全的。
后台擦除过程可以选择通过创建覆盖连续填充块的删除条目记录来优化后续的读取。
通过(流式)擦除过程
- 简单地用null (
0x00
) 字节替换所有已删除的记录。
安全擦除
敏感应用程序可能需要安全擦除存储的和已删除的记录。这可以通过类似于后台擦除的过程完成,但在此之前,安全擦除过程将运行足够的措施以消除存储介质中的残留数据痕迹。
随机访问写入
在考虑随机访问异步写入之前,先考虑你的模型。这些写入能否表示为只向前写入的小条目?例如,而不是将整个来自线的电子邮件写下来,这需要异步写入优化,可以将每个头块和每个正文块作为带有关共邮件ID的记录写入,并以终止记录(即事件溯源)结束。
或者,可以通过将Lozizol序列分割成多个流来实现简单的并行化。
尽管如此,仍然可能存在需要异步随机访问写入的情况。例如,在UDP传输或密集序列化时。
用例:我真的想异步写入
每个Lozizol序列应该有一个权威机构来分配要写入的记录。它将只写入记录长度和类型,并将字节数据范围交给异步写入器。立即之后,它可以跳到下一个可用的位置,并分配下一个记录写入请求。
在这种情况下,由于无法通过数据末尾来断言记录的完整性,因此必须在应用程序中构建提交机制 - 要么是作为记录一部分的散列,以使脏读无效,要么是单独的提交记录。写入提交记录必须阻塞权威机构。一个简单的提交机制可以是使用一种类型表示脏记录,另一种类型表示已完全写入的记录。写入、刷新、同步后,写入器将更新记录类型。这很复杂,因为vuint长度的类型必须不变。必须选择具有相同vuint长度的ID。
目前,定义合适的条目类型和处理具有完整性保证的异步写入的规则留给应用程序定义。
依赖项
~0.5–11MB
~122K SLoC