跳到主要内容

3. 创建基于 CBOR 的协议

CBOR 这类数据格式常用于没有格式协商的环境中. CBOR 的一个特定设计目标是无需任何内嵌或假定的模式: 解码器可以拿到一个 CBOR 项并在没有其他任何知识的情况下解码它.

当然, 在现实世界的实现中, 编码器和解码器会对 CBOR 数据项中应包含什么持有共同的看法. 例如, 一个商定的格式可能是 "该项是一个数组, 其第一个值是 UTF-8 字符串, 第二个值是整数, 后续值则是零个或多个浮点数", 或者 "该项是一个以字节串为键的映射, 且至少包含一个键为 0xab01 的对".

本规范对基于 CBOR 的协议不施加任何限制. 编码器能够编码的值的类型可多可少, 取决于使用它的协议所需; 解码器能够理解的值的类型也可多可少, 取决于使用它的那些协议所需. 这种不加限制的做法使 CBOR 能够用于极度受限的环境.

本节讨论创建基于 CBOR 的协议时的一些考量. 它仅为建议性质, 并且明确排除 RFC 2119 中的任何措辞, 除了那些在 RFC 2119 意义上可被解释为 "MAY" 的词.

3.1. 流式应用中的 CBOR​

在流式应用中, 数据流可以由一连串首尾相接的 CBOR 数据项组成. 在这种环境中, 如果在前一个数据项结束之后又发现了数据, 解码器就立即开始解码一个新的数据项.

组成一个数据项的字节可能并非全部立即可供解码器使用; 有些解码器会缓冲额外的数据, 直到可以向应用呈现一个完整的数据项. 另一些解码器则可以向应用呈现关于某个顶层数据项的部分信息, 例如已经可以解码的嵌套数据项, 甚至尚未完全到达的字节串的某些部分.

注意, 有些应用和协议不会想使用不定长编码. 使用不定长编码使编码器无需为了计数而编组所有数据, 但它要求解码器在等待该项结束期间分配越来越多的内存. 这对某些应用可能没问题, 对其他应用则不然.

3.2. 通用编码器和解码器​

通用 CBOR 解码器可以解码所有格式良好的 CBOR 数据并将其呈现给应用. 如果 CBOR 数据以 CBOR 所定义的方式使用初始字节以及由其值所隐含的字节串和/或数据项, 且其后不跟随无关数据, 那么它就是格式良好的 (附录 C).

尽管 CBOR 力图尽量减少这些情况, 但并非所有格式良好的 CBOR 数据都是有效的: 例如, 该格式排除了那些用扩展字节编码的、小于 32 的简单值. 此外, 特定标签可能施加可能被违反的语义约束, 例如在大数标签中再包含一个标签, 或在日期标签内后跟一个字节串. 最后, 数据可能是无效的, 例如无效的 UTF-8 字符串或不符合 [RFC3339] 的日期字符串. 并不要求通用编码器和解码器为其应用接口做出不自然的选择, 以便能够处理无效数据. 通用编码器和解码器预期会转发简单值和标签, 即使其具体的码点在编写该编码器/解码器时尚未注册 (第 3.5 节).

通用解码器提供了一些方法, 将格式良好的 CBOR 值 (无论有效还是无效) 呈现给应用. 诊断记法 (第 6 节) 可用于将格式良好的 CBOR 值呈现给人类.

通用编码器提供一种应用接口, 允许应用指定任何格式良好的值, 包括编码器不认识的简单值和标签.

3.3. 语法错误​

解码器遇到一个格式不良好的 CBOR 数据项时, 通常可以选择让解码完全失败 (发出错误和/或完全停止处理), 使用解码器特有的、能清楚表明出现了问题的约定来替换有问题的数据和数据项, 或者采取其他某种动作.

3.3.1. 不完整的 CBOR 数据项​

CBOR 数据项的表示具有特定的长度, 由其初始字节以及该数据项内所包含的任何数据项的结构决定. 如果可用的数据更少, 这可以被视为语法错误. 解码器也可以实现增量解析, 也就是说, 在数据可用的范围内解码该数据项并呈现目前发现的数据 (例如在基于事件的接口中), 并可以选择在更多数据可用后继续解码.

不完整数据项的示例包括:

  • 解码器预期有一定数量的数组或映射条目, 却遇到了数据的结尾.

  • 解码器处理它认为是映射中最后一个对的内容, 却来到了数据的结尾.

  • 解码器刚刚看到一个标签, 随后就遇到了数据的结尾.

  • 解码器已经看到一个不定长项的开头, 却在看到 "break" 停止码之前遇到了数据的结尾.

3.3.2. 畸变的不定长项​

畸变的不定长数据项的示例包括:

  • 在不定长字节串或文本串内, 解码器在找到 "break" 停止码之前发现了一个主类型不合适的项.

  • 在不定长映射内, 解码器在读取一个键之后立即遇到了 "break" 停止码 (缺少值).

另一种错误是在数据中某个位置发现 "break" 停止码, 而该位置并没有紧邻的 (未闭合的) 不定长项将其包含.

3.3.3. 未知的附加信息值​

在撰写本文时, 有些附加信息值尚未分配, 保留给本文档的未来版本 (见第 5.2 节). 由于这些附加信息值的整体语法尚未定义, 因此看到它不理解的附加信息值的解码器无法继续解析.

3.4. 其他解码错误​

一个 CBOR 数据项可能在语法上格式良好, 但在用 CBOR 数据模型解释其中编码的数据时存在问题. 一般而言, 发现具有此类问题的数据项的解码器可能会发出警告, 可能会完全停止处理, 可能会处理该错误并将有问题的值原样提供给应用, 或者采取其他某种类型的动作.

此类问题可能包括:

映射中的重复键: 通用解码器 (第 3.2 节) 使用原生 CBOR 数据模型向应用提供数据. 该数据模型包含映射 (具有唯一键的键值映射), 而不包含多重映射 (多个条目可以具有相同键的键值映射). 因此, 收到具有重复键的 CBOR 映射项的通用解码器将解码为只保留该键一个实例的映射, 或者它可能完全停止处理. 另一方面, "流式解码器" 甚至可能无法察觉 (第 3.7 节).

标签之后的值上的不可接受类型: 标签 (第 2.4 节) 规定了标签之后应当跟随何种类型的数据项; 例如, 正大数或负大数的标签应当放在字节串上. 将被加标签的数据项解码为原生表示 (在本例中为原生大整数) 的解码器, 预期会检查被加标签的数据项的类型. 即使其环境中没有此类原生表示可用的解码器, 也可以对它们所知道的那些标签执行检查并作出适当反应.

无效的 UTF-8 字符串: 解码器可能想、也可能不想验证 UTF-8 字符串 (主类型 3) 中的字节序列是否确实是有效的 UTF-8, 并作出适当反应.

3.5. 处理未知的简单值和标签​

解码器遇到它不认识的简单值 (第 2.3 节) 时, 例如在解码器部署之后才添加到 IANA 注册表的值, 或解码器选择不实现的值, 它可能会发出警告, 可能会完全停止处理, 可能会通过将未知值原样提供给应用来处理该错误 (这正是通用解码器所应做的), 或者采取其他某种类型的动作.

解码器遇到它不认识的标签 (第 2.4 节) 时, 例如在解码器部署之后才添加到 IANA 注册表的标签, 或解码器选择不实现的标签, 它可能会发出警告, 可能会完全停止处理, 可能会处理该错误并将未知标签值连同所包含的数据项一起呈现给应用 (这正是通用解码器所应做的), 可能会忽略该标签而只将所包含的数据项呈现给应用, 或者采取其他某种类型的动作.

3.6. 数字​

就本规范而言, 同一数值的所有数字表示都是等价的. 这意味着编码器可以把浮点值 0.0 编码为整数 0. 然而, 这也意味着, 如果编码器认为浮点值更可取 (例如当浮点值比 64 位整数更紧凑时), 那么原本期望只找到整数值的应用可能会发现浮点值.

使用 CBOR 的应用或协议可能会限制数字的表示形式. 例如, 只处理整数的协议可能会规定不得使用浮点数, 并且该协议的解码器不需要能够处理浮点数. 类似地, 使用 CBOR 的协议或应用可能会规定解码器需要能够处理任一类型的数字.

基于 CBOR 的协议应当考虑到, 不同的语言环境对可表示数字的范围和精度施加了不同的限制. 例如, JavaScript 的数字系统将所有数字都视为浮点, 这可能导致在解码有效位数超过 53 位的整数时静默丢失精度. 使用数字的协议应当规定其对解码器和接收应用处理非平凡数字的期望.

包含浮点数的基于 CBOR 的协议可以限制三种格式 (半精度、单精度和双精度) 中哪些需要支持. 对于仅使用整数的应用, 协议可能想完全排除浮点值的使用.

为紧凑性而设计的基于 CBOR 的协议可能想排除那些对应用而言长于所需的特定整数编码, 例如为了省去实现 64 位整数的需要. 存在一种期望, 即编码器将使用能够表示给定值的最紧凑整数表示. 然而, 只要应用能够解码给定大小的整数, 紧凑的应用就应当接受使用长于所需编码的值 (例如将 "0" 编码为 0b000_11101 后跟两个字节 0x00).

3.7. 为映射指定键​

编码应用和解码应用需要就映射中将使用哪些类型的键达成一致. 在需要与基于 JSON 的应用互操作的应用中, 键可能应当仅限于 UTF-8 字符串; 否则, 就必须规定从其他 CBOR 类型到 Unicode 字符的映射, 而这往往会导致实现错误. 在键本质上是数字、且键的数字顺序对应用很重要的应用中, 直接用这些数字作为键是很有用的.

如果要使用多种类型的键, 就应当考虑这些类型在将要使用的特定编程环境中会如何表示. 例如, 在 JavaScript 对象中, 整数键 1 无法与字符串键 "1" 区分开来. 这意味着, 如果使用了整数键, 就需要避免同时使用看起来像数字的字符串键. 这同样会得出一个结论: 键应当是单一 CBOR 类型.

在解码数据项时立即交付 CBOR 数据项内嵌套数据项的解码器 ("流式解码器") 通常不保留确定映射中键唯一性所必需的状态. 类似地, 可以在外层数据项完全可用之前就开始编码数据项的编码器 ("流式编码器") 可能想通过依赖其数据源来维持唯一性, 从而显著降低其开销.

基于 CBOR 的协议应当就接收应用确实在映射中看到多个相同键时该怎么做作出有意为之的决定. 协议中由此得出的规则应当尊重 CBOR 数据模型: 它不能规定对具有相同键的条目的具体处理方式, 但可以有一条规则, 规定映射中出现相同键表示该映射畸变, 且解码器必须报错停止. 使用严格模式 (第 3.10 节) 的 CBOR 解码器也禁止重复键.

映射的 CBOR 数据模型不允许将语义赋予映射表示中键/值对的顺序. 因此, 以改变映射中键/值对顺序就会改变语义 (除缓存使用等琐碎方面之外) 的方式来定义基于 CBOR 的协议, 将是非常糟糕的做法. (基于 CBOR 的协议可以规定特定的序列化顺序, 例如用于规范化.)

键不超过 24 个常用键的映射的受限设备应用, 应当考虑使用小整数 (而那些常用键多达 48 个的应用还应当考虑使用小的负整数), 因为这样这些键就可以编码在单个字节中.

3.8. 未定义值​

在一些基于 CBOR 的协议中, 编码器可能会使用 Undefined 这一简单值 (第 2.3 节) 作为对存在编码问题的数据项的替代, 以便让其余外层数据项能够无损地编码.

3.9. 规范 CBOR​

有些协议可能想要求编码器只以某种特定的规范格式发出 CBOR; 这些协议也可能要求解码器检查其输入是否为规范的. 这些协议可以自由定义它们所说的规范格式是什么意思, 以及编码器和解码器应当做什么. 本节为这类协议列出一些建议.

如果某个协议认为 "规范" 意味着两个编码器实现从相同输入数据出发会产生相同的 CBOR 输出, 那么以下四条规则就足够了:

  • 整数必须尽可能小.

    • 0 到 23 和 -1 到 -24 必须与主类型表达在同一字节中;

    • 24 到 255 和 -25 到 -256 必须只用一个额外的 uint8_t 来表达;

    • 256 到 65535 和 -257 到 -65536 必须只用一个额外的 uint16_t 来表达;

    • 65536 到 4294967295 和 -65537 到 -4294967296 必须只用一个额外的 uint32_t 来表达.

  • 主类型 2 到 5 中长度的表达必须尽可能短. 这些长度的规则遵循上述关于整数的规则.

  • 每个映射中的键必须按值从低到高排序. 排序是在键数据项的表示的字节上进行的, 而不考虑主类型的 3/5 位拆分. (注意, 该规则允许映射具有不同类型的键, 尽管这很可能是一种糟糕的做法, 可能在某些规范化实现中导致错误.) 排序规则如下:

    • 如果两个键长度不同, 较短的那个排在前面;

    • 如果两个键长度相同, 则按 (逐字节) 字典序值较低的那个排在前面.

  • 不定长项必须转为定长项.

如果某个协议允许 IEEE 浮点数, 那么可能需要增加额外的规范化规则. 一个示例规则可能是让所有浮点数先以 64 位浮点数开始, 然后做一次到 32 位浮点数的测试转换; 如果结果数值相同, 就使用较短的值, 并用一次到 16 位浮点数的测试转换重复该过程. (该规则对正负 Infinity 也选择 16 位浮点数.) 此外, NaN 有许多种表示. 如果 NaN 是允许的值, 它必须始终表示为 0xf97e00.

CBOR 标签为规范化带来了额外的考量. 规范格式中标签的缺席或存在由协议中标签的可选性决定. 在允许任意位置可选标签的基于 CBOR 的协议中, 规范格式不得允许它们. 在要求某些位置必须有标签的协议中, 标签需要出现在规范格式中. 使用规范化的基于 CBOR 的协议也可以改为规定: 消息中出现的所有标签无论是否可选都必须保留.

3.10. 严格模式​

CBOR 的一些应用领域不要求规范化 (第 3.9 节), 但可能要求不同的解码器即使在存在潜在恶意数据的情况下也能得到相同的 (语义等价的) 结果. 如果一个应用 (例如防火墙或其他保护实体) 基于另一个独立解码同一数据的应用所依赖的数据作出决定, 就可能需要这样做.

通常情况下, 避免可被歧义解码的数据是发送方的责任. 然而, 发送方可能是一个攻击者, 专门构造出会被不同解码器以不同方式解释的 CBOR 数据, 试图将其作为漏洞加以利用. 在此类问题可能存在的情况下, 应用中使用的通用解码器需要支持一种严格模式, 在该模式下拒绝可被歧义解码的数据同样是接收方的责任. 预期防火墙和其他解码 CBOR 的安全系统只会以严格模式进行解码.

严格模式下的解码器将可靠地拒绝任何可能被其他解码器以不同方式解释的数据. 它将可靠地拒绝存在语法错误的数据项 (第 3.3 节). 它还将付出努力来可靠地检测其他解码错误 (第 3.4 节). 特别地, 对于包含以下任一情况的 CBOR 数据项, 严格解码器需要有一个报错 (且不返回数据) 的 API:

  • 具有多个相同键条目的映射 (主类型 5)

  • 用在类型不正确数据项上的标签

  • 对其给定类型而言格式不正确的数据项, 例如无效的 UTF-8, 或无法用其所带特定标签解释的数据

严格模式下的解码器在遇到它不认识的标签或简单值时, 可以做以下两件事之一:

  • 它可以报错 (且不返回数据).

  • 它可以把未知项 (类型、值, 对于标签还包括解码后的被加标签数据项) 连同解码器未识别出该标签或简单值的指示一起发出给调用该解码器的应用.

后一种做法也适用于非严格解码器, 它支持与新注册的标签和简单值的前向兼容, 而无需在更新调用应用的同时更新编码器. (为此, 解码器的 API 需要有一种标记未知项的方式, 以便调用应用能够以适合该程序的方式处理它们.)

由于其中一些处理可能带来可观的成本 (特别是在映射的重复检测方面), 严格模式的支持并不是对所有 CBOR 解码器的要求.

有些编码器会依赖其应用以能够产生无歧义可解码 CBOR 结果的方式提供输入数据. 通用编码器也可能想提供一种严格模式, 在其中无论其应用是否提供符合 API 的数据, 它都可靠地将输出限制为无歧义可解码的 CBOR.